@oneuptime/common 12.0.23 → 12.0.25

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 (323) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  5. package/Models/DatabaseModels/Project.ts +68 -0
  6. package/Models/DatabaseModels/User.ts +68 -0
  7. package/Server/API/BaseAPI.ts +0 -24
  8. package/Server/API/SlackAPI.ts +0 -2
  9. package/Server/API/TelemetryAPI.ts +29 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  14. package/Server/Middleware/GzipRequestBody.ts +311 -0
  15. package/Server/Middleware/MultipartFormData.ts +87 -3
  16. package/Server/Middleware/SlackAuthorization.ts +96 -18
  17. package/Server/Services/DetectionRuleService.ts +73 -6
  18. package/Server/Services/Index.ts +205 -0
  19. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  20. package/Server/Services/NetworkDeviceService.ts +148 -0
  21. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  22. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  23. package/Server/Services/ProjectService.ts +29 -19
  24. package/Server/Services/SecurityEventService.ts +90 -13
  25. package/Server/Services/StatusPageResourceService.ts +223 -0
  26. package/Server/Services/TelemetryAttributeService.ts +17 -0
  27. package/Server/Services/UserService.ts +4 -6
  28. package/Server/Types/Markdown.ts +161 -67
  29. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  30. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  31. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  32. package/Server/Utils/Attribution.ts +4 -0
  33. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  34. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  35. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  36. package/Server/Utils/StartServer.ts +12 -32
  37. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  38. package/Server/Utils/Telemetry/LlmMetricSpend.ts +181 -0
  39. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  40. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  41. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  42. package/Server/Views/Partials/Head.ejs +2 -1
  43. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  44. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  45. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  46. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  47. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  48. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  49. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  50. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  51. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  52. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  53. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  54. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  55. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  56. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  57. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  58. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  59. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  60. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  61. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  62. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  63. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  64. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  65. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  66. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  67. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  68. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  69. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  70. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  71. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  72. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  73. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  74. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  75. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  76. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  77. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  78. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  79. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  80. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  81. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  83. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  84. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  85. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  86. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  87. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  88. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  89. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  90. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  91. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  92. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  93. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  94. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  95. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  96. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
  97. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
  98. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  99. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  100. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  101. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  102. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  103. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  104. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  105. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  106. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  107. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +584 -0
  108. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  109. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  110. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  111. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  112. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  113. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  114. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  115. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  116. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  117. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  118. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  119. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  120. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  121. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  122. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  123. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  124. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  125. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  126. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  138. package/Types/Telemetry/LlmConventions.ts +255 -0
  139. package/Types/Telemetry/LlmMetricConventions.ts +329 -0
  140. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  141. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  142. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  143. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  144. package/UI/Components/Detail/Detail.tsx +10 -0
  145. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  146. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  147. package/UI/Components/Forms/Types/Field.ts +8 -0
  148. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  149. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  150. package/UI/Components/Forms/Validation.ts +46 -0
  151. package/UI/Components/Input/Input.tsx +3 -0
  152. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  153. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  154. package/UI/Components/ModelTable/Column.ts +8 -0
  155. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  156. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  157. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  158. package/UI/Components/SideOver/SideOver.tsx +5 -5
  159. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  160. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  161. package/UI/Components/Toggle/Toggle.tsx +9 -1
  162. package/UI/Components/Types/FieldType.ts +1 -0
  163. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  164. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  165. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  166. package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
  167. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  168. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  170. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  172. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  174. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  176. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/User.js +76 -0
  178. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  179. package/build/dist/Server/API/BaseAPI.js +3 -19
  180. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  181. package/build/dist/Server/API/SlackAPI.js +0 -2
  182. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  183. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  184. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  193. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  194. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  195. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  196. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  197. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  198. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  199. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  200. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  201. package/build/dist/Server/Services/Index.js +204 -0
  202. package/build/dist/Server/Services/Index.js.map +1 -1
  203. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  204. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  205. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  206. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  210. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  211. package/build/dist/Server/Services/ProjectService.js +25 -45
  212. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  213. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  214. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  215. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  216. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  217. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  218. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  219. package/build/dist/Server/Services/UserService.js +2 -12
  220. package/build/dist/Server/Services/UserService.js.map +1 -1
  221. package/build/dist/Server/Types/Markdown.js +147 -61
  222. package/build/dist/Server/Types/Markdown.js.map +1 -1
  223. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  224. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  225. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  226. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  227. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  228. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  229. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  230. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  231. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  232. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  233. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  234. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  235. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  236. package/build/dist/Server/Utils/StartServer.js +12 -21
  237. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  238. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  239. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  240. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +151 -0
  241. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  242. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  243. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  244. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  245. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  246. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  247. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  248. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  249. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  250. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  251. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  252. package/build/dist/Types/Marketing/Attribution.js +10 -0
  253. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  254. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  255. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  256. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  257. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  258. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  259. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  260. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  261. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  262. package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -0
  263. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  264. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  265. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  266. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  267. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  268. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  269. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  270. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  271. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  272. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  273. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  274. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  275. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  276. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  277. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  278. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  279. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  280. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  281. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  282. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  283. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  284. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  285. package/build/dist/UI/Components/Input/Input.js +1 -1
  286. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  287. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  288. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  289. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  290. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  291. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  292. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  293. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  294. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  295. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  296. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  297. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  298. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  299. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  300. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  301. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  302. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  303. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  304. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  305. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  306. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  307. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  308. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  309. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  310. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  311. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +175 -0
  312. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  313. package/jest.config.json +2 -0
  314. package/package.json +1 -1
  315. package/tsconfig.json +2 -1
  316. package/Tests/UI/Components/Toast.test.tsx +0 -197
  317. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  318. package/UI/Components/Toast/Toast.tsx +0 -189
  319. package/UI/Components/Toast/ToastInit.tsx +0 -110
  320. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  321. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  322. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  323. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,321 @@
