@oneuptime/common 11.3.28 → 11.4.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 (244) hide show
  1. package/Models/DatabaseModels/AIRun.ts +50 -0
  2. package/Models/DatabaseModels/Project.ts +60 -0
  3. package/Server/API/AIAgentTaskAPI.ts +10 -24
  4. package/Server/API/AIInvestigationAPI.ts +148 -0
  5. package/Server/API/TelemetryAPI.ts +4 -3
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  10. package/Server/Services/AIAgentTaskService.ts +62 -0
  11. package/Server/Services/AlertService.ts +25 -0
  12. package/Server/Services/AnalyticsDatabaseService.ts +27 -0
  13. package/Server/Services/IncidentService.ts +25 -0
  14. package/Server/Services/IncidentStateTimelineService.ts +19 -0
  15. package/Server/Services/LogAggregationService.ts +29 -11
  16. package/Server/Services/MetricService.ts +603 -13
  17. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
  18. package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
  19. package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
  20. package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
  21. package/Server/Utils/AI/Sentinel/README.md +10 -0
  22. package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
  23. package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
  24. package/Server/Utils/AI/Toolbox/Index.ts +11 -0
  25. package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
  26. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +44 -0
  27. package/Server/Utils/Monitor/MonitorMetricUtil.ts +108 -2
  28. package/Tests/Server/API/BaseAPI.test.ts +1 -0
  29. package/Tests/Server/Services/AIAgentTaskServiceClaim.test.ts +146 -0
  30. package/Tests/Server/Services/LogAggregationService.test.ts +22 -0
  31. package/Tests/Server/Services/MetricServiceAggregate.test.ts +194 -0
  32. package/Tests/Types/AI/AIAgentTaskStatus.test.ts +86 -0
  33. package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +127 -0
  34. package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.test.ts +152 -0
  35. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
  36. package/Tests/Types/Trace/TraceRecordingRuleDefinition.test.ts +308 -0
  37. package/Tests/UI/Components/Dropdown.test.tsx +17 -0
  38. package/Tests/UI/Components/EntityDropdown.test.tsx +130 -0
  39. package/Tests/UI/Components/Modal.test.tsx +144 -2
  40. package/Tests/UI/Components/TelemetrySearchBar.test.tsx +90 -0
  41. package/Tests/UI/Utils/Theme.test.ts +161 -0
  42. package/Types/BaseDatabase/AggregateBy.ts +12 -0
  43. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  44. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  45. package/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.ts +19 -0
  46. package/Types/Dashboard/DashboardTemplates.ts +9 -1
  47. package/Types/Date.ts +46 -0
  48. package/Types/Monitor/CriteriaFilter.ts +4 -0
  49. package/Types/Monitor/HttpPhaseTimings.ts +16 -0
  50. package/Types/Monitor/MonitorMetricType.ts +17 -0
  51. package/Types/Monitor/PingMonitor/PingMonitorResponse.ts +20 -0
  52. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
  53. package/Types/Probe/ProbeMonitorResponse.ts +14 -0
  54. package/UI/Components/Calendar/Calendar.tsx +8 -2
  55. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
  56. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -5
  57. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +4 -1
  58. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +6 -3
  59. package/UI/Components/CodeBlock/CodeBlock.tsx +2 -1
  60. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  61. package/UI/Components/Divider/Divider.tsx +1 -1
  62. package/UI/Components/Dropdown/Dropdown.tsx +13 -4
  63. package/UI/Components/EntityDropdown/EntityDropdown.tsx +110 -2
  64. package/UI/Components/ErrorBoundary.tsx +2 -1
  65. package/UI/Components/EventItem/EventItem.tsx +2 -2
  66. package/UI/Components/GanttChart/Bar/Index.tsx +1 -1
  67. package/UI/Components/Graphs/UptimeBarTooltip.tsx +30 -23
  68. package/UI/Components/Header/Header.tsx +7 -0
  69. package/UI/Components/Header/HeaderIconDropdownButton.tsx +2 -0
  70. package/UI/Components/Label/Label.tsx +2 -2
  71. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +22 -10
  72. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +9 -3
  73. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +8 -8
  74. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +44 -20
  75. package/UI/Components/Modal/ConfirmModal.tsx +1 -1
  76. package/UI/Components/Modal/Modal.tsx +213 -121
  77. package/UI/Components/Modal/ModalFooter.tsx +33 -22
  78. package/UI/Components/ModelTable/TableView.tsx +1 -1
  79. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +20 -12
  80. package/UI/Components/Pill/Pill.tsx +21 -13
  81. package/UI/Components/StatusBubble/StatusBubble.tsx +3 -3
  82. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +7 -1
  83. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +9 -3
  84. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +62 -4
  85. package/UI/Components/Workflow/Component.tsx +53 -33
  86. package/UI/Components/Workflow/ComponentPortViewer.tsx +6 -6
  87. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +8 -8
  88. package/UI/Components/Workflow/ComponentsModal.tsx +16 -9
  89. package/UI/Components/Workflow/Workflow.tsx +13 -11
  90. package/UI/Utils/Theme.ts +140 -0
  91. package/Utils/Dashboard/Components/DashboardLogChartComponent.ts +104 -0
  92. package/Utils/Dashboard/Components/Index.ts +7 -0
  93. package/Utils/Monitor/MonitorMetricType.ts +70 -2
  94. package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
  95. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  97. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  98. package/build/dist/Server/API/AIAgentTaskAPI.js +8 -23
  99. package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
  100. package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
  101. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
  102. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  111. package/build/dist/Server/Services/AIAgentTaskService.js +59 -0
  112. package/build/dist/Server/Services/AIAgentTaskService.js.map +1 -1
  113. package/build/dist/Server/Services/AlertService.js +24 -0
  114. package/build/dist/Server/Services/AlertService.js.map +1 -1
  115. package/build/dist/Server/Services/AnalyticsDatabaseService.js +19 -0
  116. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  117. package/build/dist/Server/Services/IncidentService.js +24 -0
  118. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  119. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  120. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  121. package/build/dist/Server/Services/LogAggregationService.js +21 -7
  122. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  123. package/build/dist/Server/Services/MetricService.js +449 -7
  124. package/build/dist/Server/Services/MetricService.js.map +1 -1
  125. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
  126. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  127. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
  128. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
  129. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
  130. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
  131. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
  132. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
  133. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
  134. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
  135. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
  136. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
  137. package/build/dist/Server/Utils/AI/Toolbox/Index.js +6 -0
  138. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
  140. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
  141. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +29 -0
  142. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +86 -2
  144. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  145. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  146. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  147. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  148. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  149. package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js +2 -0
  150. package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js.map +1 -0
  151. package/build/dist/Types/Dashboard/DashboardTemplates.js +9 -1
  152. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  153. package/build/dist/Types/Date.js +32 -0
  154. package/build/dist/Types/Date.js.map +1 -1
  155. package/build/dist/Types/Monitor/CriteriaFilter.js +4 -0
  156. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  157. package/build/dist/Types/Monitor/HttpPhaseTimings.js +2 -0
  158. package/build/dist/Types/Monitor/HttpPhaseTimings.js.map +1 -0
  159. package/build/dist/Types/Monitor/MonitorMetricType.js +15 -0
  160. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  161. package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js +2 -0
  162. package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js.map +1 -0
  163. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
  164. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
  165. package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
  166. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  167. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  168. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  169. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +7 -4
  170. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  171. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +4 -1
  172. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  173. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +6 -3
  174. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  175. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +1 -1
  176. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  177. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +3 -1
  178. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  179. package/build/dist/UI/Components/Divider/Divider.js +1 -1
  180. package/build/dist/UI/Components/Divider/Divider.js.map +1 -1
  181. package/build/dist/UI/Components/Dropdown/Dropdown.js +10 -4
  182. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  183. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +76 -3
  184. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  185. package/build/dist/UI/Components/ErrorBoundary.js +2 -1
  186. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  187. package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
  188. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  189. package/build/dist/UI/Components/GanttChart/Bar/Index.js +1 -1
  190. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +1 -1
  191. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +29 -23
  192. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  193. package/build/dist/UI/Components/Header/Header.js +1 -0
  194. package/build/dist/UI/Components/Header/Header.js.map +1 -1
  195. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  196. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  197. package/build/dist/UI/Components/Label/Label.js +2 -2
  198. package/build/dist/UI/Components/Label/Label.js.map +1 -1
  199. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +20 -8
  200. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  201. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +8 -2
  202. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  203. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +8 -8
  204. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +40 -20
  205. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  206. package/build/dist/UI/Components/Modal/ConfirmModal.js +1 -1
  207. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  208. package/build/dist/UI/Components/Modal/Modal.js +109 -51
  209. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  210. package/build/dist/UI/Components/Modal/ModalFooter.js +13 -10
  211. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  212. package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
  213. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  214. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +17 -12
  215. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  216. package/build/dist/UI/Components/Pill/Pill.js +12 -13
  217. package/build/dist/UI/Components/Pill/Pill.js.map +1 -1
  218. package/build/dist/UI/Components/StatusBubble/StatusBubble.js +3 -3
  219. package/build/dist/UI/Components/StatusBubble/StatusBubble.js.map +1 -1
  220. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  221. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  222. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +36 -6
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  226. package/build/dist/UI/Components/Workflow/Component.js +43 -33
  227. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ComponentPortViewer.js +6 -6
  229. package/build/dist/UI/Components/Workflow/ComponentPortViewer.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +8 -8
  231. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  232. package/build/dist/UI/Components/Workflow/ComponentsModal.js +15 -9
  233. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  234. package/build/dist/UI/Components/Workflow/Workflow.js +13 -11
  235. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  236. package/build/dist/UI/Utils/Theme.js +106 -0
  237. package/build/dist/UI/Utils/Theme.js.map +1 -0
  238. package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js +92 -0
  239. package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js.map +1 -0
  240. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  241. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  242. package/build/dist/Utils/Monitor/MonitorMetricType.js +70 -3
  243. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  244. package/package.json +1 -1
