@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
@@ -129,6 +129,11 @@ export interface ComponentProps {
129
129
  | undefined;
130
130
  searchPlaceholder?: string | undefined;
131
131
  noResultsText?: string | undefined;
132
+ /*
133
+ * Defaults to visible for direct/legacy consumers. Dashboard hides it when
134
+ * its command palette owns Cmd/Ctrl+K instead of this products menu.
135
+ */
136
+ showCommandKShortcutHint?: boolean | undefined;
132
137
  keyboardHint?: string | undefined;
133
138
  recentLabel?: string | undefined;
134
139
  onClose: () => void;
@@ -502,14 +507,14 @@ const NavBarMenuModal: FunctionComponent<ComponentProps> = (
502
507
  >
503
508
  <Icon icon={IconProp.Close} className="h-4 w-4" />
504
509
  </button>
505
- ) : (
510
+ ) : props.showCommandKShortcutHint !== false ? (
506
511
  /* OS-appropriate hint: ⌘ K on a Mac, Ctrl K elsewhere. */
507
512
  <KeyboardShortcut
508
513
  keys={[KeyboardKey.Mod, "K"]}
509
514
  size={KeyboardShortcutSize.Small}
510
515
  className="hidden sm:inline-flex"
511
516
  />
512
- )}
517
+ ) : null}
513
518
  </div>
514
519
 
515
520
  {/* Body */}
@@ -21,6 +21,7 @@ export const REFRESH_SIDEBAR_COUNT_EVENT: string = "REFRESH_SIDEBAR_COUNTS";
21
21
 