1
+ import Column from "./Column";
2
+ import Columns from "./Columns";
3
+ import { ColumnPreference } from "./ColumnPreference";
4
+ import FieldType from "../Types/FieldType";
5
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
6
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
7
+ import { JSONObject } from "../../../Types/JSON";
8
+ import React, { ReactElement } from "react";
9
+
10
+ /*
11
+ * ---------------------------------------------------------------------------
12
+ * One table column per key inside a Map(String, String) attributes column
13
+ * ---------------------------------------------------------------------------
14
+ *
15
+ * Analytics rows (logs, spans, security events) carry the whole source
16
+ * payload flattened into a single map column - `attributes` - so that
17
+ * arbitrary vendor fields stay queryable without a schema change. Those keys
18
+ * differ per event class and per source, so there is no fixed list to ship as
19
+ * columns: the viewer picks the ones they care about and the table grows a
20
+ * column for each.
21
+ *
22
+ * The shape mirrors CustomFieldColumns, and for the same reasons:
23
+ *
24
+ * field: { attributes: true } <- a real model column
25
+ * selectedProperty: "device.hostname" <- the key inside it
26
+ *
27
+ * which BaseModelTable turns into the column key "attributes.device.hostname".
28
+ *
29
+ * - the declared field has to be the real map column. A synthetic
30
+ * `field: { "attributes.device.hostname": true }` would fail the model's
31
+ * column check in getSelectFromColumns and throw the whole table away.
32
+ *
33
+ * - sorting is off. Nothing between the header and the query builder
34
+ * validates `sort`, so a clickable header here would send a dotted
35
+ * pseudo-column into ClickHouse, which can only order by a map key through
36
+ * an explicit `attributes['key']` subscript the sort path cannot express.
37
+ *
38
+ * - cells render through `getElement`, because the typed cell renderers index
39
+ * `item[column.key]` directly and would look for a literal
40
+ * "attributes.device.hostname" property that no row has.
41
+ *
42
+ * WHERE THE VIEWER'S CHOICE LIVES
43
+ *
44
+ * Nowhere new. A column exists iff its id appears in the stored
45
+ * ColumnPreference, and the id encodes the key, so the list of chosen
46
+ * attribute columns is recovered from the layout the viewer already saves
47
+ * (see getAttributeKeysFromColumnPreference). That keeps "Reset to default"
48
+ * honest, makes a saved TableView carry its attribute columns with it, and
49
+ * means there is no second store that can drift out of sync with the first.
50
+ */
51
+
52
+ export type GetAttributeColumnIdFunction = (data: {
53
+ attributesColumnKey: string;
54
+ attributeKey: string;
55
+ }) => string;
56
+
57
+ /*
58
+ * Deliberately the same shape ColumnPreference.getColumnBaseId would derive
59
+ * from `field` + `selectedProperty` on its own ("attributes.device.hostname"),
60
+ * so an explicitly-set id and a derived one can never disagree.
61
+ */
62
+ export const getAttributeColumnId: GetAttributeColumnIdFunction = (data: {
63
+ attributesColumnKey: string;
64
+ attributeKey: string;
65
+ }): string => {
66
+ return `${data.attributesColumnKey}.${data.attributeKey}`;
67
+ };
68
+
69
+ export type GetAttributeKeyFromColumnIdFunction = (data: {
70
+ attributesColumnKey: string;
71
+ columnId: string;
72
+ }) => string | null;
73
+
74
+ export const getAttributeKeyFromColumnId: GetAttributeKeyFromColumnIdFunction =
75
+ (data: { attributesColumnKey: string; columnId: string }): string | null => {
76
+ const prefix: string = `${data.attributesColumnKey}.`;
77
+
78
+ if (!data.columnId.startsWith(prefix)) {
79
+ return null;
80
+ }
81
+
82
+ const attributeKey: string = data.columnId.slice(prefix.length);
83
+
84
+ return attributeKey.length > 0 ? attributeKey : null;
85
+ };
86
+
87
+ export type NormalizeAttributeKeysFunction = (
88
+ keys: Array<unknown> | null | undefined,
89
+ ) => Array<string>;
90
+
91
+ /*
92
+ * Whatever the server said, turned into a stable list: trimmed, de-duplicated,
93
+ * and sorted. The sort is load-bearing rather than cosmetic - the picker shows
94
+ * these in order, and an unsorted list from ClickHouse would reshuffle itself
95
+ * between requests.
96
+ */
97
+ export const normalizeAttributeKeys: NormalizeAttributeKeysFunction = (
98
+ keys: Array<unknown> | null | undefined,
99
+ ): Array<string> => {
100
+ if (!Array.isArray(keys)) {
101
+ return [];
102
+ }
103
+
104
+ const seen: Set<string> = new Set();
105
+ const normalized: Array<string> = [];
106
+
107
+ for (const key of keys) {
108
+ if (typeof key !== "string") {
109
+ continue;
110
+ }
111
+
112
+ const trimmed: string = key.trim();
113
+
114
+ if (trimmed.length === 0 || seen.has(trimmed)) {
115
+ continue;
116
+ }
117
+
118
+ seen.add(trimmed);
119
+ normalized.push(trimmed);
120
+ }
121
+
122
+ return normalized.sort((a: string, b: string): number => {
123
+ return a.localeCompare(b);
124
+ });
125
+ };
126
+
127
+ export type GetAttributeValueFunction = (data: {
128
+ item: unknown;
129
+ attributesColumnKey: string;
130
+ attributeKey: string;
131
+ }) => unknown;
132
+
133
+ export const getAttributeValue: GetAttributeValueFunction = (data: {
134
+ item: unknown;
135
+ attributesColumnKey: string;
136
+ attributeKey: string;
137
+ }): unknown => {
138
+ const attributes: unknown = (data.item as JSONObject | undefined)?.[
139
+ data.attributesColumnKey
140
+ ];
141
+
142
+ if (!attributes || typeof attributes !== "object") {
143
+ return undefined;
144
+ }
145
+
146
+ return (attributes as JSONObject)[data.attributeKey];
147
+ };
148
+
149
+ export type GetAttributeExportValueFunction = (value: unknown) => string;
150
+
151
+ export const getAttributeExportValue: GetAttributeExportValueFunction = (
152
+ value: unknown,
153
+ ): string => {
154
+ if (value === undefined || value === null) {
155
+ return "";
156
+ }
157
+
158
+ if (Array.isArray(value)) {
159
+ return value
160
+ .map((entry: unknown): string => {
161
+ return getAttributeExportValue(entry);
162
+ })
163
+ .join(", ");
164
+ }
165
+
166
+ if (typeof value === "object") {
167
+ return JSON.stringify(value);
168
+ }
169
+
170
+ return String(value);
171
+ };
172
+
173
+ export type RenderAttributeValueFunction = (data: {
174
+ value: unknown;
175
+ noValueMessage?: string | undefined;
176
+ }) => ReactElement;
177
+
178
+ /*
179
+ * Map values arrive as strings, but the ingest side is free to write a
180
+ * flattened object or an array under a key, and a project can PUT anything
181
+ * over the API - so this renders whatever it is handed rather than assuming.
182
+ */
183
+ export const renderAttributeValue: RenderAttributeValueFunction = (data: {
184
+ value: unknown;
185
+ noValueMessage?: string | undefined;
186
+ }): ReactElement => {
187
+ const noValueMessage: string = data.noValueMessage ?? "-";
188
+ const text: string = getAttributeExportValue(data.value);
189
+
190
+ if (text.length === 0) {
191
+ return <span className="text-gray-400">{noValueMessage}</span>;
192
+ }
193
+
194
+ return (
195
+ <span className="break-words" title={text}>
196
+ {text}
197
+ </span>
198
+ );
199
+ };
200
+
201
+ export type GetAttributeColumnsFunction = <
202
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
203
+ >(data: {
204
+ attributeKeys: Array<string>;
205
+ attributesColumnKey: string;
206
+ isHiddenByDefault?: boolean | undefined;
207
+ noValueMessage?: string | undefined;
208
+ }) => Columns<TBaseModel>;
209
+
210
+ /*
211
+ * `isRemovable` is what separates these from every other optional column: a
212
+ * viewer who switches an attribute column off still has it sitting in the
213
+ * picker forever, and a table whose source writes a few hundred keys would
214
+ * accumulate an unusable list. Removing drops the id from the stored layout,
215
+ * which is the same thing as the column never having been added.
216
+ */
217
+ export const getAttributeColumns: GetAttributeColumnsFunction = <
218
+ TBaseModel extends BaseModel | AnalyticsBaseModel,
219
+ >(data: {
220
+ attributeKeys: Array<string>;
221
+ attributesColumnKey: string;
222
+ isHiddenByDefault?: boolean | undefined;
223
+ noValueMessage?: string | undefined;
224
+ }): Columns<TBaseModel> => {
225
+ const { attributesColumnKey } = data;
226
+
227
+ return (data.attributeKeys || []).map(
228
+ (attributeKey: string): Column<TBaseModel> => {
229
+ return {
230
+ id: getAttributeColumnId({ attributesColumnKey, attributeKey }),
231
+ field: {
232
+ [attributesColumnKey]: true,
233
+ } as Column<TBaseModel>["field"],
234
+ selectedProperty: attributeKey,
235
+ title: attributeKey,
236
+ type: FieldType.Element,
237
+ // See the file header: a map key is not something the sort path can express.
238
+ disableSort: true,
239
+ isHiddenByDefault: Boolean(data.isHiddenByDefault),
240
+ isRemovable: true,
241
+ getExportValue: (item: TBaseModel): string => {
242
+ return getAttributeExportValue(
243
+ getAttributeValue({ item, attributesColumnKey, attributeKey }),
244
+ );
245
+ },
246
+ getElement: (item: TBaseModel): ReactElement => {
247
+ return renderAttributeValue({
248
+ value: getAttributeValue({
249
+ item,
250
+ attributesColumnKey,
251
+ attributeKey,
252
+ }),
253
+ noValueMessage: data.noValueMessage,
254
+ });
255
+ },
256
+ };
257
+ },
258
+ );
259
+ };
260
+
261
+ export type GetAttributeKeysFromColumnPreferenceFunction = (data: {
262
+ preference: ColumnPreference | null | undefined;
263
+ attributesColumnKey: string;
264
+ /*
265
+ * Ids of the columns the table declares for itself. A declared column that
266
+ * happens to sit under the same prefix is not an attribute column, and
267
+ * regenerating one for it would put two columns with the same id in the
268
+ * picker.
269
+ */
270
+ reservedColumnIds?: Array<string> | undefined;
271
+ }) => Array<string>;
272
+
273
+ /*
274
+ * The attribute columns a stored layout implies.
275
+ *
276
+ * Read from the RAW preference rather than the sanitized one: sanitizing drops
277
+ * ids that name no current column, and an attribute column is only ever a
278
+ * current column *because* its id is in here. `hidden` is scanned as well as
279
+ * `order` - buildColumnPreference always writes both, but a payload from an
280
+ * older release (or one hand-edited in devtools) may carry only the one, and
281
+ * losing the column would silently discard the viewer's choice.
282
+ */
283
+ export const getAttributeKeysFromColumnPreference: GetAttributeKeysFromColumnPreferenceFunction =
284
+ (data: {
285
+ preference: ColumnPreference | null | undefined;
286
+ attributesColumnKey: string;
287
+ reservedColumnIds?: Array<string> | undefined;
288
+ }): Array<string> => {
289
+ if (!data.preference) {
290
+ return [];
291
+ }
292
+
293
+ const reserved: Set<string> = new Set(data.reservedColumnIds || []);
294
+ const seen: Set<string> = new Set();
295
+ const attributeKeys: Array<string> = [];
296
+
297
+ const columnIds: Array<string> = [
298
+ ...(data.preference.order || []),
299
+ ...(data.preference.hidden || []),
300
+ ];
301
+
302
+ for (const columnId of columnIds) {
303
+ if (reserved.has(columnId)) {
304
+ continue;
305
+ }
306
+
307
+ const attributeKey: string | null = getAttributeKeyFromColumnId({
308
+ attributesColumnKey: data.attributesColumnKey,
309
+ columnId: columnId,
310
+ });
311
+
312
+ if (!attributeKey || seen.has(attributeKey)) {
313
+ continue;
314
+ }
315
+
316
+ seen.add(attributeKey);
317
+ attributeKeys.push(attributeKey);
318
+ }
319
+
320
+ return attributeKeys;
321
+ };
@@ -62,7 +62,14 @@ import TableColumn from "../Table/Types/Column";
62
62
  import FieldType from "../Types/FieldType";
