@oneuptime/common 12.0.19 → 12.0.20

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 (66) hide show
  1. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  2. package/Server/EnvironmentConfig.ts +17 -0
  3. package/Server/Services/ProjectService.ts +104 -0
  4. package/Server/Utils/Response.ts +2 -2
  5. package/Server/Utils/StartServer.ts +2 -2
  6. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  7. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  8. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  9. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  10. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  11. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  12. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  13. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  14. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  15. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  16. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  17. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  18. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  19. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  20. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  21. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  22. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  23. package/Types/Analytics/RevenueEvent.ts +1 -0
  24. package/Types/Marketing/MarketingEvent.ts +49 -6
  25. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  26. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  27. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  28. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  29. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  30. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  31. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  32. package/UI/Components/Detail/Detail.tsx +18 -1
  33. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  34. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  35. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  36. package/build/dist/Server/EnvironmentConfig.js +15 -0
  37. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  38. package/build/dist/Server/Services/ProjectService.js +88 -0
  39. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  40. package/build/dist/Server/Utils/Response.js +2 -2
  41. package/build/dist/Server/Utils/Response.js.map +1 -1
  42. package/build/dist/Server/Utils/StartServer.js +2 -2
  43. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  44. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  45. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  46. package/build/dist/Types/Marketing/MarketingEvent.js +40 -1
  47. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  48. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  49. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  50. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  51. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  52. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  53. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  54. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  55. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  56. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  57. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  58. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  59. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  60. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  61. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  62. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  63. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  64. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  65. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  66. package/package.json +1 -1