@@ -0,0 +1,308 @@
1
+ import TraceAggregationType from "../../../Types/Trace/TraceAggregationType";
2
+ import TraceRecordingRuleDefinition, {
3
+ TraceRecordingRuleDefinitionUtil,
4
+ TraceRecordingRuleSource,
5
+ TraceRecordingRuleAttributeFilter,
6
+ TRACE_RECORDING_RULE_MAX_SOURCES,
7
+ TRACE_RECORDING_RULE_MAX_EXPRESSION_LENGTH,
8
+ } from "../../../Types/Trace/TraceRecordingRuleDefinition";
9
+
10
+ describe("TraceRecordingRuleDefinitionUtil", () => {
11
+ const makeSource: (
12
+ overrides?: Partial<TraceRecordingRuleSource>,
13
+ ) => TraceRecordingRuleSource = (
14
+ overrides: Partial<TraceRecordingRuleSource> = {},
15
+ ): TraceRecordingRuleSource => {
16
+ return {
17
+ alias: "A",
18
+ aggregationType: TraceAggregationType.Count,
19
+ ...overrides,
20
+ };
21
+ };
22
+
23
+ describe("getAggregationOptions", () => {
24
+ test("every option has a valid value, label and description", () => {
25
+ const options: Array<{
26
+ value: TraceAggregationType;
27
+ label: string;
28
+ description: string;
29
+ }> = TraceRecordingRuleDefinitionUtil.getAggregationOptions();
30
+
31
+ expect(options.length).toBeGreaterThan(0);
32
+ for (const option of options) {
33
+ expect(option.label.length).toBeGreaterThan(0);
34
+ expect(option.description.length).toBeGreaterThan(0);
35
+ expect(Object.values(TraceAggregationType)).toContain(option.value);
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("getSpanKindOptions", () => {
41
+ test("includes an 'Any' option with an empty value", () => {
42
+ const options: Array<{ value: string; label: string }> =
43
+ TraceRecordingRuleDefinitionUtil.getSpanKindOptions();
44
+
45
+ expect(
46
+ options.some((o: { value: string }): boolean => {
47
+ return o.value === "";
48
+ }),
49
+ ).toBe(true);
50
+ expect(
51
+ options.some((o: { value: string }): boolean => {
52
+ return o.value === "SPAN_KIND_SERVER";
53
+ }),
54
+ ).toBe(true);
55
+ });
56
+ });
57
+
58
+ describe("getSourceAttributeFilters", () => {
59
+ test("returns the multi-filter entries, trimmed", () => {
60
+ const filters: Array<TraceRecordingRuleAttributeFilter> =
61
+ TraceRecordingRuleDefinitionUtil.getSourceAttributeFilters(
62
+ makeSource({
63
+ filterAttributes: [{ key: " http.route ", value: " /api " }],
64
+ }),
65
+ );
66
+
67
+ expect(filters).toEqual([{ key: "http.route", value: "/api" }]);
68
+ });
69
+
70
+ test("drops entries with a missing key or value, and deduplicates by key", () => {
71
+ const filters: Array<TraceRecordingRuleAttributeFilter> =
72
+ TraceRecordingRuleDefinitionUtil.getSourceAttributeFilters(
73
+ makeSource({
74
+ filterAttributes: [
75
+ { key: "host", value: "a" },
76
+ { key: "host", value: "b" }, // duplicate key — dropped
77
+ { key: "empty", value: "" }, // no value — dropped
78
+ ],
79
+ }),
80
+ );
81
+
82
+ expect(filters).toEqual([{ key: "host", value: "a" }]);
83
+ });
84
+
85
+ test("merges the legacy single filter when its key is not already present", () => {
86
+ const filters: Array<TraceRecordingRuleAttributeFilter> =
87
+ TraceRecordingRuleDefinitionUtil.getSourceAttributeFilters(
88
+ makeSource({
89
+ filterAttributes: [{ key: "host", value: "a" }],
90
+ filterAttributeKey: "env",
91
+ filterAttributeValue: "prod",
92
+ }),
93
+ );
94
+
95
+ expect(filters).toEqual([
96
+ { key: "host", value: "a" },
97
+ { key: "env", value: "prod" },
98
+ ]);
99
+ });
100
+
101
+ test("array entry wins over the legacy filter for the same key", () => {
102
+ const filters: Array<TraceRecordingRuleAttributeFilter> =
103
+ TraceRecordingRuleDefinitionUtil.getSourceAttributeFilters(
104
+ makeSource({
105
+ filterAttributes: [{ key: "env", value: "staging" }],
106
+ filterAttributeKey: "env",
107
+ filterAttributeValue: "prod",
108
+ }),
109
+ );
110
+
111
+ expect(filters).toEqual([{ key: "env", value: "staging" }]);
112
+ });
113
+ });
114
+
115
+ describe("getNextAlias", () => {
116
+ test("returns A when empty and the first free letter otherwise", () => {
117
+ expect(TraceRecordingRuleDefinitionUtil.getNextAlias(undefined)).toBe(
118
+ "A",
119
+ );
120
+ expect(
121
+ TraceRecordingRuleDefinitionUtil.getNextAlias([
122
+ makeSource({ alias: "A" }),
123
+ makeSource({ alias: "C" }),
124
+ ]),
125
+ ).toBe("B");
126
+ });
127
+ });
128
+
129
+ describe("getEmptyDefinition", () => {
130
+ test("returns a single Count source aliased A that validates", () => {
131
+ const def: TraceRecordingRuleDefinition =
132
+ TraceRecordingRuleDefinitionUtil.getEmptyDefinition();
133
+
134
+ expect(def.sources.length).toBe(1);
135
+ expect(def.sources[0]!.alias).toBe("A");
136
+ expect(def.sources[0]!.aggregationType).toBe(TraceAggregationType.Count);
137
+ expect(def.expression).toBe("A");
138
+ expect(
139
+ TraceRecordingRuleDefinitionUtil.getValidationError(def),
140
+ ).toBeNull();
141
+ });
142
+ });
143
+
144
+ describe("getValidationError", () => {
145
+ const validDefinition: () => TraceRecordingRuleDefinition =
146
+ (): TraceRecordingRuleDefinition => {
147
+ return {
148
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "B" })],
149
+ expression: "A / B * 100",
150
+ };
151
+ };
152
+
153
+ test("accepts a valid definition", () => {
154
+ expect(
155
+ TraceRecordingRuleDefinitionUtil.getValidationError(validDefinition()),
156
+ ).toBeNull();
157
+ });
158
+
159
+ test("requires a definition", () => {
160
+ expect(
161
+ TraceRecordingRuleDefinitionUtil.getValidationError(undefined),
162
+ ).toBe("Definition is required.");
163
+ });
164
+
165
+ test("requires at least one source", () => {
166
+ expect(
167
+ TraceRecordingRuleDefinitionUtil.getValidationError({
168
+ sources: [],
169
+ expression: "A",
170
+ }),
171
+ ).toBe("Add at least one source.");
172
+ });
173
+
174
+ test("rejects more than the maximum number of sources", () => {
175
+ const sources: Array<TraceRecordingRuleSource> = Array.from(
176
+ { length: TRACE_RECORDING_RULE_MAX_SOURCES + 1 },
177
+ (_unused: unknown, i: number) => {
178
+ return makeSource({ alias: "ABCDE"[i] as string });
179
+ },
180
+ );
181
+
182
+ expect(
183
+ TraceRecordingRuleDefinitionUtil.getValidationError({
184
+ sources,
185
+ expression: "A",
186
+ }),
187
+ ).toContain(`at most ${TRACE_RECORDING_RULE_MAX_SOURCES}`);
188
+ });
189
+
190
+ test("rejects an invalid alias", () => {
191
+ expect(
192
+ TraceRecordingRuleDefinitionUtil.getValidationError({
193
+ sources: [makeSource({ alias: "1" })],
194
+ expression: "A",
195
+ }),
196
+ ).toContain("single uppercase letter");
197
+ });
198
+
199
+ test("rejects duplicate aliases", () => {
200
+ expect(
201
+ TraceRecordingRuleDefinitionUtil.getValidationError({
202
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "A" })],
203
+ expression: "A",
204
+ }),
205
+ ).toContain("Duplicate alias");
206
+ });
207
+
208
+ test("requires an aggregation type", () => {
209
+ expect(
210
+ TraceRecordingRuleDefinitionUtil.getValidationError({
211
+ sources: [
212
+ {
213
+ alias: "A",
214
+ } as unknown as TraceRecordingRuleSource,
215
+ ],
216
+ expression: "A",
217
+ }),
218
+ ).toContain("Aggregation type is required");
219
+ });
220
+
221
+ test("requires both a legacy filter key and value, or neither", () => {
222
+ expect(
223
+ TraceRecordingRuleDefinitionUtil.getValidationError({
224
+ sources: [makeSource({ alias: "A", filterAttributeValue: "prod" })],
225
+ expression: "A",
226
+ }),
227
+ ).toContain("needs both a key and a value");
228
+ });
229
+
230
+ test("rejects a non-array filterAttributes", () => {
231
+ expect(
232
+ TraceRecordingRuleDefinitionUtil.getValidationError({
233
+ sources: [
234
+ makeSource({
235
+ alias: "A",
236
+ filterAttributes:
237
+ "nope" as unknown as Array<TraceRecordingRuleAttributeFilter>,
238
+ }),
239
+ ],
240
+ expression: "A",
241
+ }),
242
+ ).toContain("Invalid attribute filters");
243
+ });
244
+
245
+ test("rejects a half-specified filter row", () => {
246
+ expect(
247
+ TraceRecordingRuleDefinitionUtil.getValidationError({
248
+ sources: [
249
+ makeSource({
250
+ alias: "A",
251
+ filterAttributes: [{ key: "host", value: "" }],
252
+ }),
253
+ ],
254
+ expression: "A",
255
+ }),
256
+ ).toContain("remove the row");
257
+ });
258
+
259
+ test("requires an expression", () => {
260
+ expect(
261
+ TraceRecordingRuleDefinitionUtil.getValidationError({
262
+ sources: [makeSource({ alias: "A" })],
263
+ expression: " ",
264
+ }),
265
+ ).toBe("Expression is required.");
266
+ });
267
+
268
+ test("rejects an over-length expression", () => {
269
+ expect(
270
+ TraceRecordingRuleDefinitionUtil.getValidationError({
271
+ sources: [makeSource({ alias: "A" })],
272
+ expression: "A".repeat(
273
+ TRACE_RECORDING_RULE_MAX_EXPRESSION_LENGTH + 1,
274
+ ),
275
+ }),
276
+ ).toContain(
277
+ `${TRACE_RECORDING_RULE_MAX_EXPRESSION_LENGTH} characters or fewer`,
278
+ );
279
+ });
280
+
281
+ test("rejects an expression referencing an undefined alias", () => {
282
+ expect(
283
+ TraceRecordingRuleDefinitionUtil.getValidationError({
284
+ sources: [makeSource({ alias: "A" })],
285
+ expression: "A + B",
286
+ }),
287
+ ).toContain("references alias 'B'");
288
+ });
289
+
290
+ test("rejects an expression that references no alias", () => {
291
+ expect(
292
+ TraceRecordingRuleDefinitionUtil.getValidationError({
293
+ sources: [makeSource({ alias: "A" })],
294
+ expression: "5 + 10",
295
+ }),
296
+ ).toContain("must reference at least one source alias");
297
+ });
298
+
299
+ test("rejects an expression with characters outside the DSL grammar", () => {
300
+ expect(
301
+ TraceRecordingRuleDefinitionUtil.getValidationError({
302
+ sources: [makeSource({ alias: "A" }), makeSource({ alias: "B" })],
303
+ expression: "A & B",
304
+ }),
305
+ ).toContain("may only contain");
306
+ });
307
+ });
308
+ });
@@ -55,6 +55,23 @@ describe("Dropdown", () => {
55
55
  expect(await screen.findByText("2")).toBeInTheDocument();
56
56
  });