63
63
  import ModelTableColumn from "./Column";
64
64
  import Columns from "./Columns";
65
- import ColumnCustomizationModal from "./ColumnCustomizationModal";
65
+ import ColumnCustomizationModal, {
66
+ AddableColumnsConfig,
67
+ } from "./ColumnCustomizationModal";
68
+ import {
69
+ getAttributeColumns,
70
+ getAttributeKeysFromColumnPreference,
71
+ normalizeAttributeKeys,
72
+ } from "./AttributeColumns";
66
73
  import {
67
74
  ColumnPreference,
68
75
  CustomizableColumn,
@@ -364,6 +371,40 @@ export interface BaseTableProps<
364
371
  * Only meaningful for resources with a `customFields` column of their own.
365
372
  */
366
373
  customFieldsModelType?: { new (): BaseModel } | undefined;
374
+
375
+ /**
376
+ * Let the viewer add a column for any key inside a Map(String, String)
377
+ * column on the model — `attributes` on logs, spans and security events.
378
+ *
379
+ * Those keys are per-payload rather than per-schema, so they cannot ship as
380
+ * columns and there can be hundreds of them. Instead the picker grows an
381
+ * "add a column" search box fed by `fetchAttributeKeys`, and whatever the
382
+ * viewer adds becomes an ordinary column: hideable, re-orderable, exported,
383
+ * and remembered in the same stored layout as every other column.
384
+ *
385
+ * Requires `userPreferencesKey` and column customization to be on, since the
386
+ * chosen keys are recovered from the stored layout.
387
+ */
388
+ attributeColumnsProps?:
389
+ | {
390
+ /*
391
+ * The map column the generated columns read from. Must be a real
392
+ * column on the model; anything else is ignored rather than throwing
393
+ * the table away.
394
+ */
395
+ columnKey: string;
396
+ /*
397
+ * Every key the viewer may pick from. Called the first time the picker
398
+ * opens rather than on mount — it is typically a wide scan, and a
399
+ * table nobody customizes should not pay for it.
400
+ */
401
+ fetchAttributeKeys: () => Promise<Array<string>>;
402
+ title?: string | undefined;
403
+ description?: string | undefined;
404
+ placeholder?: string | undefined;
405
+ emptyMessage?: string | undefined;
406
+ }
407
+ | undefined;
367
408
  }