@@ -0,0 +1,368 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ cleanup,
12
+ fireEvent,
13
+ render,
14
+ screen,
15
+ waitFor,
16
+ } from "@testing-library/react";
17
+ import * as React from "react";
18
+ import getJestMockFunction, { MockFunction } from "../../MockType";
19
+
20
+ /*
21
+ * The per-series investigate menu is the chart's bridge from "this series
22
+ * is spiking" to the signals that explain it: the chip's magnifier must
23
+ * open a menu whose pivots carry the series' group-by labels as REAL
24
+ * filters (logs/traces URLs), whose "Filter to this series" narrows the
25
+ * live explorer view, and whose resource entry lands on the host page the
26
+ * series is about. Rendering the real MetricCharts (not a stub) pins the
27
+ * whole chain: series naming → chip → menu → scoped URL.
28
+ */
29
+
30
+ /*
31
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom,
32
+ * so the chart renders nothing without a fixed size.
33
+ */
34
+ jest.mock("recharts", () => {
35
+ const actual: Record<string, any> = jest.requireActual("recharts") as Record<
36
+ string,
37
+ any
38
+ >;
39
+ return {
40
+ ...actual,
41
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
42
+ return React.cloneElement(children, {
43
+ width: 600,
44
+ height: 300,
45
+ } as Record<string, unknown>);
46
+ },
47
+ };
48
+ });
49
+
50
+ const getListMock: MockFunction = getJestMockFunction();
51
+ const fetchExemplarsMock: MockFunction = getJestMockFunction();
52
+ const showToastMock: MockFunction = getJestMockFunction();
53
+
54
+ /*
55
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
56
+ * compiled requires, so the mock variables above are still unassigned when
57
+ * the factory runs. Dereferencing them lazily, at call time, is what makes
58
+ * this work.
59
+ */
60
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
61
+ return {
62
+ __esModule: true,
63
+ default: {
64
+ getList: (...args: Array<any>) => {
65
+ return getListMock(...args);
66
+ },
67
+ },
68
+ };
69
+ });
70
+
71
+ jest.mock("../../../UI/Components/Toast/ToastInit", () => {
72
+ return {
73
+ __esModule: true,
74
+ ShowToastNotification: (...args: Array<any>) => {
75
+ return showToastMock(...args);
76
+ },
77
+ };
78
+ });
79
+
80
+ /*
81
+ * Keep the exemplar fetch (fired by MetricCharts whenever the view has a
82
+ * resolvable window) off the network; everything else on the Metrics util
83
+ * surface that MetricCharts touches is pure enough to stub inline.
84
+ */
85
+ jest.mock(
86
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
87
+ () => {
88
+ return {
89
+ __esModule: true,
90
+ default: {
91
+ fetchExemplars: (...args: Array<any>) => {
92
+ fetchExemplarsMock(...args);
93
+ /*
94
+ * Never resolves: a resolved exemplar set would setState after
95
+ * each test's assertions and trip React's act() warning; no
96
+ * test here cares about exemplar dots.
97
+ */
98
+ return new Promise(() => {
99
+ // Intentionally never settles.
100
+ });
101
+ },
102
+ setQueryTopNOverride: () => {
103
+ return undefined;
104
+ },
105
+ getQueryConfigTopNKey: (
106
+ _queryConfig: unknown,
107
+ index: number,
108
+ scope?: string,
109
+ ) => {
110
+ return `${scope || ""}:${index}`;
111
+ },
112
+ clearQueryTopNOverridesForScope: () => {
113
+ return undefined;
114
+ },
115
+ serializeAttributeFiltersForKey: (attributes: unknown) => {
116
+ return JSON.stringify(attributes || {});
117
+ },
118
+ },
119
+ DEFAULT_TOP_N_SERIES: 10,
120
+ SHOW_ALL_SERIES_TOP_N: 10_000,
121
+ sanitizeAttributeFilters: (attributes: unknown) => {
122
+ return attributes;
123
+ },
124
+ };
125
+ },
126
+ );
127
+
128
+ import MetricCharts from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts";
129
+ import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
130
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
131
+ import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
132
+ import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
133
+ import MetricViewData from "../../../Types/Metrics/MetricViewData";
134
+ import ObjectID from "../../../Types/ObjectID";
135
+ import Navigation from "../../../UI/Utils/Navigation";
136
+ import ProjectUtil from "../../../UI/Utils/Project";
137
+
138
+ const PROJECT_ID: ObjectID = new ObjectID(
139
+ "77777777-1111-4111-8111-777777777777",
140
+ );
141
+ const HOST_MODEL_ID: string = "88888888-2222-4222-8222-888888888888";
142
+
143
+ const WINDOW_START: Date = new Date("2026-08-20T10:00:00.000Z");
144
+ const WINDOW_END: Date = new Date("2026-08-20T11:00:00.000Z");
145
+
146
+ function buildQueryConfig(): MetricQueryConfigData {
147
+ return {
148
+ metricAliasData: { metricVariable: "a" },
149
+ metricQueryData: {
150
+ filterData: {
151
+ metricName: "cpu.usage",
152
+ attributes: {},
153
+ aggegationType: MetricsAggregationType.Avg,
154
+ },
155
+ groupByAttributeKeys: ["host.name"],
156
+ },
157
+ } as unknown as MetricQueryConfigData;
158
+ }
159
+
160
+ function buildViewData(): MetricViewData {
161
+ return {
162
+ queryConfigs: [buildQueryConfig()],
163
+ formulaConfigs: [],
164
+ startAndEndDate: new InBetween<Date>(WINDOW_START, WINDOW_END),
165
+ } as MetricViewData;
166
+ }
167
+
168
+ function buildResults(): Array<AggregatedResult> {
169
+ const timestamps: Array<string> = [
170
+ "2026-08-20T10:10:00.000Z",
171
+ "2026-08-20T10:20:00.000Z",
172
+ ];
173
+ const data: Array<Record<string, unknown>> = [];
174
+ for (const host of ["web-01", "web-02"]) {
175
+ for (const [index, timestamp] of timestamps.entries()) {
176
+ data.push({
177
+ timestamp: new Date(timestamp),
178
+ value: host === "web-01" ? 90 + index : 10 + index,
179
+ attributes: { "host.name": host },
180
+ });
181
+ }
182
+ }
183
+ return [{ data, truncated: false } as unknown as AggregatedResult];
184
+ }
185
+
186
+ let navigateSpy: ReturnType<typeof jest.spyOn>;
187
+
188
+ beforeEach(() => {
189
+ getListMock.mockReset();
190
+ fetchExemplarsMock.mockReset();
191
+ showToastMock.mockReset();
192
+ navigateSpy = jest.spyOn(Navigation, "navigate").mockImplementation(() => {
193
+ return undefined;
194
+ });
195
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
196
+ });
197
+
198
+ afterEach(() => {
199
+ jest.restoreAllMocks();
200
+ cleanup();
201
+ });
202
+
203
+ function getNavigatedUrl(): string {
204
+ const lastCall: Array<unknown> | undefined =
205
+ navigateSpy.mock.calls[navigateSpy.mock.calls.length - 1];
206
+ return decodeURIComponent(String(lastCall?.[0]));
207
+ }
208
+
209
+ describe("per-series investigate menu", () => {
210
+ test("each series chip carries an investigate button that opens the menu", () => {
211
+ render(
212
+ <MetricCharts
213
+ metricViewData={buildViewData()}
214
+ metricResults={buildResults()}
215
+ metricTypes={[]}
216
+ />,
217
+ );
218
+
219
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
220
+
221
+ const menu: HTMLElement = screen.getByRole("menu", {
222
+ name: "Series investigation actions",
223
+ });
224
+ expect(menu).toBeInTheDocument();
225
+ expect(screen.getByText("View logs")).toBeInTheDocument();
226
+ expect(screen.getByText("View traces")).toBeInTheDocument();
227
+ expect(screen.getByText("View exceptions")).toBeInTheDocument();
228
+ // The menu header names the series and what it is scoped to.
229
+ expect(menu.textContent).toContain("host.name = web-01");
230
+ });
231
+
232
+ test("read-only hosts get an explorer deep link; read-write hosts filter in place", () => {
233
+ const onQueryConfigsChange: MockFunction = getJestMockFunction();
234
+
235
+ const { unmount } = render(
236
+ <MetricCharts
237
+ metricViewData={buildViewData()}
238
+ metricResults={buildResults()}
239
+ metricTypes={[]}
240
+ />,
241
+ );
242
+
243
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
244
+ expect(screen.getByText("Open in Metric Explorer")).toBeInTheDocument();
245
+ expect(screen.queryByText("Filter to this series")).toBeNull();
246
+ unmount();
247
+
248
+ render(
249
+ <MetricCharts
250
+ metricViewData={buildViewData()}
251
+ metricResults={buildResults()}
252
+ metricTypes={[]}
253
+ onQueryConfigsChange={(...args: Array<any>) => {
254
+ onQueryConfigsChange(...args);
255
+ }}
256
+ />,
257
+ );
258
+
259
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
260
+ expect(screen.queryByText("Open in Metric Explorer")).toBeNull();
261
+
262
+ fireEvent.click(screen.getByText("Filter to this series"));
263
+
264
+ expect(onQueryConfigsChange).toHaveBeenCalledTimes(1);
265
+ const narrowedConfigs: Array<MetricQueryConfigData> = onQueryConfigsChange
266
+ .mock.calls[0]?.[0] as Array<MetricQueryConfigData>;
267
+ expect(
268
+ (
269
+ narrowedConfigs[0]?.metricQueryData.filterData as Record<
270
+ string,
271
+ unknown
272
+ >
273
+ )["attributes"],
274
+ ).toEqual({ "host.name": "web-01" });
275
+ });
276
+
277
+ test("View logs navigates to the logs explorer scoped to the series and window", async () => {
278
+ render(
279
+ <MetricCharts
280
+ metricViewData={buildViewData()}
281
+ metricResults={buildResults()}
282
+ metricTypes={[]}
283
+ />,
284
+ );
285
+
286
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-02"));
287
+ fireEvent.click(screen.getByText("View logs"));
288
+
289
+ await waitFor(() => {
290
+ expect(navigateSpy).toHaveBeenCalled();
291
+ });
292
+
293
+ const url: string = getNavigatedUrl();
294
+ expect(url).toContain("/logs");
295
+ expect(url).toContain('["attributes.host.name",["web-02"]]');
296
+ expect(url).toContain("range=Custom");
297
+ expect(url).toContain("start=");
298
+ expect(url).toContain("end=");
299
+ });
300
+
301
+ test("View exceptions reports what the exceptions grammar cannot carry", async () => {
302
+ render(
303
+ <MetricCharts
304
+ metricViewData={buildViewData()}
305
+ metricResults={buildResults()}
306
+ metricTypes={[]}
307
+ />,
308
+ );
309
+
310
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
311
+ fireEvent.click(screen.getByText("View exceptions"));
312
+
313
+ await waitFor(() => {
314
+ expect(navigateSpy).toHaveBeenCalled();
315
+ });
316
+
317
+ const url: string = getNavigatedUrl();
318
+ expect(url).toContain("/exceptions");
319
+ expect(url).toContain("status=all");
320
+ // The host.name narrowing has no exceptions facet — the user is told.
321
+ expect(showToastMock).toHaveBeenCalled();
322
+ });
323
+
324
+ test("the host series jumps to the matching Host page", async () => {
325
+ getListMock.mockReturnValue(
326
+ Promise.resolve({ data: [{ _id: HOST_MODEL_ID }] }),
327
+ );
328
+
329
+ render(
330
+ <MetricCharts
331
+ metricViewData={buildViewData()}
332
+ metricResults={buildResults()}
333
+ metricTypes={[]}
334
+ />,
335
+ );
336
+
337
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
338
+ fireEvent.click(screen.getByText('Open host "web-01"'));
339
+
340
+ await waitFor(() => {
341
+ expect(navigateSpy).toHaveBeenCalled();
342
+ });
343
+
344
+ const listArgs: Record<string, unknown> = getListMock.mock
345
+ .calls[0]?.[0] as Record<string, unknown>;
346
+ expect(listArgs["query"]).toEqual({
347
+ projectId: PROJECT_ID,
348
+ hostIdentifier: "web-01",
349
+ });
350
+ expect(getNavigatedUrl()).toContain(`/host/${HOST_MODEL_ID}`);
351
+ });
352
+
353
+ test("enableSeriesActions={false} renders plain chips with no investigate affordance", () => {
354
+ render(
355
+ <MetricCharts
356
+ metricViewData={buildViewData()}
357
+ metricResults={buildResults()}
358
+ metricTypes={[]}
359
+ enableSeriesActions={false}
360
+ />,
361
+ );
362
+
363
+ // The chips themselves still render…
364
+ expect(screen.getByText("host.name=web-01")).toBeInTheDocument();
365
+ // …but nothing offers navigation.
366
+ expect(screen.queryByLabelText("Investigate host.name=web-01")).toBeNull();
367
+ });
368
+ });