57
57
 
58
+ test("renders the menu portal above modal surfaces", async () => {
59
+ const { getByRole } = render(
60
+ <Dropdown onChange={() => {}} options={options} />,
61
+ );
62
+ const dropdown: HTMLElement = getByRole("combobox");
63
+
64
+ fireEvent.keyDown(dropdown, { key: "ArrowDown", code: "ArrowDown" });
65
+ await screen.findByText("1");
66
+
67
+ const menuPortal: HTMLElement | null = document.querySelector<HTMLElement>(
68
+ ".ou-select__menu-portal",
69
+ );
70
+
71
+ expect(menuPortal).toBeInTheDocument();
72
+ expect(menuPortal).toHaveStyle({ zIndex: "60" });
73
+ });
74
+
58
75
  test("renders placeholder", async () => {
59
76
  const { getByText } = render(
60
77
  <Dropdown
@@ -0,0 +1,130 @@
1
+ import EntityDropdown from "../../../UI/Components/EntityDropdown/EntityDropdown";
2
+ import {
3
+ DROPDOWN_MENU_Z_INDEX,
4
+ DropdownOption,
5
+ } from "../../../UI/Components/Dropdown/Dropdown";
6
+ import getJestMockFunction, { MockFunction } from "../../../Tests/MockType";
7
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
8
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
9
+ import React from "react";
10
+
11
+ describe("EntityDropdown", () => {
12
+ const options: Array<DropdownOption> = [
13
+ { value: "members", label: "Members" },
14
+ { value: "admins", label: "Admins" },
15
+ ];
16
+ const originalInnerHeight: number = window.innerHeight;
17
+ const originalInnerWidth: number = window.innerWidth;
18
+
19
+ const setViewport: (width: number, height: number) => void = (
20
+ width: number,
21
+ height: number,
22
+ ): void => {
23
+ Object.defineProperty(window, "innerWidth", {
24
+ configurable: true,
25
+ value: width,
26
+ });
27
+ Object.defineProperty(window, "innerHeight", {
28
+ configurable: true,
29
+ value: height,
30
+ });
31
+ };
32
+
33
+ const makeRect: (
34
+ left: number,
35
+ top: number,
36
+ width: number,
37
+ height: number,
38
+ ) => DOMRect = (
39
+ left: number,
40
+ top: number,
41
+ width: number,
42
+ height: number,
43
+ ): DOMRect => {
44
+ return {
45
+ bottom: top + height,
46
+ height,
47
+ left,
48
+ right: left + width,
49
+ top,
50
+ width,
51
+ x: left,
52
+ y: top,
53
+ toJSON: (): Record<string, never> => {
54
+ return {};
55
+ },
56
+ } as DOMRect;
57
+ };
58
+
59
+ const openDropdownAt: (rect: DOMRect) => HTMLInputElement = (
60
+ rect: DOMRect,
61
+ ): HTMLInputElement => {
62
+ const input: HTMLInputElement = screen.getByRole("combobox", {
63
+ name: "Team",
64
+ }) as HTMLInputElement;
65
+ const control: HTMLElement | null =
66
+ input.parentElement?.parentElement || null;
67
+
68
+ if (!control) {
69
+ throw new Error("EntityDropdown control was not rendered.");
70
+ }
71
+
72
+ jest.spyOn(control, "getBoundingClientRect").mockReturnValue(rect);
73
+ fireEvent.focus(input);
74
+
75
+ return input;
76
+ };
77
+
78
+ afterEach(() => {
79
+ cleanup();
80
+ jest.restoreAllMocks();
81
+ setViewport(originalInnerWidth, originalInnerHeight);
82
+ });
83
+
84
+ test("positions its fixed menu below the control above modal surfaces", () => {
85
+ setViewport(1000, 800);
86
+ render(<EntityDropdown ariaLabel="Team" options={options} />);
87
+
88
+ openDropdownAt(makeRect(120, 200, 320, 40));
89
+
90
+ const menu: HTMLElement = screen.getByTestId("entity-dropdown-menu");
91
+
92
+ expect(menu.classList.contains("fixed")).toBe(true);
93
+ expect(menu.style.top).toBe("244px");
94
+ expect(menu.style.bottom).toBe("");
95
+ expect(menu.style.left).toBe("120px");
96
+ expect(menu.style.width).toBe("320px");
97
+ expect(menu.style.maxHeight).toBe("384px");
98
+ expect(menu.style.visibility).toBe("visible");
99
+ expect(menu.style.zIndex).toBe(String(DROPDOWN_MENU_Z_INDEX));
100
+ });
101
+
102
+ test("flips its menu above a control near the modal footer", () => {
103
+ setViewport(1000, 600);
104
+ render(<EntityDropdown ariaLabel="Team" options={options} />);
105
+
106
+ openDropdownAt(makeRect(120, 520, 320, 40));
107
+
108
+ const menu: HTMLElement = screen.getByTestId("entity-dropdown-menu");
109
+
110
+ expect(menu.style.top).toBe("");
111
+ expect(menu.style.bottom).toBe("84px");
112
+ expect(menu.style.maxHeight).toBe("384px");
113
+ expect(menu.style.visibility).toBe("visible");
114
+ });
115
+
116
+ test("selects an option from the fixed menu", () => {
117
+ const onChange: MockFunction = getJestMockFunction();
118
+ setViewport(1000, 800);
119
+ render(
120
+ <EntityDropdown ariaLabel="Team" onChange={onChange} options={options} />,
121
+ );
122
+
123
+ openDropdownAt(makeRect(120, 200, 320, 40));
124
+ fireEvent.click(screen.getByRole("option", { name: "Members" }));
125
+
126
+ expect(onChange).toHaveBeenCalledTimes(1);
127
+ expect(onChange).toHaveBeenCalledWith("members");
128
+ expect(screen.queryByTestId("entity-dropdown-menu")).toBeNull();
129
+ });
130
+ });
@@ -26,6 +26,148 @@ describe("Modal", () => {
26
26
  expect(getByText("Modal content")).toBeInTheDocument();
27
27
  });
28
28
 
29
+ it("renders an accessible elevated dialog shell", () => {
30
+ const onCloseMock: MockFunction = getJestMockFunction();
31
+ const { getByTestId } = render(
32
+ <Modal
33
+ title="Accessible Modal"
34
+ description="A concise description"
35
+ onClose={onCloseMock}
36
+ >
37
+ <div>Modal content</div>
38
+ </Modal>,
39
+ );
40
+
41
+ const modal: HTMLElement = getByTestId("modal");
42
+ const title: HTMLElement = getByTestId("modal-title");
43
+ const description: HTMLElement = getByTestId("modal-description");
44
+ const closeButton: HTMLElement = getByTestId("close-button");
45
+
46
+ expect(modal).toHaveAttribute("role", "dialog");
47
+ expect(modal).toHaveAttribute("aria-modal", "true");
48
+ expect(modal).toHaveAttribute("aria-labelledby", title.id);
49
+ expect(modal).toHaveAttribute("aria-describedby", description.id);
50
+ expect(modal).toHaveClass("rounded-t-2xl", "shadow-2xl");
51
+ expect(getByTestId("modal-backdrop")).toHaveClass(
52
+ "bg-gray-950/45",
53
+ "backdrop-blur-[2px]",
54
+ );
55
+ expect(closeButton).toHaveClass("inline-flex");
56
+ expect(closeButton).not.toHaveClass("md:hidden");
57
+ });
58
+
59
+ it("keeps keyboard focus inside the modal", () => {
60
+ const onCloseMock: MockFunction = getJestMockFunction();
61
+ const onSubmitMock: MockFunction = getJestMockFunction();
62
+ const { getByLabelText, getByTestId } = render(
63
+ <Modal title="Focus Modal" onClose={onCloseMock} onSubmit={onSubmitMock}>
64
+ <input aria-label="Modal input" />
65
+ </Modal>,
66
+ );
67
+
68
+ const input: HTMLElement = getByLabelText("Modal input");
69
+ const headerCloseButton: HTMLElement = getByTestId("close-button");
70
+ const submitButton: HTMLElement = getByTestId("modal-footer-submit-button");
71
+
72
+ expect(input).toHaveFocus();
73
+
74
+ submitButton.focus();
75
+ fireEvent.keyDown(document, { key: "Tab" });
76
+ expect(headerCloseButton).toHaveFocus();
77
+
78
+ fireEvent.keyDown(document, { key: "Tab", shiftKey: true });
79
+ expect(submitButton).toHaveFocus();
80
+ });
81
+
82
+ it("closes only the topmost modal and restores focus to its opener", () => {
83
+ const onParentCloseMock: MockFunction = getJestMockFunction();
84
+ const onChildCloseMock: MockFunction = getJestMockFunction();
85
+ const onChildSubmitMock: MockFunction = getJestMockFunction();
86
+
87
+ const NestedModalFixture: React.FunctionComponent =
88
+ (): React.ReactElement => {
89
+ const [isChildOpen, setIsChildOpen] = React.useState<boolean>(false);
90
+
91
+ return (
92
+ <Modal title="Parent Modal" onClose={onParentCloseMock}>
93
+ <>
94
+ <button
95
+ type="button"
96
+ onClick={() => {
97
+ setIsChildOpen(true);
98
+ }}
99
+ >
100
+ Open child
101
+ </button>
102
+ {isChildOpen && (
103
+ <Modal
104
+ title="Child Modal"
105
+ onSubmit={onChildSubmitMock}
106
+ onClose={() => {
107
+ onChildCloseMock();
108
+ setIsChildOpen(false);
109
+ }}
110
+ >
111
+ <div>Child content</div>
112
+ </Modal>
113
+ )}
114
+ </>
115
+ </Modal>
116
+ );
117
+ };
118
+
119
+ const { getByRole } = render(<NestedModalFixture />);
120
+ const childOpener: HTMLElement = getByRole("button", {
121
+ name: "Open child",
122
+ });
123
+
124
+ fireEvent.click(childOpener);
125
+ fireEvent.keyDown(document, { key: "Escape" });
126
+
127
+ expect(onChildCloseMock).toHaveBeenCalledTimes(1);
128
+ expect(onParentCloseMock).not.toHaveBeenCalled();
129
+ expect(childOpener).toHaveFocus();
130
+ });
131
+
132
+ it("restores focus when a modal contains an auto-focused field", () => {
133
+ const opener: HTMLButtonElement = document.createElement("button");
134
+ document.body.appendChild(opener);
135
+ opener.focus();
136
+
137
+ const { getByLabelText, unmount } = render(
138
+ <Modal title="Auto Focus Modal">
139
+ <input aria-label="Auto-focused input" autoFocus />
140
+ </Modal>,
141
+ );
142
+
143
+ expect(getByLabelText("Auto-focused input")).toHaveFocus();
144
+
145
+ unmount();
146
+ expect(opener).toHaveFocus();
147
+ opener.remove();
148
+ });
149
+
150
+ it("renders the footer only when it has content", () => {
151
+ const { queryByTestId, rerender } = render(
152
+ <Modal title="No Actions Modal">
153
+ <div>Modal content</div>
154
+ </Modal>,
155
+ );
156
+
157
+ expect(queryByTestId("modal-footer")).not.toBeInTheDocument();
158
+
159
+ rerender(
160
+ <Modal
161
+ title="Utility Action Modal"
162
+ leftFooterElement={<button type="button">Utility action</button>}
163
+ >
164
+ <div>Modal content</div>
165
+ </Modal>,
166
+ );
167
+
168
+ expect(queryByTestId("modal-footer")).toBeInTheDocument();
169
+ });
170
+
29
171
  it("closes the modal when the close button is clicked", () => {
30
172
  const onCloseMock: MockFunction = getJestMockFunction();
31
173
  const onSubmit: MockFunction = getJestMockFunction();
@@ -72,7 +214,7 @@ describe("Modal", () => {
72
214
  </Modal>,
73
215
  );
74
216
 
75
- expect(getByTestId("modal")).toHaveClass("md:max-w-lg");
217
+ expect(getByTestId("modal")).toHaveClass("sm:max-w-lg", "md:max-w-lg");
76
218
  });
77
219
 
78
220
  it("displays the modal with the correct width when modalWidth is set", () => {
@@ -88,7 +230,7 @@ describe("Modal", () => {
88
230
  </Modal>,
89
231
  );
90
232
 
91
- expect(getByTestId("modal")).toHaveClass("md:max-w-3xl");
233
+ expect(getByTestId("modal")).toHaveClass("sm:max-w-3xl", "md:max-w-3xl");
92
234
  });
93
235
 
94
236
  it("displays the children passed to the modal", () => {