368
409
 
369
410
  export interface ComponentProps<
@@ -501,14 +542,6 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
501
542
  showAs !== ShowAs.OrderedStatesList,
502
543
  );
503
544
 
504
- const allColumns: Columns<TBaseModel> = useMemo(() => {
505
- if (customFieldColumns.columns.length === 0) {
506
- return props.columns || [];
507
- }
508
-
509
- return [...(props.columns || []), ...customFieldColumns.columns];
510
- }, [props.columns, customFieldColumns.columns]);
511
-
512
545
  type ReadStoredColumnPreferenceFunction = () => ColumnPreference | null;
513
546
 
514
547
  const readStoredColumnPreference: ReadStoredColumnPreferenceFunction =
@@ -531,6 +564,68 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
531
564
  const [showColumnCustomizationModal, setShowColumnCustomizationModal] =
532
565
  useState<boolean>(false);
533
566
 
567
+ /*
568
+ * ---------------------------------------------------------------------
569
+ * Attribute columns
570
+ * ---------------------------------------------------------------------
571
+ *
572
+ * A column per viewer-chosen key inside the model's map column. There is no
573
+ * second store for "which keys did they pick": a column exists precisely
574
+ * when its id is in the stored layout, and the id encodes the key. That is
575
+ * what makes "Reset to default" take them away, a saved TableView bring its
576
+ * own, and the two halves of a layout impossible to desynchronize.
577
+ *
578
+ * Derived from the RAW preference, before sanitizing: sanitizing drops ids
579
+ * that name no current column, and these are only current columns *because*
580
+ * their ids are in there.
581
+ */
582
+ const attributeColumnKey: string | null = useMemo(() => {
583
+ const columnKey: string | undefined =
584
+ props.attributeColumnsProps?.columnKey;
585
+
586
+ if (!columnKey || !isColumnCustomizationEnabled) {
587
+ return null;
588
+ }
589
+
590
+ /*
591
+ * A key that is not a column on the model would make getSelectFromColumns
592
+ * throw and blank the whole table. A misconfigured prop should cost the
593
+ * feature, not the page.
594
+ */
595
+ return model.hasColumn(columnKey) ? columnKey : null;
596
+ }, [props.attributeColumnsProps?.columnKey, isColumnCustomizationEnabled]);
597
+
598
+ const declaredColumns: Columns<TBaseModel> = useMemo(() => {
599
+ if (customFieldColumns.columns.length === 0) {
600
+ return props.columns || [];
601
+ }
602
+
603
+ return [...(props.columns || []), ...customFieldColumns.columns];
604
+ }, [props.columns, customFieldColumns.columns]);
605
+
606
+ const attributeColumns: Columns<TBaseModel> = useMemo(() => {
607
+ if (!attributeColumnKey) {
608
+ return [];
609
+ }
610
+
611
+ return getAttributeColumns<TBaseModel>({
612
+ attributesColumnKey: attributeColumnKey,
613
+ attributeKeys: getAttributeKeysFromColumnPreference({
614
+ preference: columnPreference,
615
+ attributesColumnKey: attributeColumnKey,
616
+ reservedColumnIds: getColumnIds<TBaseModel>(declaredColumns),
617
+ }),
618
+ });
619
+ }, [attributeColumnKey, columnPreference, declaredColumns]);
620
+
621
+ const allColumns: Columns<TBaseModel> = useMemo(() => {
622
+ if (attributeColumns.length === 0) {
623
+ return declaredColumns;
624
+ }
625
+
626
+ return [...declaredColumns, ...attributeColumns];
627
+ }, [declaredColumns, attributeColumns]);
628
+
534
629
  /*
535
630
  * Ids are derived over the *whole* declared set, so a column dropping out
536
631
  * for lack of permission can never renumber the ones that remain.
@@ -584,6 +679,18 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
584
679
  });
585
680
  };
586
681
 
682
+ /*
683
+ * The pool of keys the picker offers. Fetched the first time the picker
684
+ * opens, never on mount: it is a wide scan over the analytics table, and a
685
+ * table nobody customizes should not pay for it on every page load.
686
+ */
687
+ const [attributeKeyPool, setAttributeKeyPool] = useState<{
688
+ keys: Array<string>;
689
+ isLoading: boolean;
690
+ error: string;
691
+ hasLoaded: boolean;
692
+ }>({ keys: [], isLoading: false, error: "", hasLoaded: false });
693
+
587
694
  const [classicTableFilters, setClassicTableFilters] = useState<