22
22
  export interface ComponentProps<TBaseModel extends BaseModel> {
23
23
  link: Link;
24
+ activeRoute?: Link["to"] | undefined;
24
25
  modelType?: { new (): TBaseModel } | undefined;
25
26
  badgeType?: BadgeType | undefined;
26
27
  countQuery?: Query<TBaseModel> | undefined;
@@ -136,6 +137,7 @@ const CountModelSideMenuItem: <TBaseModel extends BaseModel>(
136
137
  return (
137
138
  <SideMenuItem
138
139
  link={props.link}
140
+ activeRoute={props.activeRoute}
139
141
  badge={!isLoading && !error ? count : undefined}
140
142
  badgeType={props.badgeType}
141
143
  icon={props.icon}
@@ -20,6 +20,8 @@ import { RequestOptions } from "../../Utils/ModelAPI/ModelAPI";
20
20
 
21
21
  export interface SideMenuItemProps {
22
22
  link: Link;
23
+ // Use a different route for selection when the link target also changes query state.
24
+ activeRoute?: Link["to"] | undefined;
23
25
  showAlert?: boolean;
24
26
  showWarning?: boolean;
25
27
  badge?: number;
@@ -106,7 +108,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
106
108
  if (props.sections) {
107
109
  for (const section of props.sections) {
108
110
  for (const item of section.items) {
109
- if (Navigation.isOnThisPage(item.link.to)) {
111
+ if (Navigation.isOnThisPage(item.activeRoute || item.link.to)) {
110
112
  const result: {
111
113
  sectionTitle: string;
112
114
  itemTitle: string;
@@ -127,7 +129,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
127
129
  // Check direct items
128
130
  if (props.items) {
129
131
  for (const item of props.items) {
130
- if (Navigation.isOnThisPage(item.link.to)) {
132
+ if (Navigation.isOnThisPage(item.activeRoute || item.link.to)) {
131
133
  const result: {
132
134
  itemTitle: string;
133
135
  icon?: IconProp;
@@ -184,9 +186,13 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
184
186
  icon?: IconProp;
185
187
  defaultCollapsed?: boolean;
186
188
  collapsible?: boolean;
189
+ isActive: boolean;
187
190
  } = {
188
191
  key: `section-${sectionIndex}`,
189
192
  title: section.title,
193
+ isActive: section.items.some((item: SideMenuItemProps): boolean => {
194
+ return Navigation.isOnThisPage(item.activeRoute || item.link.to);
195
+ }),
190
196
  };
191
197
 
192
198
  if (section.icon) {
@@ -209,6 +215,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
209
215
  <CountModelSideMenuItem
210
216
  key={`section-${sectionIndex}-count-item-${itemIndex}`}
211
217
  link={item.link}
218
+ activeRoute={item.activeRoute}
212
219
  badgeType={item.badgeType}
213
220
  modelType={item.modelType as any}
214
221
  countQuery={item.countQuery as any}
@@ -225,6 +232,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
225
232
  <SideMenuItem
226
233
  key={`section-${sectionIndex}-item-${itemIndex}`}
227
234
  link={item.link}
235
+ activeRoute={item.activeRoute}
228
236
  showAlert={item.showAlert}
229
237
  showWarning={item.showWarning}
230
238
  badge={item.badge}
@@ -252,6 +260,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
252
260
  <CountModelSideMenuItem
253
261
  key={`count-item-${itemIndex}`}
254
262
  link={item.link}
263
+ activeRoute={item.activeRoute}
255
264
  badgeType={item.badgeType}
256
265
  modelType={item.modelType as any}
257
266
  countQuery={item.countQuery as any}
@@ -267,6 +276,7 @@ const SideMenu: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
267
276
  <SideMenuItem
268
277
  key={`item-${itemIndex}`}
269
278
  link={item.link}
279
+ activeRoute={item.activeRoute}
270
280
  showAlert={item.showAlert}
271
281
  showWarning={item.showWarning}
272
282
  badge={item.badge}
@@ -9,6 +9,7 @@ import React, { FunctionComponent } from "react";
9
9
 
10
10
  export interface ComponentProps {
11
11
  link: Link;
12
+ activeRoute?: Link["to"] | undefined;
12
13
  showAlert?: undefined | boolean;
13
14
  showWarning?: undefined | boolean;
14
15
  badge?: undefined | number;
@@ -23,7 +24,9 @@ const SideMenuItem: FunctionComponent<ComponentProps> = (
23
24
  props: ComponentProps,
24
25
  ) => {
25
26
  const { translateString } = useTranslateValue();
26
- const isActive: boolean = Navigation.isOnThisPage(props.link.to);
27
+ const isActive: boolean = Navigation.isOnThisPage(
28
+ props.activeRoute || props.link.to,
29
+ );
27
30
  const isSubItemActive: boolean = props.subItemLink
28
31
  ? Navigation.isOnThisPage(props.subItemLink.to)
29
32
  : false;
@@ -1,7 +1,12 @@
1
1
  import Icon from "../Icon/Icon";
2
2
  import useTranslateValue from "../../Utils/Translation";
3
3
  import IconProp from "../../../Types/Icon/IconProp";
4
- import React, { FunctionComponent, ReactElement, useState } from "react";
4
+ import React, {
5
+ FunctionComponent,
6
+ ReactElement,
7
+ useEffect,
8
+ useState,
9
+ } from "react";
5
10
 
6
11
  export interface ComponentProps {
7
12
  title: string;
@@ -9,6 +14,7 @@ export interface ComponentProps {
9
14
  defaultCollapsed?: boolean;
10
15
  collapsible?: boolean;
11
16
  icon?: IconProp;
17
+ isActive?: boolean;
12
18
  }
13
19
 
14
20
  const SideMenuSection: FunctionComponent<ComponentProps> = (
@@ -16,12 +22,18 @@ const SideMenuSection: FunctionComponent<ComponentProps> = (
16
22
  ) => {
17
23
  const { translateString } = useTranslateValue();
18
24
  const [isCollapsed, setIsCollapsed] = useState<boolean>(
19
- props.defaultCollapsed || false,
25
+ Boolean(props.defaultCollapsed && !props.isActive),
20
26
  );
21
27
  const translatedTitle: string = translateString(props.title) || props.title;
22
28
 
23
29
  const isCollapsible: boolean = props.collapsible ?? true;
24
30
 
31
+ useEffect(() => {
32
+ if (props.isActive) {
33
+ setIsCollapsed(false);
34
+ }
35
+ }, [props.isActive]);
36
+
25
37
  const handleToggle: () => void = (): void => {
26
38
  if (isCollapsible) {
27
39
  setIsCollapsed(!isCollapsed);
@@ -0,0 +1,77 @@
1
+ import DashboardTimeRangeZoomUtil, {
2
+ DashboardTimeRangeZoomState,
3
+ } from "../../Utils/Dashboard/DashboardTimeRangeZoom";
4
+ import RangeStartAndEndDateTime from "../../Types/Time/RangeStartAndEndDateTime";
5
+ import { useCallback, useState } from "react";
6
+
7
+ export interface DashboardTimeRangeZoom {
8
+ /** The window to hand every widget on the board. */
9
+ startAndEndDate: RangeStartAndEndDateTime;
10
+ /** True while a panel drag-selection is narrowing the board. */
11
+ isZoomed: boolean;
12
+ /** An explicit pick from the time-range picker — a new baseline. */
13
+ setStartAndEndDate: (range: RangeStartAndEndDateTime) => void;
14
+ /** A drag-selection on a time-series panel — zooms the whole board. */
15
+ zoomToTimeRange: (startTime: Date, endTime: Date) => void;
16
+ /** Double-click on a panel, or the toolbar's reset — undoes the zoom. */
17
+ resetZoom: () => void;
18
+ }
19
+
20
+ export type UseDashboardTimeRangeZoomFunction = (
21
+ initialRange: RangeStartAndEndDateTime,
22
+ ) => DashboardTimeRangeZoom;
23
+
24
+ /**
25
+ * Owns the dashboard-wide time range and the one level of undo that makes
26
+ * drag-to-zoom reversible. Both dashboard shells (the authenticated
27
+ * DashboardView and the public DashboardViewPage) use this so a panel
28
+ * gesture means the same thing on both.
29
+ *
30
+ * All three callbacks are identity-stable for the life of the dashboard:
31
+ * they are handed down through the canvas into React.memo'd widgets whose
32
+ * comparators do not look at function props, so a changing reference would
33
+ * leave a widget holding a stale closure.
34
+ */
35
+ const useDashboardTimeRangeZoom: UseDashboardTimeRangeZoomFunction = (
36
+ initialRange: RangeStartAndEndDateTime,
37
+ ): DashboardTimeRangeZoom => {
38
+ const [state, setState] = useState<DashboardTimeRangeZoomState>(() => {
39
+ return DashboardTimeRangeZoomUtil.getInitialState(initialRange);
40
+ });
41
+
42
+ const setStartAndEndDate: (range: RangeStartAndEndDateTime) => void =
43
+ useCallback((range: RangeStartAndEndDateTime): void => {
44
+ setState((previous: DashboardTimeRangeZoomState) => {
45
+ return DashboardTimeRangeZoomUtil.selectRange(previous, range);
46
+ });
47
+ }, []);
48
+
49
+ const zoomToTimeRange: (startTime: Date, endTime: Date) => void = useCallback(
50
+ (startTime: Date, endTime: Date): void => {
51
+ setState((previous: DashboardTimeRangeZoomState) => {
52
+ return DashboardTimeRangeZoomUtil.zoomToWindow(
53
+ previous,
54
+ startTime,
55
+ endTime,
56
+ );
57
+ });
58
+ },
59
+ [],
60
+ );
61
+
62
+ const resetZoom: () => void = useCallback((): void => {
63
+ setState((previous: DashboardTimeRangeZoomState) => {
64
+ return DashboardTimeRangeZoomUtil.resetZoom(previous);
65
+ });
66
+ }, []);
67
+
68
+ return {
69
+ startAndEndDate: state.current,
70
+ isZoomed: DashboardTimeRangeZoomUtil.isZoomed(state),
71
+ setStartAndEndDate,
72
+ zoomToTimeRange,
73
+ resetZoom,
74
+ };
75
+ };
76
+
77
+ export default useDashboardTimeRangeZoom;
@@ -0,0 +1,118 @@
1
+ import InBetween from "../../Types/BaseDatabase/InBetween";
2
+ import RangeStartAndEndDateTime from "../../Types/Time/RangeStartAndEndDateTime";
3
+ import TimeRange from "../../Types/Time/TimeRange";
4
+
5
+ /**
6
+ * The dashboard's time window, plus the window to go back to when a zoom
7
+ * is undone.
8
+ *
9
+ * A dashboard has exactly ONE time range and every widget renders it, so
10
+ * drag-selecting a window on a time-series panel is a statement about the
11
+ * whole board, not about that one panel. `baseline` is what makes the
12
+ * gesture reversible: the first zoom records the range that was in effect
13
+ * before it, later zooms keep that same original, and a reset restores it.
14
+ * `baseline === null` is the canonical "not zoomed" signal — the toolbar's
15
+ * reset affordance and the panels' double-click-to-reset both key off it.
16
+ */
17
+ export interface DashboardTimeRangeZoomState {
18
+ /** The window every widget on the board is currently querying. */
19
+ current: RangeStartAndEndDateTime;
20
+ /** Window a reset returns to; null when no zoom is active. */
21
+ baseline: RangeStartAndEndDateTime | null;
22
+ }
23
+
24
+ export default class DashboardTimeRangeZoomUtil {
25
+ public static getInitialState(
26
+ range: RangeStartAndEndDateTime,
27
+ ): DashboardTimeRangeZoomState {
28
+ return {
29
+ current: range,
30
+ baseline: null,
31
+ };
32
+ }
33
+
34
+ public static isZoomed(state: DashboardTimeRangeZoomState): boolean {
35
+ return state.baseline !== null;
36
+ }
37
+
38
+ /**
39
+ * An explicit pick from the time-range picker. That is a new baseline,
40
+ * not a zoom: it drops any pending "return to" window, so the reset
41
+ * affordance disappears instead of offering to jump back to a range the
42
+ * user has deliberately moved on from.
43
+ */
44
+ public static selectRange(
45
+ _state: DashboardTimeRangeZoomState,
46
+ range: RangeStartAndEndDateTime,
47
+ ): DashboardTimeRangeZoomState {
48
+ return {
49
+ current: range,
50
+ baseline: null,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Drag-selected window from a time-series panel. Returns the state
56
+ * unchanged (same reference, so callers can skip a render) when the
57
+ * selection is not a usable window — the chart library can hand back a
58
+ * zero-width or invalid pair when a drag never left its starting bucket.
59
+ */
60
+ public static zoomToWindow(
61
+ state: DashboardTimeRangeZoomState,
62
+ startTime: Date,
63
+ endTime: Date,
64
+ ): DashboardTimeRangeZoomState {
65
+ if (!(startTime instanceof Date) || !(endTime instanceof Date)) {
66
+ return state;
67
+ }
68
+
69
+ const startTimeInMs: number = startTime.getTime();
70
+ const endTimeInMs: number = endTime.getTime();
71
+
72
+ if (Number.isNaN(startTimeInMs) || Number.isNaN(endTimeInMs)) {
73
+ return state;
74
+ }
75
+
76
+ // A drag right-to-left is the same window as left-to-right.
77
+ const lowerInMs: number = Math.min(startTimeInMs, endTimeInMs);
78
+ const upperInMs: number = Math.max(startTimeInMs, endTimeInMs);
79
+
80
+ if (lowerInMs === upperInMs) {
81
+ return state;
82
+ }
83
+
84
+ return {
85
+ current: {
86
+ range: TimeRange.CUSTOM,
87
+ startAndEndDate: new InBetween<Date>(
88
+ new Date(lowerInMs),
89
+ new Date(upperInMs),
90
+ ),
91
+ },
92
+ /*
93
+ * Zooming again while already zoomed keeps the ORIGINAL baseline, so
94
+ * one reset always lands back where the investigation started rather
95
+ * than unwinding one drag at a time.
96
+ */
97
+ baseline: state.baseline || state.current,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Undo the zoom. A no-op (same state reference) when nothing is zoomed,
103
+ * so a stray double-click on an unzoomed dashboard cannot retime the
104
+ * board or spend a refetch.
105
+ */
106
+ public static resetZoom(
107
+ state: DashboardTimeRangeZoomState,
108
+ ): DashboardTimeRangeZoomState {
109
+ if (state.baseline === null) {
110
+ return state;
111
+ }
112
+
113
+ return {
114
+ current: state.baseline,
115
+ baseline: null,
116
+ };
117
+ }
118
+ }
@@ -0,0 +1,232 @@
1
+ import {
2
+ NetworkTopologyDeviceRole,
3
+ NetworkTopologyNode,
4
+ NetworkTopologyNodeShape,
5
+ } from "../../Types/Monitor/SnmpMonitor/NetworkTopology";
6
+
7
+ /*
8
+ * The project's configured device roles, reduced to what the topology needs.
9
+ *
10
+ * WHY THIS EXISTS. Roles are rows now (NetworkDeviceRole), which means the
11
+ * label a role is drawn with, the silhouette it uses, whether it sits at the
12
+ * core and whether it is worth walking with SNMP are all per-project answers.
13
+ * None of them can be compiled into the client any more.
14
+ *
15
+ * The client is NOT given the table to look things up in, though. The topology
16
+ * builder stamps the four answers onto each node as it builds it, so every
17
+ * consumer downstream - the shape module, three layouts, the legend, the
18
+ * detail panel, the accessible label - keeps its current pure signature and
19
+ * needs no catalogue threaded through it. This module is the one place that
20
+ * turns rows into that stamp.
21
+ *
22
+ * Pure and dependency-free so it runs identically on the server, in the
23
+ * builder, and in the unit tests.
24
+ */
25
+
26
+ /** One configured role, as the topology builder needs it. */
27
+ export interface TopologyDeviceRoleInput {
28
+ id?: string | undefined;
29
+ key: string;
30
+ name: string;
31
+ topologyShape?: string | undefined;
32
+ isCoreLayer?: boolean | undefined;
33
+ isSnmpWalkable?: boolean | undefined;
34
+ }
35
+
36
+ /** What gets stamped onto a node. Every field optional - see stampForRoleKey. */
37
+ export interface TopologyNodeRoleStamp {
38
+ roleId?: string | undefined;
39
+ roleKey?: string | undefined;
40
+ roleLabel?: string | undefined;
41
+ roleShape?: NetworkTopologyNodeShape | undefined;
42
+ isCoreLayerRole?: boolean | undefined;
43
+ isSnmpWalkableRole?: boolean | undefined;
44
+ }
45
+
46
+ /*
47
+ * The silhouettes the renderer can actually draw. A row's topologyShape is a
48
+ * free-text column - the geometry lives in the client and adding a shape must
49
+ * not need a migration - so a value that is not one of these is dropped rather
50
+ * than passed through to a renderer that would have no path for it.
51
+ */
52
+ const KNOWN_SHAPES: ReadonlySet<string> = new Set<string>([
53
+ "circle",
54
+ "rounded-square",
55
+ "diamond",
56
+ "triangle",
57
+ "hexagon",
58
+ "tower",
59
+ "cylinder",
60
+ "rect",
61
+ ]);
62
+
63
+ export type NormalizeRoleKeyFunction = (
64
+ value: string | undefined | null,
65
+ ) => string | undefined;
66
+
67
+ /*
68
+ * Keys are matched case- and whitespace-insensitively, the same way
69
+ * parseDeviceRoleOverride reads the stored column, so a hand-edited or
70
+ * imported "Router " still finds the Router row.
71
+ */
72
+ export const normalizeRoleKey: NormalizeRoleKeyFunction = (
73
+ value: string | undefined | null,
74
+ ): string | undefined => {
75
+ const normalized: string = (value || "").trim().toLowerCase();
76
+ return normalized || undefined;
77
+ };
78
+
79
+ /**
80
+ * Index the project's roles for lookup by key.
81
+ *
82
+ * Later rows do not overwrite earlier ones on a key clash. The key is unique
83
+ * per project in the database, so a clash here means the caller passed rows
84
+ * from more than one project, and quietly taking the last one would draw the
85
+ * wrong project's shapes.
86
+ */
87
+ export type BuildRoleIndexFunction = (
88
+ roles: ReadonlyArray<TopologyDeviceRoleInput>,
89
+ ) => Map<string, TopologyDeviceRoleInput>;
90
+
91
+ export const buildDeviceRoleIndex: BuildRoleIndexFunction = (
92
+ roles: ReadonlyArray<TopologyDeviceRoleInput>,
93
+ ): Map<string, TopologyDeviceRoleInput> => {
94
+ const index: Map<string, TopologyDeviceRoleInput> = new Map<
95
+ string,
96
+ TopologyDeviceRoleInput
97
+ >();
98
+
99
+ for (const role of roles) {
100
+ const key: string | undefined = normalizeRoleKey(role.key);
101
+ if (!key || index.has(key)) {
102
+ continue;
103
+ }
104
+ index.set(key, role);
105
+ }
106
+
107
+ return index;
108
+ };
109
+
110
+ export type StampForRoleKeyFunction = (
111
+ roleKey: string | undefined,
112
+ index: Map<string, TopologyDeviceRoleInput>,
113
+ ) => TopologyNodeRoleStamp;
114
+
115
+ /**
116
+ * The stamp for one role key.
117
+ *
118
+ * Returns an EMPTY object when the project has no row for the key - which is
119
+ * the normal case for a project that has not been backfilled yet, and the
120
+ * permanent case for a project that deleted a seeded role the classifier can
121
+ * still produce. Every field a node does not get falls back to the built-in
122
+ * behaviour on the client, so an empty stamp draws exactly the map that was
123
+ * drawn before roles were configurable.
124
+ *
125
+ * The one field always present when a row IS found is `roleKey`, because the
126
+ * legend groups by it and a group with no key cannot be grouped.
127
+ */
128
+ export const stampForRoleKey: StampForRoleKeyFunction = (
129
+ roleKey: string | undefined,
130
+ index: Map<string, TopologyDeviceRoleInput>,
131
+ ): TopologyNodeRoleStamp => {
132
+ const normalized: string | undefined = normalizeRoleKey(roleKey);
133
+ if (!normalized) {
134
+ return {};
135
+ }
136
+
137
+ const role: TopologyDeviceRoleInput | undefined = index.get(normalized);
138
+ if (!role) {
139
+ return {};
140
+ }
141
+
142
+ const stamp: TopologyNodeRoleStamp = {
143
+ roleKey: role.key,
144
+ };
145
+
146
+ if (role.id) {
147
+ stamp.roleId = role.id;
148
+ }
149
+
150
+ if (role.name) {
151
+ stamp.roleLabel = role.name;
152
+ }
153
+
154
+ if (role.topologyShape && KNOWN_SHAPES.has(role.topologyShape)) {
155
+ stamp.roleShape = role.topologyShape as NetworkTopologyNodeShape;
156
+ }
157
+
158
+ if (role.isCoreLayer !== undefined) {
159
+ stamp.isCoreLayerRole = Boolean(role.isCoreLayer);
160
+ }
161
+
162
+ if (role.isSnmpWalkable !== undefined) {
163
+ stamp.isSnmpWalkableRole = Boolean(role.isSnmpWalkable);
164
+ }
165
+
166
+ return stamp;
167
+ };
168
+
169
+ export type RoleKeyForNodeFunction = (
170
+ assignedRoleKey: string | undefined,
171
+ classifiedRole: NetworkTopologyDeviceRole,
172
+ ) => string | undefined;
173
+
174
+ /**
175
+ * Which key a node's presentation should come from.
176
+ *
177
+ * The operator's assignment wins, exactly as it does for `role` itself. With
178
+ * no assignment the classifier's answer is used, so a project that renamed
179
+ * "Router" to "Edge Router" sees the new name on devices it never had to
180
+ * touch. "unknown" is not a key any row holds and is deliberately not looked
181
+ * up: it is the classifier declining to answer, and a neutral node is the
182
+ * honest drawing of that.
183
+ */
184
+ export const roleKeyForNode: RoleKeyForNodeFunction = (
185
+ assignedRoleKey: string | undefined,
186
+ classifiedRole: NetworkTopologyDeviceRole,
187
+ ): string | undefined => {
188
+ const assigned: string | undefined = normalizeRoleKey(assignedRoleKey);
189
+ if (assigned) {
190
+ return assigned;
191
+ }
192
+ if (classifiedRole === "unknown") {
193
+ return undefined;
194
+ }
195
+ return classifiedRole;
196
+ };
197
+
198
+ export type ApplyRoleStampFunction = (
199
+ node: NetworkTopologyNode,
200
+ stamp: TopologyNodeRoleStamp,
201
+ ) => void;
202
+
203
+ /**
204
+ * Copy a stamp onto a node, omitting the fields it does not carry.
205
+ *
206
+ * Assigning undefined rather than omitting would break the payload's own
207
+ * contract - "absent means fall back" - for any consumer that checks with
208
+ * `in` or serialises the node, so absent stays absent.
209
+ */
210
+ export const applyRoleStamp: ApplyRoleStampFunction = (
211
+ node: NetworkTopologyNode,
212
+ stamp: TopologyNodeRoleStamp,
213
+ ): void => {
214
+ if (stamp.roleId !== undefined) {
215
+ node.roleId = stamp.roleId;
216
+ }
217
+ if (stamp.roleKey !== undefined) {
218
+ node.roleKey = stamp.roleKey;
219
+ }
220
+ if (stamp.roleLabel !== undefined) {
221
+ node.roleLabel = stamp.roleLabel;
222
+ }
223
+ if (stamp.roleShape !== undefined) {
224
+ node.roleShape = stamp.roleShape;
225
+ }
226
+ if (stamp.isCoreLayerRole !== undefined) {
227
+ node.isCoreLayerRole = stamp.isCoreLayerRole;
228
+ }
229
+ if (stamp.isSnmpWalkableRole !== undefined) {
230
+ node.isSnmpWalkableRole = stamp.isSnmpWalkableRole;
231
+ }
232
+ };
@@ -538,7 +538,8 @@ export function labelForDeviceRole(
538
538
  * The classifier above is evidence-driven, and its evidence is SNMP: a
539
539
  * device nothing walks — a ping-only device imported by discovery, say —
540
540
  * offers it a hostname and nothing else, so it comes back "unknown" and
541
- * stays there forever. `NetworkDevice.deviceRole` is the operator saying
541
+ * stays there forever. The role an operator ASSIGNED the device
542
+ * (NetworkDevice.networkDeviceRole, whose key is what arrives here) is them saying
542
543
  * what the box actually is, and it is the only statement about a role that
543
544
  * is not an inference, so it outranks everything the classifier can find.
544
545
  *