@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,500 @@
1
+ import useLogsHistogram, {
2
+ FetchHistogramBucketsFunction,
3
+ LogsHistogramState,
4
+ } from "../../../UI/Components/LogsViewer/useLogsHistogram";
5
+ import { HistogramBucket } from "../../../UI/Components/LogsViewer/types";
6
+ import { act, render, screen, waitFor } from "@testing-library/react";
7
+ import React, { FunctionComponent, ReactElement, useCallback } from "react";
8
+ import { beforeEach, describe, expect, test } from "@jest/globals";
9
+
10
+ function bucket(time: string, count: number): HistogramBucket {
11
+ return { time, severity: "Error", count };
12
+ }
13
+
14
+ const FIRST_WINDOW: Array<HistogramBucket> = [bucket("11:59", 3)];
15
+ const SECOND_WINDOW: Array<HistogramBucket> = [
16
+ bucket("11:59", 3),
17
+ bucket("12:00", 8),
18
+ ];
19
+
20
+ interface ProbeProps {
21
+ fetchBuckets: FetchHistogramBucketsFunction;
22
+ /** Bumping this hands the hook a new query identity, as a filter change would. */
23
+ queryKey?: string | undefined;
24
+ onReady?: ((state: LogsHistogramState) => void) | undefined;
25
+ }
26
+
27
+ /*
28
+ * Renders the hook's state so assertions read it through a real component
29
+ * lifecycle, and hands the state object back so tests can trigger a refresh
30
+ * the way live mode does.
31
+ */
32
+ const HistogramProbe: FunctionComponent<ProbeProps> = (
33
+ props: ProbeProps,
34
+ ): ReactElement => {
35
+ const { fetchBuckets, queryKey } = props;
36
+
37
+ const fetch: FetchHistogramBucketsFunction = useCallback(() => {
38
+ return fetchBuckets();
39
+ /*
40
+ * queryKey stands in for the filters/time range the real caller closes
41
+ * over: changing it is what makes the hook reload.
42
+ */
43
+ }, [fetchBuckets, queryKey]);
44
+
45
+ const state: LogsHistogramState = useLogsHistogram(fetch);
46
+
47
+ props.onReady?.(state);
48
+
49
+ return (
50
+ <div>
51
+ <span data-testid="counts">
52
+ {state.buckets
53
+ .map((item: HistogramBucket) => {
54
+ return `${item.time}=${item.count}`;
55
+ })
56
+ .join(",")}
57
+ </span>
58
+ <span data-testid="loading">{String(state.isLoading)}</span>
59
+ </div>
60
+ );
61
+ };
62
+
63
+ function counts(): string {
64
+ return screen.getByTestId("counts").textContent || "";
65
+ }
66
+
67
+ function isLoading(): boolean {
68
+ return screen.getByTestId("loading").textContent === "true";
69
+ }
70
+
71
+ describe("useLogsHistogram", () => {
72
+ let latest: LogsHistogramState | null;
73
+ let loadingStates: Array<boolean>;
74
+
75
+ const captureState: (state: LogsHistogramState) => void = (
76
+ state: LogsHistogramState,
77
+ ): void => {
78
+ latest = state;
79
+ loadingStates.push(state.isLoading);
80
+ };
81
+
82
+ async function refresh(silent: boolean): Promise<void> {
83
+ await act(async () => {
84
+ await latest!.refresh(silent ? { silent: true } : {});
85
+ });
86
+ }
87
+
88
+ beforeEach(() => {
89
+ latest = null;
90
+ loadingStates = [];
91
+ });
92
+
93
+ describe("first load", () => {
94
+ test("shows the buckets the query returns", async () => {
95
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
96
+ Array<HistogramBucket>
97
+ > => {
98
+ return FIRST_WINDOW;
99
+ };
100
+
101
+ render(<HistogramProbe fetchBuckets={fetchBuckets} />);
102
+
103
+ await waitFor(() => {
104
+ expect(counts()).toBe("11:59=3");
105
+ });
106
+ });
107
+
108
+ test("raises the loading flag while the query runs", async () => {
109
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
110
+ Array<HistogramBucket>
111
+ > => {
112
+ return FIRST_WINDOW;
113
+ };
114
+
115
+ render(
116
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
117
+ );
118
+
119
+ await waitFor(() => {
120
+ expect(counts()).toBe("11:59=3");
121
+ });
122
+
123
+ expect(loadingStates).toContain(true);
124
+ expect(isLoading()).toBe(false);
125
+ });
126
+
127
+ test("leaves the chart empty when the query fails", async () => {
128
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
129
+ Array<HistogramBucket>
130
+ > => {
131
+ throw new Error("clickhouse timeout");
132
+ };
133
+
134
+ render(
135
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
136
+ );
137
+
138
+ await waitFor(() => {
139
+ expect(isLoading()).toBe(false);
140
+ });
141
+
142
+ expect(counts()).toBe("");
143
+ });
144
+ });
145
+
146
+ describe("when the query changes", () => {
147
+ test("reloads for the new filters", async () => {
148
+ let window: Array<HistogramBucket> = FIRST_WINDOW;
149
+
150
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
151
+ Array<HistogramBucket>
152
+ > => {
153
+ return window;
154
+ };
155
+
156
+ const view: ReturnType<typeof render> = render(
157
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-hour" />,
158
+ );
159
+
160
+ await waitFor(() => {
161
+ expect(counts()).toBe("11:59=3");
162
+ });
163
+
164
+ window = SECOND_WINDOW;
165
+
166
+ view.rerender(
167
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-day" />,
168
+ );
169
+
170
+ await waitFor(() => {
171
+ expect(counts()).toBe("11:59=3,12:00=8");
172
+ });
173
+ });
174
+
175
+ test("does not reload when nothing about the query changed", async () => {
176
+ let calls: number = 0;
177
+
178
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
179
+ Array<HistogramBucket>
180
+ > => {
181
+ calls++;
182
+ return FIRST_WINDOW;
183
+ };
184
+
185
+ const view: ReturnType<typeof render> = render(
186
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-hour" />,
187
+ );
188
+
189
+ await waitFor(() => {
190
+ expect(counts()).toBe("11:59=3");
191
+ });
192
+
193
+ view.rerender(
194
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-hour" />,
195
+ );
196
+
197
+ expect(calls).toBe(1);
198
+ });
199
+
200
+ /*
201
+ * A poll fired against the old time range can easily outlive the switch to
202
+ * a new one. Painting its answer would leave the reader looking at a
203
+ * window they have already moved off.
204
+ */
205
+ test("ignores an answer to a query the reader has already left", async () => {
206
+ const pending: Array<(buckets: Array<HistogramBucket>) => void> = [];
207
+
208
+ const fetchBuckets: FetchHistogramBucketsFunction = (): Promise<
209
+ Array<HistogramBucket>
210
+ > => {
211
+ return new Promise<Array<HistogramBucket>>(
212
+ (resolve: (buckets: Array<HistogramBucket>) => void) => {
213
+ pending.push(resolve);
214
+ },
215
+ );
216
+ };
217
+
218
+ const view: ReturnType<typeof render> = render(
219
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-hour" />,
220
+ );
221
+
222
+ await waitFor(() => {
223
+ expect(pending.length).toBe(1);
224
+ });
225
+
226
+ view.rerender(
227
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="five-minutes" />,
228
+ );
229
+
230
+ await waitFor(() => {
231
+ expect(pending.length).toBe(2);
232
+ });
233
+
234
+ // The new window answers first, then the abandoned one straggles in.
235
+ await act(async () => {
236
+ pending[1]!(SECOND_WINDOW);
237
+ });
238
+ await act(async () => {
239
+ pending[0]!(FIRST_WINDOW);
240
+ });
241
+
242
+ expect(counts()).toBe("11:59=3,12:00=8");
243
+ });
244
+
245
+ test("does not blank the chart when an abandoned query fails", async () => {
246
+ const pending: Array<{
247
+ resolve: (buckets: Array<HistogramBucket>) => void;
248
+ reject: (error: Error) => void;
249
+ }> = [];
250
+
251
+ const fetchBuckets: FetchHistogramBucketsFunction = (): Promise<
252
+ Array<HistogramBucket>
253
+ > => {
254
+ return new Promise<Array<HistogramBucket>>(
255
+ (
256
+ resolve: (buckets: Array<HistogramBucket>) => void,
257
+ reject: (error: Error) => void,
258
+ ) => {
259
+ pending.push({ resolve: resolve, reject: reject });
260
+ },
261
+ );
262
+ };
263
+
264
+ const view: ReturnType<typeof render> = render(
265
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="one-hour" />,
266
+ );
267
+
268
+ await waitFor(() => {
269
+ expect(pending.length).toBe(1);
270
+ });
271
+
272
+ view.rerender(
273
+ <HistogramProbe fetchBuckets={fetchBuckets} queryKey="five-minutes" />,
274
+ );
275
+
276
+ await waitFor(() => {
277
+ expect(pending.length).toBe(2);
278
+ });
279
+
280
+ await act(async () => {
281
+ pending[1]!.resolve(SECOND_WINDOW);
282
+ });
283
+ await act(async () => {
284
+ pending[0]!.reject(new Error("clickhouse timeout"));
285
+ });
286
+
287
+ expect(counts()).toBe("11:59=3,12:00=8");
288
+ expect(isLoading()).toBe(false);
289
+ });
290
+ });
291
+
292
+ /*
293
+ * Live mode polls the histogram every few seconds. These are the properties
294
+ * that keep the chart from flashing, blanking or stampeding while it does.
295
+ */
296
+ describe("silent refresh", () => {
297
+ test("moves the chart on to the newer window", async () => {
298
+ let window: Array<HistogramBucket> = FIRST_WINDOW;
299
+
300
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
301
+ Array<HistogramBucket>
302
+ > => {
303
+ return window;
304
+ };
305
+
306
+ render(
307
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
308
+ );
309
+
310
+ await waitFor(() => {
311
+ expect(counts()).toBe("11:59=3");
312
+ });
313
+
314
+ window = SECOND_WINDOW;
315
+ await refresh(true);
316
+
317
+ expect(counts()).toBe("11:59=3,12:00=8");
318
+ });
319
+
320
+ test("never raises the loading flag", async () => {
321
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
322
+ Array<HistogramBucket>
323
+ > => {
324
+ return FIRST_WINDOW;
325
+ };
326
+
327
+ render(
328
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
329
+ );
330
+
331
+ await waitFor(() => {
332
+ expect(counts()).toBe("11:59=3");
333
+ });
334
+
335
+ loadingStates = [];
336
+
337
+ await refresh(true);
338
+ await refresh(true);
339
+
340
+ expect(loadingStates).not.toContain(true);
341
+ });
342
+
343
+ test("keeps the chart on screen when a poll fails", async () => {
344
+ let shouldFail: boolean = false;
345
+
346
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
347
+ Array<HistogramBucket>
348
+ > => {
349
+ if (shouldFail) {
350
+ throw new Error("clickhouse timeout");
351
+ }
352
+
353
+ return FIRST_WINDOW;
354
+ };
355
+
356
+ render(
357
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
358
+ );
359
+
360
+ await waitFor(() => {
361
+ expect(counts()).toBe("11:59=3");
362
+ });
363
+
364
+ shouldFail = true;
365
+ await refresh(true);
366
+
367
+ expect(counts()).toBe("11:59=3");
368
+ });
369
+
370
+ test("recovers on the next poll after a failure", async () => {
371
+ let shouldFail: boolean = true;
372
+ let window: Array<HistogramBucket> = FIRST_WINDOW;
373
+
374
+ const fetchBuckets: FetchHistogramBucketsFunction = async (): Promise<
375
+ Array<HistogramBucket>
376
+ > => {
377
+ if (shouldFail) {
378
+ throw new Error("clickhouse timeout");
379
+ }
380
+
381
+ return window;
382
+ };
383
+
384
+ render(
385
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
386
+ );
387
+
388
+ await waitFor(() => {
389
+ expect(isLoading()).toBe(false);
390
+ });
391
+
392
+ shouldFail = false;
393
+ window = SECOND_WINDOW;
394
+ await refresh(true);
395
+
396
+ expect(counts()).toBe("11:59=3,12:00=8");
397
+ });
398
+
399
+ test("drops a poll that lands while the previous one is still running", async () => {
400
+ const pending: Array<(buckets: Array<HistogramBucket>) => void> = [];
401
+
402
+ const fetchBuckets: FetchHistogramBucketsFunction = (): Promise<
403
+ Array<HistogramBucket>
404
+ > => {
405
+ return new Promise<Array<HistogramBucket>>(
406
+ (resolve: (buckets: Array<HistogramBucket>) => void) => {
407
+ pending.push(resolve);
408
+ },
409
+ );
410
+ };
411
+
412
+ render(
413
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
414
+ );
415
+
416
+ // Let the first load settle so only the polls are in play.
417
+ await waitFor(() => {
418
+ expect(pending.length).toBe(1);
419
+ });
420
+ await act(async () => {
421
+ pending[0]!(FIRST_WINDOW);
422
+ });
423
+
424
+ // A poll that is still waiting on a slow aggregation...
425
+ let slowPoll: Promise<void> = Promise.resolve();
426
+ await act(async () => {
427
+ slowPoll = latest!.refresh({ silent: true });
428
+ });
429
+ expect(pending.length).toBe(2);
430
+
431
+ // ...swallows the ticks that land behind it.
432
+ await act(async () => {
433
+ await latest!.refresh({ silent: true });
434
+ await latest!.refresh({ silent: true });
435
+ });
436
+ expect(pending.length).toBe(2);
437
+
438
+ await act(async () => {
439
+ pending[1]!(SECOND_WINDOW);
440
+ await slowPoll;
441
+ });
442
+
443
+ expect(counts()).toBe("11:59=3,12:00=8");
444
+
445
+ // The gate lifts once the slow poll comes back.
446
+ await act(async () => {
447
+ void latest!.refresh({ silent: true });
448
+ });
449
+ expect(pending.length).toBe(3);
450
+
451
+ await act(async () => {
452
+ pending[2]!(SECOND_WINDOW);
453
+ });
454
+ });
455
+
456
+ test("does not hold up a full reload triggered by the reader", async () => {
457
+ const pending: Array<(buckets: Array<HistogramBucket>) => void> = [];
458
+
459
+ const fetchBuckets: FetchHistogramBucketsFunction = (): Promise<
460
+ Array<HistogramBucket>
461
+ > => {
462
+ return new Promise<Array<HistogramBucket>>(
463
+ (resolve: (buckets: Array<HistogramBucket>) => void) => {
464
+ pending.push(resolve);
465
+ },
466
+ );
467
+ };
468
+
469
+ render(
470
+ <HistogramProbe fetchBuckets={fetchBuckets} onReady={captureState} />,
471
+ );
472
+
473
+ await waitFor(() => {
474
+ expect(pending.length).toBe(1);
475
+ });
476
+ await act(async () => {
477
+ pending[0]!(FIRST_WINDOW);
478
+ });
479
+
480
+ await act(async () => {
481
+ void latest!.refresh({ silent: true });
482
+ });
483
+ expect(pending.length).toBe(2);
484
+
485
+ // A filter change or a manual reload must not be swallowed by the gate.
486
+ await act(async () => {
487
+ void latest!.refresh();
488
+ });
489
+ expect(pending.length).toBe(3);
490
+ expect(isLoading()).toBe(true);
491
+
492
+ await act(async () => {
493
+ pending[1]!(FIRST_WINDOW);
494
+ pending[2]!(SECOND_WINDOW);
495
+ });
496
+
497
+ expect(isLoading()).toBe(false);
498
+ });
499
+ });
500
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import DashboardComponentsUtil from "../../../../Utils/Dashboard/Components/Index";
3
+ import DashboardComponentType from "../../../../Types/Dashboard/DashboardComponentType";
4
+ import {
5
+ ComponentArgument,
6
+ ComponentInputType,
7
+ } from "../../../../Types/Dashboard/DashboardComponents/ComponentArgument";
8
+ import DashboardBaseComponent from "../../../../Types/Dashboard/DashboardComponents/DashboardBaseComponent";
9
+ import BadDataException from "../../../../Types/Exception/BadDataException";
10
+
11
+ /*
12
+ * DashboardComponentsUtil.getComponentSettingsArguments is the single dispatch
13
+ * from a widget's type to the config-form fields the dashboard editor renders
14
+ * for it. It is a long if/else chain that ends in `throw BadDataException`, so
15
+ * the failure mode it invites is a real one: add a widget to the
16
+ * DashboardComponentType enum, ship the component util, and forget the branch
17
+ * here — every attempt to configure that widget then throws at runtime with no
18
+ * compiler warning, because the chain has no exhaustiveness check.
19
+ *
20
+ * This suite is that missing exhaustiveness check. It also validates the shape
21
+ * of what every branch returns, so a malformed argument list (a duplicate id
22
+ * that silently drops a form field, an empty label) fails here instead of in
23
+ * the editor.
24
+ */
25
+
26
+ const ALL_TYPES: Array<DashboardComponentType> = Object.values(
27
+ DashboardComponentType,
28
+ );
29
+
30
+ const VALID_INPUT_TYPES: Set<string> = new Set<string>(
31
+ Object.values(ComponentInputType),
32
+ );
33
+
34
+ describe("DashboardComponentsUtil.getComponentSettingsArguments", () => {
35
+ // Sanity: the enum is non-trivial, so an empty loop can't vacuously pass.
36
+ test("there is more than one dashboard component type to cover", () => {
37
+ expect(ALL_TYPES.length).toBeGreaterThan(1);
38
+ });
39
+
40
+ test.each(ALL_TYPES)(
41
+ "%s is wired into the dispatcher and returns an argument array",
42
+ (type: DashboardComponentType) => {
43
+ // The whole point: no enum member may fall through to BadDataException.
44
+ const args: Array<ComponentArgument<DashboardBaseComponent>> =
45
+ DashboardComponentsUtil.getComponentSettingsArguments(type);
46
+ expect(Array.isArray(args)).toBe(true);
47
+ },
48
+ );
49
+
50
+ test.each(ALL_TYPES)(
51
+ "every config argument for %s is well-formed",
52
+ (type: DashboardComponentType) => {
53
+ const args: Array<ComponentArgument<DashboardBaseComponent>> =
54
+ DashboardComponentsUtil.getComponentSettingsArguments(type);
55
+
56
+ for (const arg of args) {
57
+ // A blank label renders an unlabeled form field.
58
+ expect(typeof arg.name).toBe("string");
59
+ expect(arg.name.length).toBeGreaterThan(0);
60
+
61
+ /*
62
+ * id is the key the entered value is stored under; empty means the
63
+ * value has nowhere to go.
64
+ */
65
+ expect(typeof arg.id).toBe("string");
66
+ expect(String(arg.id).length).toBeGreaterThan(0);
67
+
68
+ /*
69
+ * required drives validation; it must be an actual boolean, not
70
+ * undefined coerced as falsy.
71
+ */
72
+ expect(typeof arg.required).toBe("boolean");
73
+
74
+ // An unknown input type renders no editor at all.
75
+ expect(VALID_INPUT_TYPES.has(arg.type)).toBe(true);
76
+ }
77
+ },
78
+ );
79
+
80
+ test.each(ALL_TYPES)(
81
+ "%s has no duplicate argument ids",
82
+ (type: DashboardComponentType) => {
83
+ const args: Array<ComponentArgument<DashboardBaseComponent>> =
84
+ DashboardComponentsUtil.getComponentSettingsArguments(type);
85
+
86
+ const ids: Array<string> = args.map(
87
+ (a: ComponentArgument<DashboardBaseComponent>) => {
88
+ return String(a.id);
89
+ },
90
+ );
91
+ // A duplicate id means the second field overwrites the first's value.
92
+ expect(new Set<string>(ids).size).toBe(ids.length);
93
+ },
94
+ );
95
+
96
+ test("an unknown component type is rejected with BadDataException", () => {
97
+ expect(() => {
98
+ return DashboardComponentsUtil.getComponentSettingsArguments(
99
+ "NotARealComponentType" as DashboardComponentType,
100
+ );
101
+ }).toThrow(BadDataException);
102
+ });
103
+
104
+ test("calling twice returns equivalent, independent argument arrays", () => {
105
+ /*
106
+ * The editor may request the arguments repeatedly; each call must yield a
107
+ * fresh array so mutating one rendering cannot corrupt the next.
108
+ */
109
+ const first: Array<ComponentArgument<DashboardBaseComponent>> =
110
+ DashboardComponentsUtil.getComponentSettingsArguments(
111
+ DashboardComponentType.Text,
112
+ );
113
+ const second: Array<ComponentArgument<DashboardBaseComponent>> =
114
+ DashboardComponentsUtil.getComponentSettingsArguments(
115
+ DashboardComponentType.Text,
116
+ );
117
+ expect(second).toEqual(first);
118
+ expect(second).not.toBe(first);
119
+ });
120
+ });