588
695
  Array<ClassicFilterType<TBaseModel>>
589
696
  >([]);
@@ -1972,6 +2079,20 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1972
2079
  (selectFields as Dictionary<boolean>)[CustomFieldsColumnKey] = true;
1973
2080
  }
1974
2081
 
2082
+ /*
2083
+ * Same reasoning for the attribute map column: a column added from the
2084
+ * picker appears without a refetch, so the map it reads has to have been
2085
+ * on the wire since the first request or the new column renders blank
2086
+ * until something else happens to reload the page.
2087
+ */
2088
+ if (
2089
+ attributeColumnKey &&
2090
+ (hasPermissionToReadField(attributeColumnKey as keyof TBaseModel) ||
2091
+ User.isMasterAdmin())
2092
+ ) {
2093
+ (selectFields as Dictionary<boolean>)[attributeColumnKey] = true;
2094
+ }
2095
+
1975
2096
  const selectMoreFields: Array<keyof TBaseModel> = props.selectMoreFields
1976
2097
  ? (Object.keys(props.selectMoreFields) as Array<keyof TBaseModel>)
1977
2098
  : [];
@@ -2431,12 +2552,77 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
2431
2552
  }, [props.columns]);
2432
2553
 
2433
2554
  /*
2434
- * The viewer changed their layout, or the project's custom field
2435
- * definitions finally arrived.
2555
+ * The viewer changed their layout, the project's custom field definitions
2556
+ * finally arrived, or an attribute column was added or removed.
2436
2557
  */
