@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
@@ -40,6 +40,7 @@ import useChartAnnotations, {
40
40
  UseChartAnnotationsResult,
41
41
  } from "../Annotations/UseChartAnnotations";
42
42
  import { cx } from "../Utils/Cx";
43
+ import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../Utils/DoubleClick";
43
44
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
44
45
  import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
45
46
  import {
@@ -625,6 +626,15 @@ interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
625
626
  formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
626
627
  onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
627
628
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
629
+ /**
630
+ * Double-click on the plot surface: the counterpart to drag-to-select,
631
+ * for undoing the zoom it produced. Supplying it makes single clicks
632
+ * wait DOUBLE_CLICK_DISAMBIGUATION_MS before they act, so a double-click
633
+ * cannot also fire the bucket-click path twice on its way past — hosts
634
+ * that have nothing to reset should leave it undefined and keep clicks
635
+ * instant.
636
+ */
637
+ onTimeRangeReset?: (() => void) | undefined;
628
638
  /**
629
639
  * Plain click on a time bucket (the plot background — dot/legend clicks
630
640
  * and drag-selections never reach this). [bucketStart, bucketEnd) uses
@@ -693,6 +703,7 @@ const AreaChart: React.ForwardRefExoticComponent<
693
703
  formattedTimeReferenceLines,
694
704
  formattedReferenceRegions,
695
705
  onTimeRangeSelect,
706
+ onTimeRangeReset,
696
707
  onBucketClick,
697
708
  ghostCategories,
698
709
  ...other
@@ -723,9 +734,26 @@ const AreaChart: React.ForwardRefExoticComponent<
723
734
  React.useRef<number | null>(null);
724
735
  const suppressNextClickRef: React.MutableRefObject<boolean> =
725
736
  React.useRef<boolean>(false);
737
+ const hasOnTimeRangeReset: boolean = Boolean(onTimeRangeReset);
738
+ /*
739
+ * A pending single-click, held open long enough for a second click to
740
+ * cancel it. Only armed when onTimeRangeReset is supplied.
741
+ */
742
+ const pendingClickTimeoutRef: React.MutableRefObject<ReturnType<
743
+ typeof setTimeout
744
+ > | null> = React.useRef<ReturnType<typeof setTimeout> | null>(null);
726
745
  const categoryColors: Map<string, ChartColorValue> =
727
746
  constructCategoryColors(categories, colors);
728
747
 
748
+ React.useEffect(() => {
749
+ return () => {
750
+ if (pendingClickTimeoutRef.current !== null) {
751
+ clearTimeout(pendingClickTimeoutRef.current);
752
+ pendingClickTimeoutRef.current = null;
753
+ }
754
+ };
755
+ }, []);
756
+
729
757
  /*
730
758
  * Annotations are drawn onto the categorical axis, so the layer needs
731
759
  * the same x values recharts is drawing — not the source dates.
@@ -971,6 +999,51 @@ const AreaChart: React.ForwardRefExoticComponent<
971
999
  onTimeRangeSelect(startDate, endDate);
972
1000
  }
973
1001
 
1002
+ /*
1003
+ * The plain-click path, split out so it can either run inline (no
1004
+ * reset handler) or be deferred behind the double-click window.
1005
+ */
1006
+ function handleChartClick(chartState: RangeSelectionChartState): void {
1007
+ /*
1008
+ * A click while a legend/dot selection is active keeps its
1009
+ * long-standing meaning — clear the selection — and never
1010
+ * also pins a bucket.
1011
+ */
1012
+ if (hasOnValueChange && (activeLegend || activeDot)) {
1013
+ setActiveDot(undefined);
1014
+ setActiveLegend(undefined);
1015
+ onValueChange?.(null);
1016
+ return;
1017
+ }
1018
+ if (!onBucketClick) {
1019
+ return;
1020
+ }
1021
+ const rowIndex: number | null = getRowIndexFromChartState(chartState);
1022
+ if (rowIndex === null) {
1023
+ return;
1024
+ }
1025
+ const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1026
+ if (!bucketStart) {
1027
+ return;
1028
+ }
1029
+ /*
1030
+ * Cover the full bucket: width from adjacent row dates, the
1031
+ * same derivation drag-to-select uses.
1032
+ */
1033
+ const adjacentBucketDate: Date | null =
1034
+ rowIndex > 0
1035
+ ? getBucketDateAtIndex(rowIndex - 1)
1036
+ : getBucketDateAtIndex(rowIndex + 1);
1037
+ const clickedBucketWidthInMs: number = adjacentBucketDate
1038
+ ? Math.abs(bucketStart.getTime() - adjacentBucketDate.getTime())
1039
+ : 0;
1040
+ onBucketClick(
1041
+ bucketStart,
1042
+ new Date(bucketStart.getTime() + clickedBucketWidthInMs),
1043
+ (data[rowIndex] || {}) as Record<string, number | string>,
1044
+ );
1045
+ }
1046
+
974
1047
  return (
975
1048
  <div
976
1049
  ref={ref}
@@ -1009,46 +1082,35 @@ const AreaChart: React.ForwardRefExoticComponent<
1009
1082
  if (suppressNextClickRef.current) {
1010
1083
  return;
1011
1084
  }
1012
- /*
1013
- * A click while a legend/dot selection is active keeps its
1014
- * long-standing meaning — clear the selection — and never
1015
- * also pins a bucket.
1016
- */
1017
- if (hasOnValueChange && (activeLegend || activeDot)) {
1018
- setActiveDot(undefined);
1019
- setActiveLegend(undefined);
1020
- onValueChange?.(null);
1021
- return;
1022
- }
1023
- if (!onBucketClick) {
1024
- return;
1025
- }
1026
- const rowIndex: number | null =
1027
- getRowIndexFromChartState(chartState);
1028
- if (rowIndex === null) {
1029
- return;
1030
- }
1031
- const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1032
- if (!bucketStart) {
1085
+ if (!hasOnTimeRangeReset) {
1086
+ handleChartClick(chartState);
1033
1087
  return;
1034
1088
  }
1035
1089
  /*
1036
- * Cover the full bucket: width from adjacent row dates, the
1037
- * same derivation drag-to-select uses.
1090
+ * Reset is on, so hold the click open: both clicks of a
1091
+ * double-click arrive before dblclick does, and neither
1092
+ * may pin a bucket. The second click re-arms the timer,
1093
+ * dblclick clears it.
1038
1094
  */
1039
- const adjacentDate: Date | null =
1040
- rowIndex > 0
1041
- ? getBucketDateAtIndex(rowIndex - 1)
1042
- : getBucketDateAtIndex(rowIndex + 1);
1043
- const bucketWidthInMs: number = adjacentDate
1044
- ? Math.abs(bucketStart.getTime() - adjacentDate.getTime())
1045
- : 0;
1046
- onBucketClick(
1047
- bucketStart,
1048
- new Date(bucketStart.getTime() + bucketWidthInMs),
1049
- (data[rowIndex] || {}) as Record<string, number | string>,
1050
- );
1095
+ if (pendingClickTimeoutRef.current !== null) {
1096
+ clearTimeout(pendingClickTimeoutRef.current);
1097
+ }
1098
+ pendingClickTimeoutRef.current = setTimeout(() => {
1099
+ pendingClickTimeoutRef.current = null;
1100
+ handleChartClick(chartState);
1101
+ }, DOUBLE_CLICK_DISAMBIGUATION_MS);
1051
1102
  }}
1103
+ {...(hasOnTimeRangeReset
1104
+ ? {
1105
+ onDoubleClick: () => {
1106
+ if (pendingClickTimeoutRef.current !== null) {
1107
+ clearTimeout(pendingClickTimeoutRef.current);
1108
+ pendingClickTimeoutRef.current = null;
1109
+ }
1110
+ onTimeRangeReset?.();
1111
+ },
1112
+ }
1113
+ : {})}
1052
1114
  margin={{
1053
1115
  bottom: (xAxisLabel
1054
1116
  ? 40
@@ -692,6 +692,12 @@ interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
692
692
  referenceLines?: Array<ChartReferenceLineProps> | undefined;
693
693
  formattedTimeReferenceLines?: Array<FormattedTimeReferenceLine> | undefined;
694
694
  formattedReferenceRegions?: Array<FormattedReferenceRegion> | undefined;
695
+ /**
696
+ * Double-click on the plot surface. Bar charts have no drag-to-select of
697
+ * their own, but they sit on the same dashboards as line/area panels and
698
+ * must offer the same way out of a board-wide zoom.
699
+ */
700
+ onTimeRangeReset?: (() => void) | undefined;
695
701
  }
696
702
 
697
703
  const BarChart: React.ForwardRefExoticComponent<
@@ -735,6 +741,7 @@ const BarChart: React.ForwardRefExoticComponent<
735
741
  customTooltip,
736
742
  formattedTimeReferenceLines,
737
743
  formattedReferenceRegions,
744
+ onTimeRangeReset,
738
745
  ...other
739
746
  } = props;
740
747
  const CustomTooltip: React.ComponentType<any> | undefined = customTooltip;
@@ -877,6 +884,13 @@ const BarChart: React.ForwardRefExoticComponent<
877
884
  onClick: handleChartClick,
878
885
  }
879
886
  : {})}
