@oneuptime/common 12.0.2 → 12.0.3

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 (228) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  4. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  5. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  6. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  7. package/Server/API/BaseAPI.ts +13 -2
  8. package/Server/API/DashboardAPI.ts +39 -0
  9. package/Server/EnvironmentConfig.ts +31 -1
  10. package/Server/Infrastructure/GlobalCache.ts +115 -0
  11. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  14. package/Server/Services/AlertStateTimelineService.ts +20 -0
  15. package/Server/Services/CephClusterService.ts +26 -86
  16. package/Server/Services/CloudResourceService.ts +23 -83
  17. package/Server/Services/DatabaseService.ts +111 -0
  18. package/Server/Services/DockerHostService.ts +19 -79
  19. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  20. package/Server/Services/HostService.ts +46 -99
  21. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/IoTFleetService.ts +19 -79
  24. package/Server/Services/KubernetesClusterService.ts +17 -78
  25. package/Server/Services/LabelService.ts +57 -10
  26. package/Server/Services/MetricTypeService.ts +84 -0
  27. package/Server/Services/MonitorService.ts +89 -14
  28. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  29. package/Server/Services/MonitorTemplateService.ts +65 -0
  30. package/Server/Services/PodmanHostService.ts +19 -79
  31. package/Server/Services/ProxmoxClusterService.ts +23 -83
  32. package/Server/Services/RumApplicationService.ts +19 -80
  33. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  34. package/Server/Services/ServerlessFunctionService.ts +24 -85
  35. package/Server/Services/ServiceService.ts +299 -73
  36. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  37. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  38. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  39. package/Server/Services/TelemetryEntityService.ts +1 -0
  40. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  41. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  44. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  45. package/Server/Utils/SingleFlight.ts +83 -0
  46. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  47. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  48. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  49. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  50. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  51. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  52. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  53. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  54. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  55. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  56. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  57. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  58. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  59. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  60. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  61. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  62. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  63. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  64. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  65. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  66. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  67. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  68. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  69. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  70. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  71. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  72. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  73. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  74. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  75. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  76. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  77. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  78. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  79. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  80. package/Tests/Types/Decimal.test.ts +37 -0
  81. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  82. package/Tests/Types/Port.test.ts +38 -0
  83. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  84. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  85. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  86. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  87. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  88. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  89. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  90. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  91. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  92. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  93. package/Types/Database/DatabaseProperty.ts +26 -0
  94. package/Types/Database/NumericColumnValue.ts +128 -0
  95. package/Types/Decimal.ts +16 -4
  96. package/Types/Permission.ts +48 -0
  97. package/Types/Port.ts +16 -4
  98. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  99. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  100. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  101. package/Utils/Dashboard/Components/Index.ts +7 -0
  102. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  103. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  105. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  107. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  109. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  111. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  113. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  114. package/build/dist/Server/API/BaseAPI.js +11 -1
  115. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  116. package/build/dist/Server/API/DashboardAPI.js +39 -0
  117. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  118. package/build/dist/Server/EnvironmentConfig.js +27 -1
  119. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  120. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  121. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  123. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  128. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  129. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  130. package/build/dist/Server/Services/CephClusterService.js +26 -76
  131. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  132. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  133. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  134. package/build/dist/Server/Services/DatabaseService.js +86 -0
  135. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  136. package/build/dist/Server/Services/DockerHostService.js +19 -69
  137. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  138. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  139. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  140. package/build/dist/Server/Services/HostService.js +58 -102
  141. package/build/dist/Server/Services/HostService.js.map +1 -1
  142. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  143. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  144. package/build/dist/Server/Services/Index.js +2 -0
  145. package/build/dist/Server/Services/Index.js.map +1 -1
  146. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  147. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  148. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  149. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  150. package/build/dist/Server/Services/LabelService.js +42 -7
  151. package/build/dist/Server/Services/LabelService.js.map +1 -1
  152. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  153. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  154. package/build/dist/Server/Services/MonitorService.js +81 -9
  155. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  156. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  157. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  158. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  159. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  160. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  161. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  162. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  163. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  164. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  165. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  168. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  169. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  170. package/build/dist/Server/Services/ServiceService.js +236 -61
  171. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  173. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  174. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  175. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  176. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  177. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  178. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  179. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  180. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  181. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  183. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  185. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  186. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  187. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  190. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  191. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  192. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  193. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  194. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  195. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  196. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  197. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  198. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  199. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  200. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  201. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  202. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  203. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  204. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  205. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  206. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  207. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  208. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  209. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  210. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  211. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  212. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  213. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  214. package/build/dist/Types/Decimal.js +14 -3
  215. package/build/dist/Types/Decimal.js.map +1 -1
  216. package/build/dist/Types/Permission.js +42 -0
  217. package/build/dist/Types/Permission.js.map +1 -1
  218. package/build/dist/Types/Port.js +15 -5
  219. package/build/dist/Types/Port.js.map +1 -1
  220. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  221. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  222. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  223. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  224. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  225. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  226. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  227. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  228. package/package.json +1 -1