2437
2558
  useEffect(() => {
2438
2559
  serializeToTableColumns();
2439
- }, [effectiveColumnPreference, customFieldColumns.columns]);
2560
+ }, [effectiveColumnPreference, customFieldColumns.columns, attributeColumns]);
2561
+
2562
+ /*
2563
+ * The attribute key pool is only ever needed by the picker, so it is fetched
2564
+ * when the picker first opens and kept for the rest of the page's life.
2565
+ * A failure costs the "add a column" search box and nothing else — the
2566
+ * columns the viewer already added are rebuilt from their stored layout and
2567
+ * do not depend on this at all.
2568
+ */
2569
+ useEffect(() => {
2570
+ if (
2571
+ !showColumnCustomizationModal ||
2572
+ !attributeColumnKey ||
2573
+ !props.attributeColumnsProps ||
2574
+ attributeKeyPool.hasLoaded ||
2575
+ attributeKeyPool.isLoading
2576
+ ) {
2577
+ return;
2578
+ }
2579
+
2580
+ const fetchAttributeKeys: () => Promise<Array<string>> =
2581
+ props.attributeColumnsProps.fetchAttributeKeys;
2582
+
2583
+ let isCancelled: boolean = false;
2584
+
2585
+ setAttributeKeyPool({
2586
+ keys: [],
2587
+ isLoading: true,
2588
+ error: "",
2589
+ hasLoaded: false,
2590
+ });
2591
+
2592
+ fetchAttributeKeys()
2593
+ .then((keys: Array<string>) => {
2594
+ if (isCancelled) {
2595
+ return;
2596
+ }
2597
+
2598
+ setAttributeKeyPool({
2599
+ keys: normalizeAttributeKeys(keys),
2600
+ isLoading: false,
2601
+ error: "",
2602
+ hasLoaded: true,
2603
+ });
2604
+ })
2605
+ .catch((err: Error) => {
2606
+ if (isCancelled) {
2607
+ return;
2608
+ }
2609
+
2610
+ setAttributeKeyPool({
2611
+ keys: [],
2612
+ isLoading: false,
2613
+ error: API.getFriendlyMessage(err),
2614
+ /*
2615
+ * Not "loaded": closing and reopening the picker is the only retry
2616
+ * anyone would think to try, so let it be one.
2617
+ */
2618
+ hasLoaded: false,
2619
+ });
2620
+ });
2621
+
2622
+ return () => {
2623
+ isCancelled = true;
2624
+ };
2625
+ }, [showColumnCustomizationModal, attributeColumnKey]);
2440
2626
 
