@oneuptime/common 12.0.10 → 12.0.12

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 (98) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  6. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  7. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  8. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  9. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  10. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  11. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  12. package/Server/Utils/VM/VMRunner.ts +693 -874
  13. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  14. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  15. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  16. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  17. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  18. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  19. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  20. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  21. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  22. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  23. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  24. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  25. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  26. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  27. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  28. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  29. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  30. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  31. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  32. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  33. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  34. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  35. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  36. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  37. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  38. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  39. package/Types/CustomField/CustomFieldType.ts +28 -0
  40. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  41. package/Types/Monitor/MonitorType.ts +11 -0
  42. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  43. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  44. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  45. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  46. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  47. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  48. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  49. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  50. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  51. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  52. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  53. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  55. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  60. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  61. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  62. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  63. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  64. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  65. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  66. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  67. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  68. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  69. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  70. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  71. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  72. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  73. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  74. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  75. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  76. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  77. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  78. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  79. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  80. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  81. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  82. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  83. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  84. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  85. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  86. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  87. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  88. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  89. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  90. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  91. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  92. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  93. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  94. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  95. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  96. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  97. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  98. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import React from "react";
4
+ import { afterEach, describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
8
+ * the chart renders nothing without a fixed size. Same shim as
9
+ * AxisTickColor.test.tsx — anything that asserts on rendered GEOMETRY needs
10
+ * it, because a 0x0 chart draws no marks either and would make every one of
11
+ * these assertions pass for the wrong reason.
12
+ */
13
+ jest.mock("recharts", () => {
14
+ const actual: Record<string, any> = jest.requireActual("recharts");
15
+ return {
16
+ ...actual,
17
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
18
+ return React.cloneElement(children, { width: 600, height: 300 });
19
+ },
20
+ };
21
+ });
22
+
23
+ import AreaChartElement from "../../../../UI/Components/Charts/Area/AreaChart";
24
+ import BarChartElement from "../../../../UI/Components/Charts/Bar/BarChart";
25
+ import LineChartElement from "../../../../UI/Components/Charts/Line/LineChart";
26
+ import { CHART_DATA_POINT_X_AXIS_KEY } from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
27
+ import ChartCurve from "../../../../UI/Components/Charts/Types/ChartCurve";
28
+ import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
29
+ import SeriesPoint from "../../../../UI/Components/Charts/Types/SeriesPoints";
30
+ import {
31
+ XAxis as ChartXAxis,
32
+ XAxisAggregateType,
33
+ } from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
34
+ import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
35
+ import YAxis, {
36
+ YAxisPrecision,
37
+ } from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
38
+ import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
39
+
40
+ /*
41
+ * The three chart wrappers hand recharts a FIXED x-axis dataKey, while the
42
+ * rows they hand it are built by DataPointUtil. Those two used to agree only
43
+ * by convention: the util keyed each row by the caller's `xAxis.legend`, and
44
+ * every caller but one happened to pass the literal "Time".
45
+ *
46
+ * The one that did not — the SLO dashboard widget, which passed "" — got a
47
+ * chart with axes, grid and tooltip but no line: recharts looked up "Time" on
48
+ * every row, found nothing, and drew no path. Nothing failed, nothing logged;
49
+ * the widget just rendered an empty frame on public and private dashboards
50
+ * alike.
51
+ *
52
+ * These tests hold the axis key on both sides of that seam: whatever legend a
53
+ * caller passes, the marks get drawn.
54
+ */
55
+
56
+ const START: Date = new Date("2026-08-10T00:00:00.000Z");
57
+ const END: Date = new Date("2026-08-10T01:00:00.000Z");
58
+
59
+ /* An hour window buckets at one minute, so these land on distinct buckets. */
60
+ type BuildPointsFunction = () => Array<DataPoint>;
61
+
62
+ const buildPoints: BuildPointsFunction = (): Array<DataPoint> => {
63
+ const points: Array<DataPoint> = [];
64
+
65
+ for (let index: number = 0; index < 10; index++) {
66
+ points.push({
67
+ x: new Date(START.getTime() + index * 60 * 1000),
68
+ y: 90 + index,
69
+ });
70
+ }
71
+
72
+ return points;
73
+ };
74
+
75
+ type BuildXAxisFunction = (legend: string) => ChartXAxis;
76
+
77
+ const buildXAxis: BuildXAxisFunction = (legend: string): ChartXAxis => {
78
+ return {
79
+ legend: legend,
80
+ options: {
81
+ type: XAxisType.Time,
82
+ min: START,
83
+ max: END,
84
+ aggregateType: XAxisAggregateType.Average,
85
+ },
86
+ };
87
+ };
88
+
89
+ const Y_AXIS: YAxis = {
90
+ legend: "%",
91
+ options: {
92
+ type: YAxisType.Number,
93
+ min: "auto",
94
+ max: 100,
95
+ precision: YAxisPrecision.TwoDecimals,
96
+ formatter: (value: number): string => {
97
+ return `${value}%`;
98
+ },
99
+ },
100
+ };
101
+
102
+ const SERIES_NAME: string = "SLI";
103
+
104
+ type BuildSeriesFunction = () => Array<SeriesPoint>;
105
+
106
+ const buildSeries: BuildSeriesFunction = (): Array<SeriesPoint> => {
107
+ return [{ seriesName: SERIES_NAME, data: buildPoints() }];
108
+ };
109
+
110
+ /*
111
+ * Legends worth covering: the canonical one every other caller passes, the
112
+ * empty string the SLO widget passed (the reported bug), and an arbitrary
113
+ * label, which is what a future caller is most likely to reach for.
114
+ */
115
+ const LEGENDS: Array<[string, string]> = [
116
+ ['the canonical "Time"', "Time"],
117
+ ["an empty legend", ""],
118
+ ["an arbitrary legend", "Timestamp"],
119
+ ["a legend with markup-ish characters", "Time (UTC) <>"],
120
+ ];
121
+
122
+ interface ChartUnderTest {
123
+ name: string;
124
+ /** CSS selector for the SVG geometry this chart type draws per series. */
125
+ markSelector: string;
126
+ render: (xAxis: ChartXAxis) => React.ReactElement;
127
+ }
128
+
129
+ const CHARTS: Array<ChartUnderTest> = [
130
+ {
131
+ name: "LineChart",
132
+ markSelector: "path.recharts-line-curve",
133
+ render: (xAxis: ChartXAxis): React.ReactElement => {
134
+ return (
135
+ <LineChartElement
136
+ data={buildSeries()}
137
+ xAxis={xAxis}
138
+ yAxis={Y_AXIS}
139
+ curve={ChartCurve.MONOTONE}
140
+ heightInPx={300}
141
+ showLegend={false}
142
+ sync={false}
143
+ syncid="x-axis-key-test"
144
+ />
145
+ );
146
+ },
147
+ },
148
+ {
149
+ name: "AreaChart",
150
+ markSelector: "path.recharts-area-area",
151
+ render: (xAxis: ChartXAxis): React.ReactElement => {
152
+ return (
153
+ <AreaChartElement
154
+ data={buildSeries()}
155
+ xAxis={xAxis}
156
+ yAxis={Y_AXIS}
157
+ curve={ChartCurve.MONOTONE}
158
+ heightInPx={300}
159
+ showLegend={false}
160
+ sync={false}
161
+ syncid="x-axis-key-test"
162
+ />
163
+ );
164
+ },
165
+ },
166
+ {
167
+ /*
168
+ * Bars are drawn through a custom `shape` renderer, so the mark is a bare
169
+ * <path> under recharts' own rectangle group rather than a classed one.
170
+ */
171
+ name: "BarChart",
172
+ markSelector: "g.recharts-bar-rectangle path",
173
+ render: (xAxis: ChartXAxis): React.ReactElement => {
174
+ return (
175
+ <BarChartElement
176
+ data={buildSeries()}
177
+ xAxis={xAxis}
178
+ yAxis={Y_AXIS}
179
+ heightInPx={300}
180
+ showLegend={false}
181
+ sync={false}
182
+ syncid="x-axis-key-test"
183
+ />
184
+ );
185
+ },
186
+ },
187
+ ];
188
+
189
+ afterEach((): void => {
190
+ cleanup();
191
+ });
192
+
193
+ describe("Chart x-axis data key", () => {
194
+ test("the canonical key is the literal recharts reads", (): void => {
195
+ /*
196
+ * Pinned, not derived. The wrappers and DataPointUtil both import this
197
+ * constant, so a rename would keep them agreeing with each other while
198
+ * silently changing the shape handed to recharts.
199
+ */
200
+ expect(CHART_DATA_POINT_X_AXIS_KEY).toBe("Time");
201
+ });
202
+
203
+ for (const chart of CHARTS) {
204
+ describe(chart.name, () => {
205
+ test.each(LEGENDS)(
206
+ `draws its series with %s`,
207
+ (_label: string, legend: string): void => {
208
+ const { container } = render(chart.render(buildXAxis(legend)));
209
+
210
+ const marks: Array<Element> = Array.from(
211
+ container.querySelectorAll(chart.markSelector),
212
+ );
213
+
214
+ expect(marks.length).toBeGreaterThan(0);
215
+
216
+ /*
217
+ * Present-but-empty geometry is the exact failure mode here: a
218
+ * <path> with no `d` renders nothing at all, so the element count
219
+ * alone would not have caught the bug.
220
+ */
221
+ for (const mark of marks) {
222
+ expect(mark.getAttribute("d")).toBeTruthy();
223
+ }
224
+ },
225
+ );
226
+
227
+ test("draws the same geometry whatever the legend says", (): void => {
228
+ const canonical: RenderResultContainer = render(
229
+ chart.render(buildXAxis("Time")),
230
+ );
231
+ const canonicalPaths: Array<string | null> = Array.from(
232
+ canonical.container.querySelectorAll(chart.markSelector),
233
+ ).map((element: Element) => {
234
+ return element.getAttribute("d");
235
+ });
236
+
237
+ cleanup();
238
+
239
+ const other: RenderResultContainer = render(
240
+ chart.render(buildXAxis("")),
241
+ );
242
+ const otherPaths: Array<string | null> = Array.from(
243
+ other.container.querySelectorAll(chart.markSelector),
244
+ ).map((element: Element) => {
245
+ return element.getAttribute("d");
246
+ });
247
+
248
+ expect(otherPaths).toEqual(canonicalPaths);
249
+ });
250
+ });
251
+ }
252
+ });
253
+
254
+ interface RenderResultContainer {
255
+ container: HTMLElement;
256
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @timezone UTC
3
+ */
4
+ import { describe, expect, test } from "@jest/globals";
5
+
6
+ import ChartDataPoint, {
7
+ CHART_DATA_POINT_DATE_KEY,
8
+ CHART_DATA_POINT_X_AXIS_KEY,
9
+ } from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
10
+ import DataPointUtil from "../../../../UI/Components/Charts/Utils/DataPoint";
11
+ import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
12
+ import SeriesPoints from "../../../../UI/Components/Charts/Types/SeriesPoints";
13
+ import {
14
+ XAxis as ChartXAxis,
15
+ XAxisAggregateType,
16
+ } from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
17
+ import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
18
+ import YAxis, {
19
+ YAxisPrecision,
20
+ } from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
21
+ import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
22
+
23
+ /*
24
+ * DataPointUtil produces the rows recharts is handed directly, so the KEY it
25
+ * writes them under is a contract, not an implementation detail: the chart
26
+ * wrappers look up CHART_DATA_POINT_X_AXIS_KEY on every row and draw nothing
27
+ * at all when it is absent.
28
+ *
29
+ * That key used to be whatever the caller put in `xAxis.legend`, which made
30
+ * the contract invisible — and the one caller that passed something else (the
31
+ * SLO dashboard widget, with "") rendered an empty chart. These tests pin the
32
+ * key, and pin that the legend can no longer influence it.
33
+ *
34
+ * Pinned to UTC via the docblock pragma above: the bucket labels come from
35
+ * local-time formatters, so an unpinned run would assert against the
36
+ * machine's zone.
37
+ */
38
+
39
+ const START: Date = new Date("2026-08-10T00:00:00.000Z");
40
+ const END: Date = new Date("2026-08-10T01:00:00.000Z");
41
+
42
+ /** A 1-hour window buckets EVERY_MINUTE, inclusive of both ends. */
43
+ const EXPECTED_BUCKET_COUNT: number = 61;
44
+
45
+ const Y_AXIS: YAxis = {
46
+ legend: "%",
47
+ options: {
48
+ type: YAxisType.Number,
49
+ min: "auto",
50
+ max: 100,
51
+ precision: YAxisPrecision.TwoDecimals,
52
+ formatter: (value: number): string => {
53
+ return `${value}%`;
54
+ },
55
+ },
56
+ };
57
+
58
+ type BuildXAxisFunction = (data?: {
59
+ legend?: string | undefined;
60
+ aggregateType?: XAxisAggregateType | undefined;
61
+ }) => ChartXAxis;
62
+
63
+ const buildXAxis: BuildXAxisFunction = (
64
+ data: {
65
+ legend?: string | undefined;
66
+ aggregateType?: XAxisAggregateType | undefined;
67
+ } = {},
68
+ ): ChartXAxis => {
69
+ return {
70
+ legend: data.legend ?? "Time",
71
+ options: {
72
+ type: XAxisType.Time,
73
+ min: START,
74
+ max: END,
75
+ aggregateType: data.aggregateType ?? XAxisAggregateType.Average,
76
+ },
77
+ };
78
+ };
79
+
80
+ type MinutesAfterStartFunction = (minutes: number) => Date;
81
+
82
+ const minutesAfterStart: MinutesAfterStartFunction = (
83
+ minutes: number,
84
+ ): Date => {
85
+ return new Date(START.getTime() + minutes * 60 * 1000);
86
+ };
87
+
88
+ type GetPointsFunction = (data: {
89
+ seriesPoints: Array<SeriesPoints>;
90
+ legend?: string | undefined;
91
+ aggregateType?: XAxisAggregateType | undefined;
92
+ }) => Array<ChartDataPoint>;
93
+
94
+ const getPoints: GetPointsFunction = (data: {
95
+ seriesPoints: Array<SeriesPoints>;
96
+ legend?: string | undefined;
97
+ aggregateType?: XAxisAggregateType | undefined;
98
+ }): Array<ChartDataPoint> => {
99
+ return DataPointUtil.getChartDataPoints({
100
+ seriesPoints: data.seriesPoints,
101
+ xAxis: buildXAxis({
102
+ legend: data.legend,
103
+ aggregateType: data.aggregateType,
104
+ }),
105
+ yAxis: Y_AXIS,
106
+ });
107
+ };
108
+
109
+ describe("DataPointUtil.getChartDataPoints", () => {
110
+ describe("the x-axis row key", () => {
111
+ test("writes the canonical key on every bucket", (): void => {
112
+ const rows: Array<ChartDataPoint> = getPoints({ seriesPoints: [] });
113
+
114
+ expect(rows.length).toBe(EXPECTED_BUCKET_COUNT);
115
+
116
+ for (const row of rows) {
117
+ expect(typeof row[CHART_DATA_POINT_X_AXIS_KEY]).toBe("string");
118
+ expect(row[CHART_DATA_POINT_X_AXIS_KEY]).not.toBe("");
119
+ }
120
+ });
121
+
122
+ test.each([
123
+ ["an empty legend", ""],
124
+ ["an arbitrary legend", "Timestamp"],
125
+ ["the canonical legend", "Time"],
126
+ ])(
127
+ "keys rows the same way with %s",
128
+ (_label: string, legend: string): void => {
129
+ const rows: Array<ChartDataPoint> = getPoints({
130
+ seriesPoints: [],
131
+ legend: legend,
132
+ });
133
+
134
+ for (const row of rows) {
135
+ expect(row[CHART_DATA_POINT_X_AXIS_KEY]).toBeDefined();
136
+ }
137
+ },
138
+ );
139
+
140
+ test("never leaks the legend into the row as a second key", (): void => {
141
+ const rows: Array<ChartDataPoint> = getPoints({
142
+ seriesPoints: [],
143
+ legend: "Timestamp",
144
+ });
145
+
146
+ for (const row of rows) {
147
+ expect(Object.keys(row).sort()).toEqual(
148
+ [CHART_DATA_POINT_X_AXIS_KEY, CHART_DATA_POINT_DATE_KEY].sort(),
149
+ );
150
+ }
151
+ });
152
+
153
+ test("produces identical rows whatever the legend says", (): void => {
154
+ const series: Array<SeriesPoints> = [
155
+ {
156
+ seriesName: "SLI",
157
+ data: [
158
+ { x: minutesAfterStart(0), y: 99.1 },
159
+ { x: minutesAfterStart(30), y: 99.4 },
160
+ ],
161
+ },
162
+ ];
163
+
164
+ const canonical: Array<ChartDataPoint> = getPoints({
165
+ seriesPoints: series,
166
+ legend: "Time",
167
+ });
168
+ const empty: Array<ChartDataPoint> = getPoints({
169
+ seriesPoints: series,
170
+ legend: "",
171
+ });
172
+
173
+ expect(empty).toEqual(canonical);
174
+ });
175
+ });
176
+
177
+ describe("the hidden raw-date field", () => {
178
+ test("carries each bucket start as epoch ms", (): void => {
179
+ const rows: Array<ChartDataPoint> = getPoints({ seriesPoints: [] });
180
+
181
+ expect(rows[0]![CHART_DATA_POINT_DATE_KEY]).toBe(START.getTime());
182
+ expect(rows[rows.length - 1]![CHART_DATA_POINT_DATE_KEY]).toBe(
183
+ END.getTime(),
184
+ );
185
+ });
186
+
187
+ test("is underscore-prefixed so it can never collide with a series", (): void => {
188
+ expect(CHART_DATA_POINT_DATE_KEY.startsWith("__")).toBe(true);
189
+ expect(CHART_DATA_POINT_DATE_KEY).not.toBe(CHART_DATA_POINT_X_AXIS_KEY);
190
+ });
191
+ });
192
+
193
+ describe("placing series values into buckets", () => {
194
+ test("lands a point on the bucket its timestamp falls in", (): void => {
195
+ const rows: Array<ChartDataPoint> = getPoints({
196
+ seriesPoints: [
197
+ {
198
+ seriesName: "SLI",
199
+ data: [
200
+ { x: minutesAfterStart(0), y: 10 },
201
+ { x: minutesAfterStart(5), y: 20 },
202
+ { x: minutesAfterStart(60), y: 30 },
203
+ ],
204
+ },
205
+ ],
206
+ });
207
+
208
+ expect(rows[0]!["SLI"]).toBe(10);
209
+ expect(rows[5]!["SLI"]).toBe(20);
210
+ expect(rows[60]!["SLI"]).toBe(30);
211
+ // Buckets with no datapoint stay absent rather than becoming 0.
212
+ expect(rows[1]!["SLI"]).toBeUndefined();
213
+ });
214
+
215
+ test("drops points outside the charted window", (): void => {
216
+ const rows: Array<ChartDataPoint> = getPoints({
217
+ seriesPoints: [
218
+ {
219
+ seriesName: "SLI",
220
+ data: [
221
+ { x: minutesAfterStart(-30), y: 1 },
222
+ { x: minutesAfterStart(120), y: 2 },
223
+ { x: minutesAfterStart(10), y: 3 },
224
+ ],
225
+ },
226
+ ],
227
+ });
228
+
229
+ const withValues: Array<ChartDataPoint> = rows.filter(
230
+ (row: ChartDataPoint) => {
231
+ return row["SLI"] !== undefined;
232
+ },
233
+ );
234
+
235
+ expect(withValues.length).toBe(1);
236
+ expect(rows[10]!["SLI"]).toBe(3);
237
+ });
238
+
239
+ test("keeps several series side by side on one row", (): void => {
240
+ const rows: Array<ChartDataPoint> = getPoints({
241
+ seriesPoints: [
242
+ {
243
+ seriesName: "SLI",
244
+ data: [{ x: minutesAfterStart(3), y: 99 }],
245
+ },
246
+ {
247
+ seriesName: "Burn Rate",
248
+ data: [{ x: minutesAfterStart(3), y: 1.5 }],
249
+ },
250
+ ],
251
+ });
252
+
253
+ expect(rows[3]!["SLI"]).toBe(99);
254
+ expect(rows[3]!["Burn Rate"]).toBe(1.5);
255
+ });
256
+ });
257
+
258
+ describe("rolling up several points into one bucket", () => {
259
+ const sameBucket: Array<SeriesPoints> = [
260
+ {
261
+ seriesName: "SLI",
262
+ data: [
263
+ { x: new Date(START.getTime() + 1000), y: 10 },
264
+ { x: new Date(START.getTime() + 2000), y: 20 },
265
+ { x: new Date(START.getTime() + 3000), y: 60 },
266
+ ],
267
+ },
268
+ ];
269
+
270
+ test.each([
271
+ [XAxisAggregateType.Average, 30],
272
+ [XAxisAggregateType.Sum, 90],
273
+ [XAxisAggregateType.Max, 60],
274
+ [XAxisAggregateType.Min, 10],
275
+ ])("%s", (aggregateType: XAxisAggregateType, expected: number): void => {
276
+ const rows: Array<ChartDataPoint> = getPoints({
277
+ seriesPoints: sameBucket,
278
+ aggregateType: aggregateType,
279
+ });
280
+
281
+ expect(rows[0]!["SLI"]).toBe(expected);
282
+ });
283
+
284
+ /*
285
+ * The accumulator is keyed by (series, bucket). A shared accumulator once
286
+ * carried each bucket's running total into the next, turning an
287
+ * "Incidents Over Time" Sum chart into a reverse-cumulative staircase.
288
+ */
289
+ test("resets its accumulator at every bucket boundary", (): void => {
290
+ const rows: Array<ChartDataPoint> = getPoints({
291
+ aggregateType: XAxisAggregateType.Sum,
292
+ seriesPoints: [
293
+ {
294
+ seriesName: "Incidents",
295
+ data: [
296
+ { x: minutesAfterStart(0), y: 41 },
297
+ { x: minutesAfterStart(1), y: 91 },
298
+ { x: minutesAfterStart(2), y: 356 },
299
+ ],
300
+ },
301
+ ],
302
+ });
303
+
304
+ expect(rows[0]!["Incidents"]).toBe(41);
305
+ expect(rows[1]!["Incidents"]).toBe(91);
306
+ expect(rows[2]!["Incidents"]).toBe(356);
307
+ });
308
+ });
309
+
310
+ describe("empty input", () => {
311
+ test("still returns the full bucket grid so the axis can draw", (): void => {
312
+ const rows: Array<ChartDataPoint> = getPoints({
313
+ seriesPoints: [{ seriesName: "SLI", data: [] as Array<DataPoint> }],
314
+ });
315
+
316
+ expect(rows.length).toBe(EXPECTED_BUCKET_COUNT);
317
+
318
+ for (const row of rows) {
319
+ expect(row["SLI"]).toBeUndefined();
320
+ }
321
+ });
322
+ });
323
+ });