@@ -0,0 +1,276 @@
1
+ /** @timezone UTC */
2
+
3
+ import LogsHistogram from "../../../UI/Components/LogsViewer/components/LogsHistogram";
4
+ import { HistogramBucket } from "../../../UI/Components/LogsViewer/types";
5
+ import LogSeverity from "../../../Types/Log/LogSeverity";
6
+ import { render, screen } from "@testing-library/react";
7
+ import React from "react";
8
+ import { describe, expect, test } from "@jest/globals";
9
+
10
+ /*
11
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so the
12
+ * chart renders nothing without a fixed size.
13
+ */
14
+ jest.mock("recharts", () => {
15
+ const actual: Record<string, any> = jest.requireActual("recharts");
16
+ return {
17
+ ...actual,
18
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
19
+ return React.cloneElement(children, { width: 600, height: 300 });
20
+ },
21
+ };
22
+ });
23
+
24
+ function bucket(
25
+ time: string,
26
+ severity: string,
27
+ count: number,
28
+ ): HistogramBucket {
29
+ return { time, severity, count };
30
+ }
31
+
32
+ /** One bar rectangle is drawn per severity per bucket that has a count. */
33
+ function barCount(container: HTMLElement): number {
34
+ return container.querySelectorAll(".recharts-bar-rectangle").length;
35
+ }
36
+
37
+ /** One <g> per stacked series, i.e. per severity present in the data. */
38
+ function seriesCount(container: HTMLElement): number {
39
+ return container.querySelectorAll(".recharts-bar").length;
40
+ }
41
+
42
+ const CLOCK_LABEL: RegExp = /^\d{2}:\d{2}$/;
43
+
44
+ /*
45
+ * Recharts portals tick labels out of their axis group, so the two axes are
46
+ * told apart by what they render: the time axis is formatted as a clock, the
47
+ * count axis as a number.
48
+ */
49
+ function axisLabels(
50
+ container: HTMLElement,
51
+ axis: "time" | "count",
52
+ ): Array<string> {
53
+ const labels: Array<string> = Array.from(
54
+ container.querySelectorAll(".recharts-cartesian-axis-tick-value"),
55
+ ).map((tick: Element): string => {
56
+ return tick.textContent || "";
57
+ });
58
+
59
+ return labels.filter((label: string): boolean => {
60
+ return CLOCK_LABEL.test(label) === (axis === "time");
61
+ });
62
+ }
63
+
64
+ const ONE_SEVERITY: Array<HistogramBucket> = [
65
+ bucket("2026-08-05T11:58:00Z", LogSeverity.Error, 3),
66
+ bucket("2026-08-05T11:59:00Z", LogSeverity.Error, 5),
67
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Error, 7),
68
+ ];
69
+
70
+ describe("LogsHistogram", () => {
71
+ describe("empty and loading states", () => {
72
+ test("renders nothing when the window holds no logs", () => {
73
+ const { container } = render(
74
+ <LogsHistogram buckets={[]} isLoading={false} />,
75
+ );
76
+
77
+ expect(container.innerHTML).toBe("");
78
+ });
79
+
80
+ test("shows the loader on the very first load", () => {
81
+ render(<LogsHistogram buckets={[]} isLoading={true} />);
82
+
83
+ expect(screen.queryByTestId("component-loader")).not.toBeNull();
84
+ });
85
+
86
+ /*
87
+ * Live mode refreshes the histogram every few seconds. Swapping the chart
88
+ * for a loader on each poll would make it flash; the chart already on
89
+ * screen stays until the new buckets land.
90
+ */
91
+ test("keeps the chart on screen while a refresh is in flight", () => {
92
+ const { container } = render(
93
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={true} />,
94
+ );
95
+
96
+ expect(screen.queryByTestId("component-loader")).toBeNull();
97
+ expect(screen.queryByText("Log Volume")).not.toBeNull();
98
+ expect(barCount(container)).toBe(3);
99
+ });
100
+ });
101
+
102
+ describe("drawing the buckets", () => {
103
+ test("draws one bar per bucket", () => {
104
+ const { container } = render(
105
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
106
+ );
107
+
108
+ expect(barCount(container)).toBe(3);
109
+ expect(seriesCount(container)).toBe(1);
110
+ });
111
+
112
+ test("stacks a bar per severity inside a bucket", () => {
113
+ const { container } = render(
114
+ <LogsHistogram
115
+ buckets={[
116
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Error, 4),
117
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Warning, 6),
118
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Information, 9),
119
+ ]}
120
+ isLoading={false}
121
+ />,
122
+ );
123
+
124
+ expect(seriesCount(container)).toBe(3);
125
+ expect(barCount(container)).toBe(3);
126
+ });
127
+
128
+ test("labels the time axis in the reader's clock", () => {
129
+ const { container } = render(
130
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
131
+ );
132
+
133
+ expect(axisLabels(container, "time")).toContain("12:00");
134
+ });
135
+
136
+ test("lists only the severities present in the window", () => {
137
+ render(
138
+ <LogsHistogram
139
+ buckets={[
140
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Error, 4),
141
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Debug, 1),
142
+ ]}
143
+ isLoading={false}
144
+ />,
145
+ );
146
+
147
+ expect(screen.queryByText("Error")).not.toBeNull();
148
+ expect(screen.queryByText("Debug")).not.toBeNull();
149
+ expect(screen.queryByText("Fatal")).toBeNull();
150
+ });
151
+ });
152
+
153
+ /*
154
+ * What live mode looks like from the chart's side: the same component is
155
+ * handed a fresh set of buckets every poll and has to redraw.
156
+ */
157
+ describe("when a live refresh brings new data", () => {
158
+ test("adds a bar as a new bucket opens", () => {
159
+ const view: ReturnType<typeof render> = render(
160
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
161
+ );
162
+
163
+ expect(barCount(view.container)).toBe(3);
164
+
165
+ view.rerender(
166
+ <LogsHistogram
167
+ buckets={[
168
+ ...ONE_SEVERITY,
169
+ bucket("2026-08-05T12:01:00Z", LogSeverity.Error, 2),
170
+ ]}
171
+ isLoading={false}
172
+ />,
173
+ );
174
+
175
+ expect(barCount(view.container)).toBe(4);
176
+ });
177
+
178
+ test("follows the window as it slides off the oldest bucket", () => {
179
+ const view: ReturnType<typeof render> = render(
180
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
181
+ );
182
+
183
+ expect(axisLabels(view.container, "time")).toContain("11:58");
184
+
185
+ view.rerender(
186
+ <LogsHistogram
187
+ buckets={[
188
+ ...ONE_SEVERITY.slice(1),
189
+ bucket("2026-08-05T12:01:00Z", LogSeverity.Error, 2),
190
+ ]}
191
+ isLoading={false}
192
+ />,
193
+ );
194
+
195
+ const labels: Array<string> = axisLabels(view.container, "time");
196
+ expect(labels).not.toContain("11:58");
197
+ expect(labels).toContain("12:01");
198
+ });
199
+
200
+ test("rescales the count axis as the newest bucket fills up", () => {
201
+ const view: ReturnType<typeof render> = render(
202
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
203
+ );
204
+
205
+ const before: Array<string> = axisLabels(view.container, "count");
206
+
207
+ view.rerender(
208
+ <LogsHistogram
209
+ buckets={[
210
+ ...ONE_SEVERITY.slice(0, 2),
211
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Error, 400),
212
+ ]}
213
+ isLoading={false}
214
+ />,
215
+ );
216
+
217
+ const after: Array<string> = axisLabels(view.container, "count");
218
+
219
+ expect(after).not.toEqual(before);
220
+ expect(Number(after[after.length - 1])).toBeGreaterThan(
221
+ Number(before[before.length - 1]),
222
+ );
223
+ });
224
+
225
+ test("adds a series and a legend entry when a new severity shows up", () => {
226
+ const view: ReturnType<typeof render> = render(
227
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
228
+ );
229
+
230
+ expect(screen.queryByText("Fatal")).toBeNull();
231
+
232
+ view.rerender(
233
+ <LogsHistogram
234
+ buckets={[
235
+ ...ONE_SEVERITY,
236
+ bucket("2026-08-05T12:00:00Z", LogSeverity.Fatal, 1),
237
+ ]}
238
+ isLoading={false}
239
+ />,
240
+ );
241
+
242
+ expect(screen.queryByText("Fatal")).not.toBeNull();
243
+ expect(seriesCount(view.container)).toBe(2);
244
+ });
245
+
246
+ test("empties the chart when the refreshed window has no logs", () => {
247
+ const view: ReturnType<typeof render> = render(
248
+ <LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />,
249
+ );
250
+
251
+ view.rerender(<LogsHistogram buckets={[]} isLoading={false} />);
252
+
253
+ expect(view.container.innerHTML).toBe("");
254
+ });
255
+ });
256
+
257
+ describe("drag to zoom", () => {
258
+ test("offers the affordance when the parent can act on a selection", () => {
259
+ render(
260
+ <LogsHistogram
261
+ buckets={ONE_SEVERITY}
262
+ isLoading={false}
263
+ onTimeRangeSelect={() => {}}
264
+ />,
265
+ );
266
+
267
+ expect(screen.queryByText("Drag to zoom")).not.toBeNull();
268
+ });
269
+
270
+ test("hides the affordance when there is nothing to zoom into", () => {
271
+ render(<LogsHistogram buckets={ONE_SEVERITY} isLoading={false} />);
272
+
273
+ expect(screen.queryByText("Drag to zoom")).toBeNull();
274
+ });
275
+ });
276
+ });