2441
2627
  const setActionSchema: VoidFunction = () => {
2442
2628
  const permissions: Array<Permission> = PermissionUtil.getAllPermissions();
@@ -4007,13 +4193,15 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
4007
4193
 
4008
4194
  type GetPickerEntriesFunction = (
4009
4195
  preference: ColumnPreference | null,
4196
+ columns?: Columns<TBaseModel> | undefined,
4010
4197
  ) => Array<CustomizableColumn<TBaseModel>>;
4011
4198
 
4012
4199
  const getPickerEntries: GetPickerEntriesFunction = (
4013
4200
  preference: ColumnPreference | null,
4201
+ columns?: Columns<TBaseModel> | undefined,
4014
4202
  ): Array<CustomizableColumn<TBaseModel>> => {
4015
4203
  return getCustomizableColumns<TBaseModel>({
4016
- columns: allColumns,
4204
+ columns: columns || allColumns,
4017
4205
  preference: preference,
4018
4206
  }).filter((entry: CustomizableColumn<TBaseModel>) => {
4019
4207
  return isPickableColumn(entry.column);
@@ -4055,8 +4243,16 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
4055
4243
  * would be pinned to the column set of the release they opened it in, and
4056
4244
  * columns added later would arrive already stale.
4057
4245
  */
4246
+ /*
4247
+ * Over the DECLARED columns only. An attribute column exists because the
4248
+ * viewer added it, so the layout that has none of them is the default one
4249
+ * — otherwise adding a column and then removing it again would leave the
4250
+ * table pinned to a stored layout it no longer differs from.
4251
+ */
4058
4252
  const defaultPreference: ColumnPreference =
4059
- buildColumnPreference<TBaseModel>(getPickerEntries(null));
4253
+ buildColumnPreference<TBaseModel>(
4254
+ getPickerEntries(null, declaredColumns),
4255
+ );
4060
4256
 
4061
4257
  const isBackToDefault: boolean =
4062
4258
  JSON.stringify(preference) === JSON.stringify(defaultPreference);
@@ -4089,6 +4285,58 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
4089
4285
  setShowColumnCustomizationModal(false);
4090
4286
  };
4091
4287
 
4288
+ /*
4289
+ * The pool the picker offers, as picker entries rather than model columns,
4290
+ * so the modal can stage one straight into its list. Every one starts
4291
+ * removable — the whole point is that the viewer can take it away again.
4292
+ */
4293
+ const addableAttributeColumns: Array<CustomizableColumn<TBaseModel>> =
4294
+ useMemo(() => {
4295
+ if (!attributeColumnKey || attributeKeyPool.keys.length === 0) {
4296
+ return [];
4297
+ }
4298
+
4299
+ return getAttributeColumns<TBaseModel>({
4300
+ attributesColumnKey: attributeColumnKey,
4301
+ attributeKeys: attributeKeyPool.keys,
4302
+ }).map(
4303
+ (
4304
+ column: ModelTableColumn<TBaseModel>,
4305
+ index: number,
4306
+ ): CustomizableColumn<TBaseModel> => {
4307
+ return {
4308
+ id: column.id || `${attributeColumnKey}.${index}`,
4309
+ column: column,
4310
+ isVisible: true,
4311
+ isPinned: false,
4312
+ isRemovable: true,
4313
+ };
4314
+ },
4315
+ );
4316
+ }, [attributeColumnKey, attributeKeyPool.keys]);
4317
+
4318
+ const getAddableColumnsConfig: () =>
4319
+ | AddableColumnsConfig<TBaseModel>
4320
+ | undefined = (): AddableColumnsConfig<TBaseModel> | undefined => {
4321
+ if (!attributeColumnKey || !props.attributeColumnsProps) {
4322
+ return undefined;
4323
+ }
4324
+
4325
+ return {
4326
+ title: props.attributeColumnsProps.title || tx("Add Attribute Column"),
4327
+ description:
4328
+ props.attributeColumnsProps.description ||
4329
+ tx(
4330
+ "Attributes vary per event, so they are not listed above. Search for one to add it as a column.",
4331
+ ),
4332
+ placeholder: props.attributeColumnsProps.placeholder || "Search...",
4333
+ emptyMessage: props.attributeColumnsProps.emptyMessage,
4334
+ isLoading: attributeKeyPool.isLoading,
4335
+ errorMessage: attributeKeyPool.error,
4336
+ columns: addableAttributeColumns,
4337
+ };
4338
+ };
4339
+
4092
4340
  const getColumnCustomizationModal: () => ReactElement | null =
4093
4341
  (): ReactElement | null => {
4094
4342
  if (!showColumnCustomizationModal || !isColumnCustomizationEnabled) {
@@ -4098,6 +4346,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
4098
4346
  return (
4099
4347
  <ColumnCustomizationModal<TBaseModel>
4100
4348
  columns={getPickerEntries(effectiveColumnPreference)}
4349
+ addableColumns={getAddableColumnsConfig()}
4101
4350
  isDefaultLayout={isEmptyColumnPreference(effectiveColumnPreference)}
4102
4351
  title={tx("Customize Columns")}
4103
4352
  onSave={onColumnCustomizationSave}
@@ -36,6 +36,14 @@ export default interface Columns<
36
36
  isNotCustomizable?: boolean | undefined;
37
37
  // Start hidden. The viewer can still switch it on from the picker.
38
38
  isHiddenByDefault?: boolean | undefined;
39
+ /*
40
+ * This column exists only because the viewer added it, so the picker offers
41
+ * to take it away again rather than only switching it off. Set it on columns
42
+ * generated from viewer-chosen keys (see AttributeColumns) - a table that
43
+ * ships a column should never be removable, because nothing would put it
44
+ * back.
45
+ */
46
+ isRemovable?: boolean | undefined;
39
47
  contentClassName?: string | undefined;
40
48
  colSpan?: number | undefined;
41
49
  disableSort?: boolean;