887
+ {...(onTimeRangeReset
888
+ ? {
889
+ onDoubleClick: () => {
890
+ onTimeRangeReset();
891
+ },
892
+ }
893
+ : {})}
880
894
  margin={{
881
895
  bottom: xAxisLabel ? 40 : showXAxis ? 24 : 8,
882
896
  left: yAxisLabel ? 20 : 0,
@@ -41,6 +41,7 @@ import useChartAnnotations, {
41
41
  UseChartAnnotationsResult,
42
42
  } from "../Annotations/UseChartAnnotations";
43
43
  import { cx } from "../Utils/Cx";
44
+ import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../Utils/DoubleClick";
44
45
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
45
46
  import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
46
47
  import {
@@ -643,6 +644,15 @@ interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
643
644
  formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
644
645
  onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
645
646
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
647
+ /**
648
+ * Double-click on the plot surface: the counterpart to drag-to-select,
649
+ * for undoing the zoom it produced. Supplying it makes single clicks
650
+ * wait DOUBLE_CLICK_DISAMBIGUATION_MS before they act, so a double-click
651
+ * cannot also fire the bucket-click path twice on its way past — hosts
652
+ * that have nothing to reset should leave it undefined and keep clicks
653
+ * instant.
654
+ */
655
+ onTimeRangeReset?: (() => void) | undefined;
646
656
  /**
647
657
  * Plain click on a time bucket (the plot background — dot/legend clicks
648
658
  * and drag-selections never reach this). [bucketStart, bucketEnd) uses
@@ -702,6 +712,7 @@ const LineChart: React.ForwardRefExoticComponent<
702
712
  formattedTimeReferenceLines,
703
713
  formattedReferenceRegions,
704
714
  onTimeRangeSelect,
715
+ onTimeRangeReset,
705
716
  onBucketClick,
706
717
  ghostCategories,
707
718
  ...other
@@ -732,9 +743,26 @@ const LineChart: React.ForwardRefExoticComponent<
732
743
  React.useRef<number | null>(null);
733
744
  const suppressNextClickRef: React.MutableRefObject<boolean> =
734
745
  React.useRef<boolean>(false);
746
+ const hasOnTimeRangeReset: boolean = Boolean(onTimeRangeReset);
747
+ /*
748
+ * A pending single-click, held open long enough for a second click to
749
+ * cancel it. Only armed when onTimeRangeReset is supplied.
750
+ */
751
+ const pendingClickTimeoutRef: React.MutableRefObject<ReturnType<
752
+ typeof setTimeout
753
+ > | null> = React.useRef<ReturnType<typeof setTimeout> | null>(null);
735
754
  const categoryColors: Map<string, ChartColorValue> =
736
755
  constructCategoryColors(categories, colors);
737
756
 
757
+ React.useEffect(() => {
758
+ return () => {
759
+ if (pendingClickTimeoutRef.current !== null) {
760
+ clearTimeout(pendingClickTimeoutRef.current);
761
+ pendingClickTimeoutRef.current = null;
762
+ }
763
+ };
764
+ }, []);
765
+
738
766
  /*
739
767
  * Annotations are drawn onto the categorical axis, so the layer needs
740
768
  * the same x values recharts is drawing — not the source dates.
@@ -978,6 +1006,51 @@ const LineChart: React.ForwardRefExoticComponent<
978
1006
  onTimeRangeSelect(startDate, endDate);
979
1007
  }
980
1008
 
1009
+ /*
1010
+ * The plain-click path, split out so it can either run inline (no
1011
+ * reset handler) or be deferred behind the double-click window.
1012
+ */
1013
+ function handleChartClick(chartState: RangeSelectionChartState): void {
1014
+ /*
1015
+ * A click while a legend/dot selection is active keeps its
1016
+ * long-standing meaning — clear the selection — and never
1017
+ * also pins a bucket.
1018
+ */
1019
+ if (hasOnValueChange && (activeLegend || activeDot)) {
1020
+ setActiveDot(undefined);
1021
+ setActiveLegend(undefined);
1022
+ onValueChange?.(null);
1023
+ return;
1024
+ }
1025
+ if (!onBucketClick) {
1026
+ return;
1027
+ }
1028
+ const rowIndex: number | null = getRowIndexFromChartState(chartState);
1029
+ if (rowIndex === null) {
1030
+ return;
1031
+ }
1032
+ const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1033
+ if (!bucketStart) {
1034
+ return;
1035
+ }
1036
+ /*
1037
+ * Cover the full bucket: width from adjacent row dates, the
1038
+ * same derivation drag-to-select uses.
1039
+ */
1040
+ const adjacentBucketDate: Date | null =
1041
+ rowIndex > 0
1042
+ ? getBucketDateAtIndex(rowIndex - 1)
1043
+ : getBucketDateAtIndex(rowIndex + 1);
1044
+ const clickedBucketWidthInMs: number = adjacentBucketDate
1045
+ ? Math.abs(bucketStart.getTime() - adjacentBucketDate.getTime())
1046
+ : 0;
1047
+ onBucketClick(
1048
+ bucketStart,
1049
+ new Date(bucketStart.getTime() + clickedBucketWidthInMs),
1050
+ (data[rowIndex] || {}) as Record<string, number | string>,
1051
+ );
1052
+ }
1053
+
981
1054
  return (
982
1055
  <div
983
1056
  ref={ref}
@@ -1016,46 +1089,35 @@ const LineChart: React.ForwardRefExoticComponent<
1016
1089
  if (suppressNextClickRef.current) {
1017
1090
  return;
1018
1091
  }
1019
- /*
1020
- * A click while a legend/dot selection is active keeps its
1021
- * long-standing meaning — clear the selection — and never
1022
- * also pins a bucket.
1023
- */
1024
- if (hasOnValueChange && (activeLegend || activeDot)) {
1025
- setActiveDot(undefined);
1026
- setActiveLegend(undefined);
1027
- onValueChange?.(null);
1028
- return;
1029
- }
1030
- if (!onBucketClick) {
1031
- return;
1032
- }
1033
- const rowIndex: number | null =
1034
- getRowIndexFromChartState(chartState);
1035
- if (rowIndex === null) {
1036
- return;
1037
- }
1038
- const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1039
- if (!bucketStart) {
1092
+ if (!hasOnTimeRangeReset) {
1093
+ handleChartClick(chartState);
1040
1094
  return;
1041
1095
  }
1042
1096
  /*
1043
- * Cover the full bucket: width from adjacent row dates, the
1044
- * same derivation drag-to-select uses.
1097
+ * Reset is on, so hold the click open: both clicks of a
1098
+ * double-click arrive before dblclick does, and neither
1099
+ * may pin a bucket. The second click re-arms the timer,
1100
+ * dblclick clears it.
1045
1101
  */
1046
- const adjacentDate: Date | null =
1047
- rowIndex > 0
1048
- ? getBucketDateAtIndex(rowIndex - 1)
1049
- : getBucketDateAtIndex(rowIndex + 1);
1050
- const bucketWidthInMs: number = adjacentDate
1051
- ? Math.abs(bucketStart.getTime() - adjacentDate.getTime())
1052
- : 0;
1053
- onBucketClick(
1054
- bucketStart,
1055
- new Date(bucketStart.getTime() + bucketWidthInMs),
1056
- (data[rowIndex] || {}) as Record<string, number | string>,
1057
- );
1102
+ if (pendingClickTimeoutRef.current !== null) {
1103
+ clearTimeout(pendingClickTimeoutRef.current);
1104
+ }
1105
+ pendingClickTimeoutRef.current = setTimeout(() => {
1106
+ pendingClickTimeoutRef.current = null;
1107
+ handleChartClick(chartState);
1108
+ }, DOUBLE_CLICK_DISAMBIGUATION_MS);
1058
1109
  }}
1110
+ {...(hasOnTimeRangeReset
1111
+ ? {
1112
+ onDoubleClick: () => {
1113
+ if (pendingClickTimeoutRef.current !== null) {
1114
+ clearTimeout(pendingClickTimeoutRef.current);
1115
+ pendingClickTimeoutRef.current = null;
1116
+ }
1117
+ onTimeRangeReset?.();
1118
+ },
1119
+ }
1120
+ : {})}
1059
1121
  margin={{
1060
1122
  /*
1061
1123
  * Tick labels are 10px font with a translate(0, 6) — they
@@ -0,0 +1,13 @@
1
+ /**
2
+ * How long a chart holds a single click open, waiting to see whether it is
3
+ * really the first half of a double-click.
4
+ *
5
+ * Charts that accept `onTimeRangeReset` have to tell a bucket click apart
6
+ * from a reset gesture, and the browser delivers BOTH clicks of a
7
+ * double-click before it delivers `dblclick` — so the only way a
8
+ * double-click can avoid also pinning a bucket twice is for the click path
9
+ * to wait. Kept just above the ~200ms most platforms use as their
10
+ * double-click threshold, and short enough that a real single click still
11
+ * feels immediate. Charts with no reset handler skip the wait entirely.
12
+ */
13
+ export const DOUBLE_CLICK_DISAMBIGUATION_MS: number = 250;
@@ -62,6 +62,12 @@ export interface ComponentProps {
62
62
  * buckets calls back with the [start, end) of the selected time range.
63
63
  */
64
64
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
65
+ /*
66
+ * Double-click on the plot: undoes whatever drag-to-select produced.
67
+ * Supply it only when a reset is actually possible — see ChartLibrary
68
+ * onTimeRangeReset for why an idle handler costs click latency.
69
+ */
70
+ onTimeRangeReset?: (() => void) | undefined;
65
71
  // Plain click on a bucket — see ChartLibrary onBucketClick.
66
72
  onBucketClick?:
67
73
  | ((
@@ -221,6 +227,7 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
221
227
  }
222
228
  onExemplarClick={props.onExemplarClick}
223
229
  onTimeRangeSelect={props.onTimeRangeSelect}
230
+ onTimeRangeReset={props.onTimeRangeReset}
224
231
  onBucketClick={props.onBucketClick}
225
232
  ghostCategories={props.ghostSeriesNames}
226
233
  />
@@ -201,6 +201,13 @@ export enum ModalTableBulkDefaultActions {
201
201
  export interface BulkActionProps<T extends BaseModel | AnalyticsBaseModel> {
202
202
  buttons: Array<BulkActionButtonSchema<T> | ModalTableBulkDefaultActions>;
203
203
  matchBulkSelectedItemByField?: keyof T | undefined; // which field to use to match selected items. For exmaple this could be '_id'
204
+ /**
205
+ * Appended to the bulk Delete confirmation, for a surface where deleting the
206
+ * selected rows also removes records the user did not select, or where a
207
+ * reversible alternative exists. The default sentence only says the deletion
208
+ * cannot be undone - it cannot know what else goes with it.
209
+ */
210
+ deleteConfirmationWarning?: string | undefined;
204
211
  }
205
212
 
206
213
  export interface BaseTableProps<
@@ -2923,7 +2930,12 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2923
2930
  items.length === 1
2924
2931
  ? props.singularName || model.singularName || "item"
2925
2932
  : props.pluralName || model.pluralName || "items";
2926
- return `Are you sure you want to delete ${items.length} ${itemLabel}? This action cannot be undone.`;
2933
+
2934
+ const warning: string = props.bulkActions?.deleteConfirmationWarning
2935
+ ? ` ${props.bulkActions.deleteConfirmationWarning}`
2936
+ : "";
2937
+
2938
+ return `Are you sure you want to delete ${items.length} ${itemLabel}? This action cannot be undone.${warning}`;
2927
2939
  },
2928
2940
  confirmTitle: (items: Array<TBaseModel>) => {
2929
2941
  const itemLabel: string =
@@ -2946,26 +2958,41 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2946
2958
  const failedItems: Array<BulkActionFailed<TBaseModel>> = [];
2947
2959
 
2948
2960
  for (let i: number = 0; i < items.length; i++) {
2949
- try {
2950
- const item: TBaseModel = items[i]!;
2951
- // remove items from inProgressItems
2952
- inProgressItems.splice(inProgressItems.indexOf(item), 1);
2961
+ const item: TBaseModel = items[i]!;
2962
+
2963
+ // remove items from inProgressItems
2964
+ inProgressItems.splice(inProgressItems.indexOf(item), 1);
2953
2965
 
2966
+ try {
2954
2967
  await props.callbacks.deleteItem(item);
2955
2968
  successItems.push(item);
2956
-
2957
- onProgressInfo({
2958
- inProgressItems: inProgressItems,
2959
- successItems: successItems,
2960
- failed: failedItems,
2961
- totalItems: items,
2962
- });
2963
2969
  } catch (err) {
2964
2970
  failedItems.push({
2965
- item: items[i]!,
2971
+ item: item,
2966
2972
  failedMessage: API.getFriendlyMessage(err),
2967
2973
  });
2968
2974
  }
2975
+
2976
+ /*
2977
+ * Reported after a failure as well as after a success. This used to
2978
+ * sit inside the `try`, so a row that failed moved neither the
2979
+ * progress bar nor the failure list until some later row succeeded
2980
+ * - and a run whose trailing rows all failed (or one where every
2981
+ * row failed, which is what a permission or constraint problem
2982
+ * looks like) ended with the modal still showing the counts from
2983
+ * before them. Deleting hundreds of rows at once is exactly where
2984
+ * partial failure is expected, so it has to be visible.
2985
+ *
2986
+ * Copies, not the live arrays: the progress info goes into React
2987
+ * state, and pushing into the same array reference leaves the
2988
+ * previous render's snapshot mutated underneath it.
2989
+ */
2990
+ onProgressInfo({
2991
+ inProgressItems: [...inProgressItems],
2992
+ successItems: [...successItems],
2993
+ failed: [...failedItems],
2994
+ totalItems: items,
2995
+ });
2969
2996
  }
2970
2997
 
2971
2998
  onBulkActionEnd();
@@ -3116,9 +3143,17 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
3116
3143
  },
3117
3144
  );
3118
3145
 
3146
+ /*
3147
+ * On a table that already offers bulk actions the gate decides on
3148
+ * its own, because there is no selection column left to grow: the
3149
+ * row checkboxes are already there. That covers the locked case, and
3150
+ * it also covers a master admin - allowed everything by the gate, but
3151
+ * holding no project permission for `userCanDelete` to find, so the
3152
+ * Delete they are entitled to used to be missing from a menu whose
3153
+ * every other action worked (issue #3559).
3154
+ */
3119
3155
  const canAutoInjectDelete: boolean =
3120
- userCanDelete ||
3121
- (bulkDeleteGate.disabled && sourceButtons.length > 0);
3156
+ userCanDelete || (bulkDeleteGate.show && sourceButtons.length > 0);
3122
3157
 
3123
3158
  if (canAutoInjectDelete && !alreadyHasDeleteAction) {
3124
3159
  sourceButtons.push(ModalTableBulkDefaultActions.Delete);
@@ -66,11 +66,11 @@ export interface ComponentProps {
66
66
  link: URL;
67
67
  };
68
68
  /*
69
- * When true, the document-level Cmd/Ctrl+K shortcut that toggles the
70
- * products menu is not registered — for apps where another surface (e.g. a
71
- * command palette) owns that shortcut. The menu itself stays fully usable
72
- * via its trigger button. Defaults to false: existing consumers keep the
73
- * shortcut.
69
+ * When true, Cmd/Ctrl+K never opens or toggles the products menu — for apps
70
+ * where another surface (e.g. a command palette) owns that shortcut. The
71
+ * menu itself stays usable via its trigger button; if it is already open,
72
+ * the owner's chord dismisses it without consuming the event. Defaults to
73
+ * false so existing consumers keep the products shortcut.
74
74
  */
75
75
  disableCommandKShortcut?: boolean | undefined;
76
76
  className?: string;
@@ -85,6 +85,22 @@ export interface ComponentProps {
85
85
  const MOBILE_MENU_BOTTOM_GAP_IN_PX: number = 16;
86
86
  const MOBILE_MENU_MIN_HEIGHT_IN_PX: number = 160;
87
87
 
88
+ /*
89
+ * Keep the products-menu shortcut and the dashboard command palette on the
90
+ * same definition of Cmd/Ctrl+K. In particular, modified chords such as
91
+ * Cmd+Shift+K must be left alone for the browser or another feature.
92
+ */
93
+ const isCommandKShortcut: (event: KeyboardEvent) => boolean = (
94
+ event: KeyboardEvent,
95
+ ): boolean => {
96
+ return (
97
+ (event.metaKey || event.ctrlKey) &&
98
+ !event.shiftKey &&
99
+ !event.altKey &&
100
+ event.key.toLowerCase() === "k"
101
+ );
102
+ };
103
+
88
104
  const Navbar: FunctionComponent<ComponentProps> = (
89
105
  props: ComponentProps,
90
106
  ): ReactElement => {
@@ -96,6 +112,9 @@ const Navbar: FunctionComponent<ComponentProps> = (
96
112
  const [mobileMenuMaxHeight, setMobileMenuMaxHeight] = useState<
97
113
  number | undefined
98
114
  >(undefined);
115
+ const hasMoreMenu: boolean = Boolean(
116
+ !props.children && props.moreMenuItems && props.moreMenuItems.length > 0,
117
+ );
99
118
 
100
119
  // Use the existing outside click hook for mobile menu
101
120
  const {
@@ -173,27 +192,56 @@ const Navbar: FunctionComponent<ComponentProps> = (
173
192
 
174
193
  // Open/close the products menu with Cmd/Ctrl + K from anywhere.
175
194
  useEffect(() => {
176
- if (props.disableCommandKShortcut) {
177
- // Another surface (e.g. a command palette) owns Cmd/Ctrl+K.
195
+ if (props.disableCommandKShortcut || !hasMoreMenu) {
196
+ // Another surface owns Cmd/Ctrl+K, or there is no products menu to open.
178
197
  return;
179
198
  }
180
199
 
181
200
  const handleGlobalKeyDown: (event: KeyboardEvent) => void = (
182
201
  event: KeyboardEvent,
183
202
  ): void => {
184
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
185
- event.preventDefault();
186
- setIsMoreMenuVisible((visible: boolean) => {
187
- return !visible;
188
- });
203
+ if (!isCommandKShortcut(event) || event.defaultPrevented) {
204
+ return;
189
205
  }
206
+
207
+ event.preventDefault();
208
+ setIsMoreMenuVisible((visible: boolean) => {
209
+ return !visible;
210
+ });
190
211
  };
191
212
 
192
213
  document.addEventListener("keydown", handleGlobalKeyDown);
193
214
  return () => {
194
215
  document.removeEventListener("keydown", handleGlobalKeyDown);
195
216
  };
196
- }, [props.disableCommandKShortcut]);
217
+ }, [hasMoreMenu, props.disableCommandKShortcut]);
218
+
219
+ /*
220
+ * When the dashboard has surrendered Cmd/Ctrl+K to its command palette, an
221
+ * already-open products modal must get out of the way before the palette
222
+ * opens. This close-only listener deliberately does not prevent or stop the
223
+ * event: the real shortcut owner still needs to receive the same keydown.
224
+ * It also ignores defaultPrevented so the result is deterministic regardless
225
+ * of which document listener React registered first.
226
+ */
227
+ useEffect(() => {
228
+ if (!props.disableCommandKShortcut || !hasMoreMenu || !isMoreMenuVisible) {
229
+ return;
230
+ }
231
+
232
+ const closeProductsOnCommandK: (event: KeyboardEvent) => void = (
233
+ event: KeyboardEvent,
234
+ ): void => {
235
+ if (isCommandKShortcut(event)) {
236
+ setIsMoreMenuVisible(false);
237
+ }
238
+ };
239
+
240
+ document.addEventListener("keydown", closeProductsOnCommandK);
241
+ return () => {
242
+ document.removeEventListener("keydown", closeProductsOnCommandK);
243
+ };
244
+ }, [hasMoreMenu, isMoreMenuVisible, props.disableCommandKShortcut]);
197
245
 
198
246
  // More menu open/close.
199
247
  const openMoreMenu: () => void = (): void => {
@@ -488,6 +536,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
488
536
  footer={props.moreMenuFooter}
489
537
  searchPlaceholder={props.moreMenuSearchPlaceholder}
490
538
  noResultsText={props.moreMenuNoResultsText}
539
+ showCommandKShortcutHint={!props.disableCommandKShortcut}
491
540
  keyboardHint={props.moreMenuKeyboardHint}
492
541
  recentLabel={props.moreMenuRecentLabel}
493
542
  onClose={closeMoreMenu}