@oneuptime/common 14.0.0 → 14.0.1

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 (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
package/Types/API/URL.ts CHANGED
@@ -459,7 +459,26 @@ export default class URL extends DatabaseProperty {
459
459
  const path: string = pathSegments.join("/");
460
460
 
461
461
  if (path || fragment) {
462
- route = new Route(path + fragment);
462
+ /*
463
+ * The route is held without the "/" that separates it from the
464
+ * authority ("api/v1/items"), and toString puts it back. Dropping it is
465
+ * only harmless while the first segment has no ":" — once it does, the
466
+ * bare path reads as a scheme ("bot123:ABC/sendMessage" is scheme
467
+ * "bot123", RFC 3986 4.2) and Route rejects it. Telegram puts the bot
468
+ * token, which always contains a ":", in exactly that segment, so every
469
+ * https://api.telegram.org/bot<token>/... URL failed to parse — which
470
+ * broke the Telegram workflow component, the API components, and the
471
+ * SSRF check on every sandboxed axios call to Telegram.
472
+ *
473
+ * Keep the "/" for those paths only. It is the path as written, it
474
+ * cannot be read as a scheme, and toString produces the same string
475
+ * either way; every path that parsed before parses to the same Route.
476
+ */
477
+ const routeValue: string = path + fragment;
478
+
479
+ route = new Route(
480
+ Route.hasSchemePrefix(routeValue) ? "/" + routeValue : routeValue,
481
+ );
463
482
  }
464
483
 
465
484
  const queryString: string = URL.queryStringOf(url);
@@ -535,8 +554,13 @@ export default class URL extends DatabaseProperty {
535
554
  }
536
555
 
537
556
  public addRoute(route: Route | string): URL {
557
+ /*
558
+ * A string goes to Route.addRoute as-is, which prefixes the "/" before
559
+ * validating. Wrapping it in a Route first validated the bare form, so
560
+ * addRoute("bot123:ABC/sendMessage") threw for the same reason as above.
561
+ */
538
562
  if (typeof route === Typeof.String) {
539
- this.route.addRoute(new Route(route.toString()));
563
+ this.route.addRoute(route.toString());
540
564
  }
541
565
 
542
566
  if (route instanceof Route) {
@@ -25,7 +25,8 @@ export interface MetricBreachingSample {
25
25
 
26
26
  /**
27
27
  * Descriptor for one variable referenced by a formula, used to label
28
- * table columns and render the Metric Details section. Name is the
28
+ * each breaching sample's component values and render the Metric
29
+ * Details section. Name is the
29
30
  * metric display name (usually the metric name, sometimes the formula
30
31
  * expression for nested formulas) and unit is the variable's native
31
32
  * unit as configured.
@@ -103,8 +104,8 @@ export default interface MetricCriteriaContext {
103
104
  breachingSample?: MetricBreachingSample | undefined;
104
105
  /**
105
106
  * All samples in the evaluation window that breached the threshold,
106
- * in chronological order. Used to render a table of timestamps and
107
- * values on the incident root-cause page.
107
+ * in chronological order. Used to render the Breaching Samples list
108
+ * of timestamps and values on the incident root-cause page.
108
109
  */
109
110
  breachingSamples?: Array<MetricBreachingSample> | undefined;
110
111
  /**
@@ -116,8 +117,8 @@ export default interface MetricCriteriaContext {
116
117
  /**
117
118
  * For formulas: metadata for each variable the formula references,
118
119
  * in the order they appear in the expression. Consumers use this to
119
- * label breakdown columns and to surface underlying units/metric
120
- * names in the Metric Details section.
120
+ * label each breaching sample's component values and to surface
121
+ * underlying units/metric names in the Metric Details section.
121
122
  */
122
123
  components?: Array<MetricComponent> | undefined;
123
124
  }
@@ -13,7 +13,16 @@ export interface KubernetesAffectedResource {
13
13
  containerName?: string | undefined;
14
14
  workloadType?: string | undefined;
15
15
  workloadName?: string | undefined;
16
+ /** Highest sample seen for this resource in the monitoring window. */
16
17
  metricValue: number;
18
+ /**
19
+ * Lowest sample seen for this resource in the monitoring window. A
20
+ * criteria that fires when the metric FALLS (node Ready = 0, etcd has
21
+ * leader = 0) breached on this value, not on `metricValue`: a node that
22
+ * was NotReady for one scrape and Ready for the rest has a highest of 1.
23
+ * Absent on breakdowns built before this field existed.
24
+ */
25
+ lowestMetricValue?: number | undefined;
17
26
  }
18
27
 
19
28
  export interface KubernetesResourceBreakdown {
@@ -2018,6 +2018,18 @@ enum Permission {
2018
2018
  EditServiceLevelObjectiveOwnerTeam = "EditServiceLevelObjectiveOwnerTeam",
2019
2019
  ReadServiceLevelObjectiveOwnerTeam = "ReadServiceLevelObjectiveOwnerTeam",
2020
2020
 
2021
+ // Service Level Objective Owner Rule Permissions
2022
+ CreateServiceLevelObjectiveOwnerRule = "CreateServiceLevelObjectiveOwnerRule",
2023
+ DeleteServiceLevelObjectiveOwnerRule = "DeleteServiceLevelObjectiveOwnerRule",
2024
+ EditServiceLevelObjectiveOwnerRule = "EditServiceLevelObjectiveOwnerRule",
2025
+ ReadServiceLevelObjectiveOwnerRule = "ReadServiceLevelObjectiveOwnerRule",
2026
+
2027
+ // Service Level Objective Label Rule Permissions
2028
+ CreateServiceLevelObjectiveLabelRule = "CreateServiceLevelObjectiveLabelRule",
2029
+ DeleteServiceLevelObjectiveLabelRule = "DeleteServiceLevelObjectiveLabelRule",
2030
+ EditServiceLevelObjectiveLabelRule = "EditServiceLevelObjectiveLabelRule",
2031
+ ReadServiceLevelObjectiveLabelRule = "ReadServiceLevelObjectiveLabelRule",
2032
+
2021
2033
  /*
2022
2034
  * Wildcard permissions covering all models marked @OperationalResource().
2023
2035
  * These short-circuit table-level checks for that resource class. Scope on
@@ -16140,6 +16152,84 @@ export class PermissionHelper {
16140
16152
  group: PermissionGroup.SLO,
16141
16153
  },
16142
16154
 
16155
+ // Service Level Objective Owner Rule Permissions
16156
+ {
16157
+ permission: Permission.CreateServiceLevelObjectiveOwnerRule,
16158
+ title: "Create SLO Owner Rule",
16159
+ description:
16160
+ "Permission to create SLO Owner Rules, which add owner users and teams to matching SLOs, in this project.",
16161
+ isAssignableToTenant: true,
16162
+ isAccessControlPermission: false,
16163
+ isRolePermission: false,
16164
+ group: PermissionGroup.SLO,
16165
+ },
16166
+ {
16167
+ permission: Permission.DeleteServiceLevelObjectiveOwnerRule,
16168
+ title: "Delete SLO Owner Rule",
16169
+ description: "Permission to delete SLO Owner Rules of this project.",
16170
+ isAssignableToTenant: true,
16171
+ isAccessControlPermission: false,
16172
+ isRolePermission: false,
16173
+ group: PermissionGroup.SLO,
16174
+ },
16175
+ {
16176
+ permission: Permission.EditServiceLevelObjectiveOwnerRule,
16177
+ title: "Edit SLO Owner Rule",
16178
+ description: "Permission to edit SLO Owner Rules of this project.",
16179
+ isAssignableToTenant: true,
16180
+ isAccessControlPermission: false,
16181
+ isRolePermission: false,
16182
+ group: PermissionGroup.SLO,
16183
+ },
16184
+ {
16185
+ permission: Permission.ReadServiceLevelObjectiveOwnerRule,
16186
+ title: "Read SLO Owner Rule",
16187
+ description: "Permission to read SLO Owner Rules of this project.",
16188
+ isAssignableToTenant: true,
16189
+ isAccessControlPermission: false,
16190
+ isRolePermission: false,
16191
+ group: PermissionGroup.SLO,
16192
+ },
16193
+
16194
+ // Service Level Objective Label Rule Permissions
16195
+ {
16196
+ permission: Permission.CreateServiceLevelObjectiveLabelRule,
16197
+ title: "Create SLO Label Rule",
16198
+ description:
16199
+ "Permission to create SLO Label Rules, which attach labels to matching SLOs, in this project.",
16200
+ isAssignableToTenant: true,
16201
+ isAccessControlPermission: false,
16202
+ isRolePermission: false,
16203
+ group: PermissionGroup.SLO,
16204
+ },
16205
+ {
16206
+ permission: Permission.DeleteServiceLevelObjectiveLabelRule,
16207
+ title: "Delete SLO Label Rule",
16208
+ description: "Permission to delete SLO Label Rules of this project.",
16209
+ isAssignableToTenant: true,
16210
+ isAccessControlPermission: false,
16211
+ isRolePermission: false,
16212
+ group: PermissionGroup.SLO,
16213
+ },
16214
+ {
16215
+ permission: Permission.EditServiceLevelObjectiveLabelRule,
16216
+ title: "Edit SLO Label Rule",
16217
+ description: "Permission to edit SLO Label Rules of this project.",
16218
+ isAssignableToTenant: true,
16219
+ isAccessControlPermission: false,
16220
+ isRolePermission: false,
16221
+ group: PermissionGroup.SLO,
16222
+ },
16223
+ {
16224
+ permission: Permission.ReadServiceLevelObjectiveLabelRule,
16225
+ title: "Read SLO Label Rule",
16226
+ description: "Permission to read SLO Label Rules of this project.",
16227
+ isAssignableToTenant: true,
16228
+ isAccessControlPermission: false,
16229
+ isRolePermission: false,
16230
+ group: PermissionGroup.SLO,
16231
+ },
16232
+
16143
16233
  // Operational Resource Wildcard Permissions
16144
16234
  {
16145
16235
  permission: Permission.ReadAllOperationalResources,
@@ -379,11 +379,22 @@ export const RULE_CRITERIA_FIELDS_BY_MODEL: Readonly<
379
379
  "serviceNamePattern",
380
380
  "serviceDescriptionPattern",
381
381
  ],
382
+ ServiceLevelObjectiveLabelRule: [
383
+ "serviceLevelObjectiveLabels",
384
+ "serviceLevelObjectiveNamePattern",
385
+ "serviceLevelObjectiveDescriptionPattern",
386
+ ],
382
387
  ServiceLevelObjectiveMonitorRule: [
383
388
  "monitorLabels",
389
+ "monitorType",
384
390
  "monitorNamePattern",
385
391
  "monitorDescriptionPattern",
386
392
  ],
393
+ ServiceLevelObjectiveOwnerRule: [
394
+ "serviceLevelObjectiveLabels",
395
+ "serviceLevelObjectiveNamePattern",
396
+ "serviceLevelObjectiveDescriptionPattern",
397
+ ],
387
398
  ServiceOwnerRule: [
388
399
  "serviceLabels",
389
400
  "serviceNamePattern",
@@ -92,7 +92,9 @@ export enum RuleRunType {
92
92
  ServerlessFunctionLabelRule = "ServerlessFunctionLabelRule",
93
93
  ServerlessFunctionOwnerRule = "ServerlessFunctionOwnerRule",
94
94
  ServiceLabelRule = "ServiceLabelRule",
95
+ ServiceLevelObjectiveLabelRule = "ServiceLevelObjectiveLabelRule",
95
96
  ServiceLevelObjectiveMonitorRule = "ServiceLevelObjectiveMonitorRule",
97
+ ServiceLevelObjectiveOwnerRule = "ServiceLevelObjectiveOwnerRule",
96
98
  ServiceOwnerRule = "ServiceOwnerRule",
97
99
  StatusPageLabelRule = "StatusPageLabelRule",
98
100
  StatusPageOwnerRule = "StatusPageOwnerRule",
@@ -316,11 +318,13 @@ export const RULE_RUN_TYPE_METADATA: Readonly<
316
318
  "serverless functions",
317
319
  ),
318
320
  [RuleRunType.ServiceLabelRule]: metadata(Labels, "service", "services"),
321
+ [RuleRunType.ServiceLevelObjectiveLabelRule]: metadata(Labels, "SLO", "SLOs"),
319
322
  [RuleRunType.ServiceLevelObjectiveMonitorRule]: metadata(
320
323
  RuleRunAction.SyncSloMonitors,
321
324
  "monitor",
322
325
  "monitors",
323
326
  ),
327
+ [RuleRunType.ServiceLevelObjectiveOwnerRule]: metadata(Owners, "SLO", "SLOs"),
324
328
  [RuleRunType.ServiceOwnerRule]: metadata(Owners, "service", "services"),
325
329
  [RuleRunType.StatusPageLabelRule]: metadata(
326
330
  Labels,
@@ -0,0 +1,427 @@
1
+ import GenericObject from "../../../Types/GenericObject";
2
+ import Includes from "../../../Types/BaseDatabase/Includes";
3
+ import Query from "../../../Types/BaseDatabase/Query";
4
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
5
+ import IconProp from "../../../Types/Icon/IconProp";
6
+
7
+ /*
8
+ * What a reader can change about a dashboard activity feed from its one
9
+ * "Filter & Sort" button: which way the timeline runs, and which kinds of
10
+ * event it shows.
11
+ *
12
+ * Both are applied by the API, never to the rows already in the browser. A
13
+ * feed is read a window at a time (see useFeedItems), so filtering the loaded
14
+ * window would show "no items" while matching events sat one page further
15
+ * back, and reversing it would show the newest ten upside down instead of the
16
+ * oldest ten.
17
+ *
18
+ * This module is React-free so the rules can be tested on their own.
19
+ */
20
+ export interface FeedOptions {
21
+ sortOrder: SortOrder;
22
+ /*
23
+ * The event types to show. Empty means no filter, so every event type is
24
+ * shown - including one a feed model gains after the reader last looked.
25
+ */
26
+ eventTypes: Array<string>;
27
+ }
28
+
29
+ export const DEFAULT_FEED_SORT_ORDER: SortOrder = SortOrder.Descending;
30
+
31
+ export const DEFAULT_FEED_OPTIONS: FeedOptions = {
32
+ sortOrder: DEFAULT_FEED_SORT_ORDER,
33
+ eventTypes: [],
34
+ };
35
+
36
+ export interface FeedSortOrderOption {
37
+ value: SortOrder;
38
+ label: string;
39
+ icon: IconProp;
40
+ }
41
+
42
+ export const FEED_SORT_ORDER_OPTIONS: Array<FeedSortOrderOption> = [
43
+ {
44
+ value: SortOrder.Descending,
45
+ label: "Newest first",
46
+ icon: IconProp.BarsArrowDown,
47
+ },
48
+ {
49
+ value: SortOrder.Ascending,
50
+ label: "Oldest first",
51
+ icon: IconProp.BarsArrowUp,
52
+ },
53
+ ];
54
+
55
+ export interface FeedEventTypeOption {
56
+ value: string;
57
+ label: string;
58
+ icon?: IconProp | undefined;
59
+ }
60
+
61
+ export type GetFeedEventTypeLabelFunction = (
62
+ eventType: string,
63
+ ) => string | undefined;
64
+
65
+ export type GetFeedEventTypeIconFunction = (
66
+ eventType: string,
67
+ ) => IconProp | undefined;
68
+
69
+ /*
70
+ * Event types shared by many feeds whose split name reads badly
71
+ * ("Owner User Added"). A Map rather than an object literal, so an event type
72
+ * that happens to share a name with an Object.prototype member ("toString")
73
+ * cannot come back as a function.
74
+ */
75
+ const EVENT_TYPE_LABELS: Map<string, string> = new Map<string, string>([
76
+ ["OwnerUserAdded", "User Added as Owner"],
77
+ ["OwnerUserRemoved", "User Removed as Owner"],
78
+ ["OwnerTeamAdded", "Team Added as Owner"],
79
+ ["OwnerTeamRemoved", "Team Removed as Owner"],
80
+ ["AutoRemediation", "Auto-Remediation"],
81
+ ]);
82
+
83
+ /*
84
+ * Runs that camel-case splitting would get wrong, matched before any other
85
+ * word: "VMware" would split into "V Mware", "OnCall" should read
86
+ * "On-Call", and the SLO feed's "ServiceLevelObjective" prefix is written
87
+ * "SLO" everywhere else in the dashboard.
88
+ */
89
+ const EVENT_TYPE_WORDS: Array<{ word: string; label: string }> = [
90
+ { word: "ServiceLevelObjective", label: "SLO" },
91
+ { word: "VMware", label: "VMware" },
92
+ { word: "VCenter", label: "vCenter" },
93
+ { word: "OnCall", label: "On-Call" },
94
+ ];
95
+
96
+ // Joining words stay lower case inside a title ("Added to Episode").
97
+ const LOWER_CASE_WORDS: Set<string> = new Set<string>([
98
+ "a",
99
+ "an",
100
+ "and",
101
+ "as",
102
+ "by",
103
+ "for",
104
+ "from",
105
+ "in",
106
+ "of",
107
+ "on",
108
+ "or",
109
+ "the",
110
+ "to",
111
+ ]);
112
+
113
+ const EVENT_TYPE_WORD_PATTERN: RegExp = new RegExp(
114
+ `${EVENT_TYPE_WORDS.map((entry: { word: string }) => {
115
+ return entry.word;
116
+ }).join("|")}|[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+`,
117
+ "g",
118
+ );
119
+
120
+ type GetFeedEventTypeLabel = (eventType: string) => string;
121
+
122
+ /*
123
+ * Feed event types are enum values named like "IncidentStateChanged", so a
124
+ * readable label is the value split into words. A feed with a better name for
125
+ * one of its own events passes getEventTypeLabel to getFeedEventTypeOptions.
126
+ */
127
+ export const getFeedEventTypeLabel: GetFeedEventTypeLabel = (
128
+ eventType: string,
129
+ ): string => {
130
+ const knownLabel: string | undefined = EVENT_TYPE_LABELS.get(eventType);
131
+
132
+ if (knownLabel) {
133
+ return knownLabel;
134
+ }
135
+
136
+ const words: Array<string> = eventType.match(EVENT_TYPE_WORD_PATTERN) || [];
137
+
138
+ if (words.length === 0) {
139
+ return eventType;
140
+ }
141
+
142
+ return words
143
+ .map((word: string, index: number): string => {
144
+ const knownWord: { word: string; label: string } | undefined =
145
+ EVENT_TYPE_WORDS.find((entry: { word: string }) => {
146
+ return entry.word === word;
147
+ });
148
+
149
+ if (knownWord) {
150
+ return knownWord.label;
151
+ }
152
+
153
+ if (index > 0 && LOWER_CASE_WORDS.has(word.toLowerCase())) {
154
+ return word.toLowerCase();
155
+ }
156
+
157
+ return word.charAt(0).toUpperCase() + word.slice(1);
158
+ })
159
+ .join(" ");
160
+ };
161
+
162
+ type GetFeedEventTypeOptions = (data: {
163
+ eventTypes: Array<string>;
164
+ getEventTypeLabel?: GetFeedEventTypeLabelFunction | undefined;
165
+ getEventTypeIcon?: GetFeedEventTypeIconFunction | undefined;
166
+ }) => Array<FeedEventTypeOption>;
167
+
168
+ /*
169
+ * The checklist behind the button: one entry per event type, alphabetical by
170
+ * label so the reader can scan for the one they want.
171
+ */
172
+ export const getFeedEventTypeOptions: GetFeedEventTypeOptions = (data: {
173
+ eventTypes: Array<string>;
174
+ getEventTypeLabel?: GetFeedEventTypeLabelFunction | undefined;
175
+ getEventTypeIcon?: GetFeedEventTypeIconFunction | undefined;
176
+ }): Array<FeedEventTypeOption> => {
177
+ const uniqueEventTypes: Array<string> = Array.from(
178
+ new Set<string>(
179
+ data.eventTypes.filter((eventType: string) => {
180
+ return Boolean(eventType);
181
+ }),
182
+ ),
183
+ );
184
+
185
+ return uniqueEventTypes
186
+ .map((eventType: string): FeedEventTypeOption => {
187
+ return {
188
+ value: eventType,
189
+ label:
190
+ data.getEventTypeLabel?.(eventType) ||
191
+ getFeedEventTypeLabel(eventType),
192
+ icon: data.getEventTypeIcon?.(eventType),
193
+ };
194
+ })
195
+ .sort((a: FeedEventTypeOption, b: FeedEventTypeOption): number => {
196
+ return a.label.localeCompare(b.label);
197
+ });
198
+ };
199
+
200
+ type SanitizeFeedEventTypes = (data: {
201
+ selectedEventTypes: Array<string>;
202
+ knownEventTypes: Array<string>;
203
+ }) => Array<string>;
204
+
205
+ /*
206
+ * Keep only event types the feed actually has, once each, in the feed's own
207
+ * order. The order matters: it makes the request - and so the reload key -
208
+ * the same whichever order the reader ticked the boxes in.
209
+ */
210
+ export const sanitizeFeedEventTypes: SanitizeFeedEventTypes = (data: {
211
+ selectedEventTypes: Array<string>;
212
+ knownEventTypes: Array<string>;
213
+ }): Array<string> => {
214
+ const selected: Set<string> = new Set<string>(data.selectedEventTypes);
215
+
216
+ return Array.from(new Set<string>(data.knownEventTypes)).filter(
217
+ (eventType: string) => {
218
+ return selected.has(eventType);
219
+ },
220
+ );
221
+ };
222
+
223
+ type IsFeedSortOrder = (value: unknown) => value is SortOrder;
224
+
225
+ export const isFeedSortOrder: IsFeedSortOrder = (
226
+ value: unknown,
227
+ ): value is SortOrder => {
228
+ return value === SortOrder.Ascending || value === SortOrder.Descending;
229
+ };
230
+
231
+ type GetFeedOptionsKey = (options: FeedOptions) => string;
232
+
233
+ /*
234
+ * A stable string for "what this feed is showing". useFeedItems re-reads the
235
+ * feed from its first window whenever it changes.
236
+ */
237
+ export const getFeedOptionsKey: GetFeedOptionsKey = (
238
+ options: FeedOptions,
239
+ ): string => {
240
+ return `${options.sortOrder}|${options.eventTypes.join(",")}`;
241
+ };
242
+
243
+ type IsFeedFiltered = (options: FeedOptions) => boolean;
244
+
245
+ export const isFeedFiltered: IsFeedFiltered = (
246
+ options: FeedOptions,
247
+ ): boolean => {
248
+ return options.eventTypes.length > 0;
249
+ };
250
+
251
+ type IsDefaultFeedOptions = (options: FeedOptions) => boolean;
252
+
253
+ export const isDefaultFeedOptions: IsDefaultFeedOptions = (
254
+ options: FeedOptions,
255
+ ): boolean => {
256
+ return (
257
+ options.sortOrder === DEFAULT_FEED_SORT_ORDER && !isFeedFiltered(options)
258
+ );
259
+ };
260
+
261
+ /*
262
+ * The query fragment for the event type filter, keyed by the feed model's
263
+ * event type column. It is empty when nothing is filtered, so an unfiltered
264
+ * feed sends exactly the query it always sent.
265
+ */
266
+ export const getFeedEventTypeQuery: <TFeedModel extends GenericObject>(
267
+ eventTypeColumn: Extract<keyof TFeedModel, string>,
268
+ options: FeedOptions,
269
+ ) => Query<TFeedModel> = <TFeedModel extends GenericObject>(
270
+ eventTypeColumn: Extract<keyof TFeedModel, string>,
271
+ options: FeedOptions,
272
+ ): Query<TFeedModel> => {
273
+ if (!isFeedFiltered(options)) {
274
+ return {};
275
+ }
276
+
277
+ return {
278
+ [eventTypeColumn]: new Includes([...options.eventTypes]),
279
+ } as unknown as Query<TFeedModel>;
280
+ };
281
+
282
+ export const FILTERED_FEED_NO_ITEMS_MESSAGE: string =
283
+ "No events in this feed match the selected event types.";
284
+
285
+ type GetFeedNoItemsMessage = (data: {
286
+ options: FeedOptions;
287
+ noItemsMessage: string;
288
+ }) => string;
289
+
290
+ /*
291
+ * An empty filtered feed says so, rather than claiming the resource has no
292
+ * activity at all.
293
+ */
294
+ export const getFeedNoItemsMessage: GetFeedNoItemsMessage = (data: {
295
+ options: FeedOptions;
296
+ noItemsMessage: string;
297
+ }): string => {
298
+ return isFeedFiltered(data.options)
299
+ ? FILTERED_FEED_NO_ITEMS_MESSAGE
300
+ : data.noItemsMessage;
301
+ };
302
+
303
+ /*
304
+ * The control's own sentences, as the English keys the dashboard's locale
305
+ * files translate (see Utils/Translation). The ones with placeholders are
306
+ * looked up whole and filled afterwards, so a translation can reorder the
307
+ * words around the numbers.
308
+ */
309
+ export const FEED_OPTIONS_TEXT: {
310
+ triggerLabel: string;
311
+ searchResults: string;
312
+ panelLabel: string;
313
+ sortHeading: string;
314
+ eventTypesHeading: string;
315
+ showAll: string;
316
+ searchPlaceholder: string;
317
+ showingEveryEventType: string;
318
+ showingSomeEventTypes: string;
319
+ noSearchMatches: string;
320
+ noEventTypes: string;
321
+ reset: string;
322
+ summaryAllEventTypes: string;
323
+ summarySomeEventTypes: string;
324
+ summarySomeOfOneEventType: string;
325
+ } = {
326
+ triggerLabel: "Filter & Sort",
327
+ searchResults: "Matching event types: {{count}}",
328
+ panelLabel: "Filter and sort feed",
329
+ sortHeading: "Sort by time",
330
+ eventTypesHeading: "Event types",
331
+ showAll: "Show all",
332
+ searchPlaceholder: "Search event types",
333
+ showingEveryEventType:
334
+ "Showing every event type. Tick one or more to narrow the feed.",
335
+ showingSomeEventTypes: "Showing {{selected}} of {{total}} event types.",
336
+ noSearchMatches: 'No event types match "{{search}}".',
337
+ noEventTypes: "This feed has no event types to filter by.",
338
+ reset: "Reset to default",
339
+ summaryAllEventTypes: "{{sortOrder}}, all event types",
340
+ summarySomeEventTypes: "{{sortOrder}}, {{selected}} of {{total}} event types",
341
+ summarySomeOfOneEventType:
342
+ "{{sortOrder}}, {{selected}} of {{total}} event type",
343
+ };
344
+
345
+ // Looks a sentence up in the reader's language; undefined means "as written".
346
+ export type FeedOptionsTranslateFunction = (
347
+ text: string | undefined,
348
+ ) => string | undefined;
349
+
350
+ const PLACEHOLDER_PATTERN: RegExp = /\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g;
351
+
352
+ type TranslateFeedOptionsText = (data: {
353
+ text: string;
354
+ values?: Record<string, string | number> | undefined;
355
+ translate?: FeedOptionsTranslateFunction | undefined;
356
+ }) => string;
357
+
358
+ /*
359
+ * Translate, then fill the placeholders. A placeholder with no value is left
360
+ * showing, so a broken translation is visible rather than a sentence with a
361
+ * hole in it.
362
+ */
363
+ export const translateFeedOptionsText: TranslateFeedOptionsText = (data: {
364
+ text: string;
365
+ values?: Record<string, string | number> | undefined;
366
+ translate?: FeedOptionsTranslateFunction | undefined;
367
+ }): string => {
368
+ const translated: string =
369
+ (data.translate ? data.translate(data.text) : undefined) || data.text;
370
+ const values: Record<string, string | number> = data.values || {};
371
+
372
+ return translated.replace(
373
+ PLACEHOLDER_PATTERN,
374
+ (placeholder: string, name: string): string => {
375
+ const value: string | number | undefined = values[name];
376
+
377
+ return value === undefined ? placeholder : String(value);
378
+ },
379
+ );
380
+ };
381
+
382
+ type GetFeedOptionsSummary = (data: {
383
+ options: FeedOptions;
384
+ eventTypeCount: number;
385
+ translate?: FeedOptionsTranslateFunction | undefined;
386
+ }) => string;
387
+
388
+ /*
389
+ * One sentence for the trigger's accessible name and tooltip, so the state
390
+ * hidden behind the button can be read without opening it.
391
+ */
392
+ export const getFeedOptionsSummary: GetFeedOptionsSummary = (data: {
393
+ options: FeedOptions;
394
+ eventTypeCount: number;
395
+ translate?: FeedOptionsTranslateFunction | undefined;
396
+ }): string => {
397
+ const sortOrder: string = translateFeedOptionsText({
398
+ text:
399
+ FEED_SORT_ORDER_OPTIONS.find((option: FeedSortOrderOption) => {
400
+ return option.value === data.options.sortOrder;
401
+ })?.label || "Newest first",
402
+ translate: data.translate,
403
+ });
404
+
405
+ const selectedCount: number = data.options.eventTypes.length;
406
+
407
+ if (selectedCount === 0) {
408
+ return translateFeedOptionsText({
409
+ text: FEED_OPTIONS_TEXT.summaryAllEventTypes,
410
+ values: { sortOrder },
411
+ translate: data.translate,
412
+ });
413
+ }
414
+
415
+ return translateFeedOptionsText({
416
+ text:
417
+ data.eventTypeCount === 1
418
+ ? FEED_OPTIONS_TEXT.summarySomeOfOneEventType
419
+ : FEED_OPTIONS_TEXT.summarySomeEventTypes,
420
+ values: {
421
+ sortOrder,
422
+ selected: selectedCount,
423
+ total: data.eventTypeCount,
424
+ },
425
+ translate: data.translate,
426
+ });
427
+ };