@oneuptime/common 12.0.17 → 12.0.18

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.
@@ -7,6 +7,7 @@ import {
7
7
  buildRightSizingRecommendation,
8
8
  formatCpuCores,
9
9
  formatMemoryBytes,
10
+ getVerdictLabel,
10
11
  hasActionableRecommendation,
11
12
  } from "../../../Types/Kubernetes/KubernetesRightSizing";
12
13
  import { describe, expect, test } from "@jest/globals";
@@ -321,3 +322,55 @@ describe("formatting", () => {
321
322
  expect(formatMemoryBytes(null)).toBe("-");
322
323
  });
323
324
  });
325
+
326
+ /*
327
+ * getVerdictLabel turns the internal verdict enum into the human string the
328
+ * right-sizing UI renders. It is a switch with a default arm, so the failure it
329
+ * invites is a new verdict added to the enum without a matching case — it would
330
+ * silently render as "Unavailable". This block pins each label and, crucially,
331
+ * that every enum member has an explicit, distinct, non-"Unavailable" label
332
+ * except the one that is meant to be Unavailable.
333
+ */
334
+ describe("getVerdictLabel", () => {
335
+ const EXPECTED: Array<[RightSizingVerdict, string]> = [
336
+ [RightSizingVerdict.Overprovisioned, "Over-provisioned"],
337
+ [RightSizingVerdict.Underprovisioned, "Under-provisioned"],
338
+ [RightSizingVerdict.Optimal, "Right-sized"],
339
+ [RightSizingVerdict.NoRequestSet, "No request set"],
340
+ [RightSizingVerdict.Unavailable, "Unavailable"],
341
+ ];
342
+
343
+ test.each(EXPECTED)(
344
+ "maps %s to its label",
345
+ (verdict: RightSizingVerdict, label: string) => {
346
+ expect(getVerdictLabel(verdict)).toBe(label);
347
+ },
348
+ );
349
+
350
+ test("every verdict produces a non-empty label", () => {
351
+ for (const verdict of Object.values(RightSizingVerdict)) {
352
+ const label: string = getVerdictLabel(verdict);
353
+ expect(typeof label).toBe("string");
354
+ expect(label.length).toBeGreaterThan(0);
355
+ }
356
+ });
357
+
358
+ test('only the Unavailable verdict resolves to "Unavailable"', () => {
359
+ /*
360
+ * If a new verdict fell through to the default arm, more than one enum
361
+ * member would carry the "Unavailable" label — catch that here.
362
+ */
363
+ const unavailableCount: number = Object.values(RightSizingVerdict).filter(
364
+ (verdict: RightSizingVerdict) => {
365
+ return getVerdictLabel(verdict) === "Unavailable";
366
+ },
367
+ ).length;
368
+ expect(unavailableCount).toBe(1);
369
+ });
370
+
371
+ test("an unknown verdict falls back to Unavailable", () => {
372
+ expect(getVerdictLabel("SomethingElse" as RightSizingVerdict)).toBe(
373
+ "Unavailable",
374
+ );
375
+ });
376
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import AggregationType from "../../../Types/BaseDatabase/AggregationType";
3
+ import MeasurementAggregationType, {
4
+ MeasurementAggregationTypeUtil,
5
+ } from "../../../Types/Measurement/MeasurementAggregationType";
6
+
7
+ /*
8
+ * MeasurementAggregationType is the deliberately-narrow subset of
9
+ * AggregationType a measurement chart is allowed to default to (Sum is left
10
+ * out on purpose: summing durations across incidents is meaningless). The Util
11
+ * maps each subset member onto the full AggregationType the query layer
12
+ * understands. The mapping is a switch with a default arm, so the failure it
13
+ * invites is a member added to the enum without a matching case — it would
14
+ * silently fall through to Avg. This suite pins the mapping and that
15
+ * exhaustiveness.
16
+ */
17
+
18
+ describe("MeasurementAggregationType", () => {
19
+ test("is a strict subset of AggregationType and excludes Sum/Count", () => {
20
+ const full: Array<string> = Object.values(AggregationType);
21
+ for (const member of Object.values(MeasurementAggregationType)) {
22
+ // Every measurement member must be a real AggregationType value.
23
+ expect(full).toContain(member);
24
+ }
25
+ // The two intentionally-omitted members must never appear here.
26
+ expect(Object.values(MeasurementAggregationType)).not.toContain(
27
+ AggregationType.Sum as unknown as MeasurementAggregationType,
28
+ );
29
+ expect(Object.values(MeasurementAggregationType)).not.toContain(
30
+ AggregationType.Count as unknown as MeasurementAggregationType,
31
+ );
32
+ });
33
+ });
34
+
35
+ describe("MeasurementAggregationTypeUtil.toAggregationType", () => {
36
+ const CASES: Array<[MeasurementAggregationType, AggregationType]> = [
37
+ [MeasurementAggregationType.Avg, AggregationType.Avg],
38
+ [MeasurementAggregationType.Max, AggregationType.Max],
39
+ [MeasurementAggregationType.Min, AggregationType.Min],
40
+ [MeasurementAggregationType.P50, AggregationType.P50],
41
+ [MeasurementAggregationType.P90, AggregationType.P90],
42
+ [MeasurementAggregationType.P95, AggregationType.P95],
43
+ [MeasurementAggregationType.P99, AggregationType.P99],
44
+ ];
45
+
46
+ test.each(CASES)(
47
+ "maps %s to the matching AggregationType",
48
+ (input: MeasurementAggregationType, expected: AggregationType) => {
49
+ expect(MeasurementAggregationTypeUtil.toAggregationType(input)).toBe(
50
+ expected,
51
+ );
52
+ },
53
+ );
54
+
55
+ test("covers every enum member (no member falls through to the default)", () => {
56
+ /*
57
+ * If a new member is added to MeasurementAggregationType without its own
58
+ * case, it would resolve to Avg here. Compare against an independently
59
+ * built expectation so that fall-through is caught rather than accepted.
60
+ */
61
+ const mapped: Set<AggregationType> = new Set<AggregationType>(
62
+ Object.values(MeasurementAggregationType).map(
63
+ (m: MeasurementAggregationType) => {
64
+ return MeasurementAggregationTypeUtil.toAggregationType(m);
65
+ },
66
+ ),
67
+ );
68
+ // Avg, Max, Min, P50, P90, P95, P99 => 7 distinct targets.
69
+ expect(mapped.size).toBe(Object.values(MeasurementAggregationType).length);
70
+ });
71
+
72
+ test("defaults undefined to Avg", () => {
73
+ // The picker can be empty; an unset selection must resolve to a safe Avg.
74
+ expect(MeasurementAggregationTypeUtil.toAggregationType(undefined)).toBe(
75
+ AggregationType.Avg,
76
+ );
77
+ });
78
+
79
+ test("an unknown value falls back to Avg rather than throwing", () => {
80
+ // Defensive: a value outside the enum (bad persisted data) must not crash.
81
+ expect(
82
+ MeasurementAggregationTypeUtil.toAggregationType(
83
+ "NotARealAggregation" as MeasurementAggregationType,
84
+ ),
85
+ ).toBe(AggregationType.Avg);
86
+ });
87
+
88
+ test("only ever returns AggregationTypes valid for measurements", () => {
89
+ /*
90
+ * The mapping must never emit Sum or Count — those are exactly the
91
+ * aggregations the measurement subset exists to keep out.
92
+ */
93
+ for (const member of Object.values(MeasurementAggregationType)) {
94
+ const result: AggregationType =
95
+ MeasurementAggregationTypeUtil.toAggregationType(member);
96
+ expect(result).not.toBe(AggregationType.Sum);
97
+ expect(result).not.toBe(AggregationType.Count);
98
+ }
99
+ });
100
+ });
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS,
3
+ DEFAULT_MONITOR_RETRY_COUNT,
3
4
  MAX_MONITOR_REQUEST_TIMEOUT_IN_MS,
5
+ MAX_MONITOR_RETRY_COUNT,
4
6
  clampMonitorRequestTimeoutInMs,
7
+ clampMonitorRetryCount,
5
8
  } from "../../../Types/Monitor/MonitorStep";
6
9
  import { describe, expect, test } from "@jest/globals";
7
10
 
@@ -163,3 +166,114 @@ describe("clampMonitorRequestTimeoutInMs", () => {
163
166
  });
164
167
  });
165
168
  });
169
+
170
+ /*
171
+ * clampMonitorRetryCount constrains how many times a probe retries a step. Its
172
+ * guard differs from the timeout clamp in one behaviour that matters: the lower
173
+ * bound is `value < 0`, NOT `value <= 0`. Zero is therefore a VALID retry count
174
+ * (retry never) and must pass through unchanged, whereas only genuinely invalid
175
+ * inputs (undefined / null / NaN / negative) fall back to the default. These
176
+ * tests pin exactly that zero-is-valid distinction plus the usual boundary and
177
+ * defensive-coercion cases.
178
+ */
179
+ describe("clampMonitorRetryCount", () => {
180
+ describe("invalid inputs fall back to the default", () => {
181
+ test("NaN returns the default", () => {
182
+ expect(clampMonitorRetryCount(NaN)).toBe(DEFAULT_MONITOR_RETRY_COUNT);
183
+ });
184
+
185
+ test("a negative value returns the default (not a clamp to zero)", () => {
186
+ expect(clampMonitorRetryCount(-1)).toBe(DEFAULT_MONITOR_RETRY_COUNT);
187
+ });
188
+
189
+ test("negative Infinity returns the default", () => {
190
+ expect(clampMonitorRetryCount(-Infinity)).toBe(
191
+ DEFAULT_MONITOR_RETRY_COUNT,
192
+ );
193
+ });
194
+
195
+ test("undefined coerced through the signature returns the default", () => {
196
+ expect(clampMonitorRetryCount(undefined as unknown as number)).toBe(
197
+ DEFAULT_MONITOR_RETRY_COUNT,
198
+ );
199
+ });
200
+
201
+ test("null coerced through the signature returns the default", () => {
202
+ expect(clampMonitorRetryCount(null as unknown as number)).toBe(
203
+ DEFAULT_MONITOR_RETRY_COUNT,
204
+ );
205
+ });
206
+ });
207
+
208
+ describe("zero is a valid retry count", () => {
209
+ test("zero passes through unchanged (retry never)", () => {
210
+ // The whole point of `< 0` rather than `<= 0`: 0 is a real choice.
211
+ expect(clampMonitorRetryCount(0)).toBe(0);
212
+ });
213
+
214
+ test("negative zero also passes through as a numeric zero", () => {
215
+ /*
216
+ * -0 < 0 is false, so -0 is not treated as invalid and passes through.
217
+ * It stays -0 (toBe uses Object.is, under which -0 !== 0), which is still
218
+ * numerically zero — assert that rather than the Object.is identity.
219
+ */
220
+ const result: number = clampMonitorRetryCount(-0);
221
+ expect(result === 0).toBe(true);
222
+ expect(Math.abs(result)).toBe(0);
223
+ });
224
+ });
225
+
226
+ describe("in-range and boundary values", () => {
227
+ test("a value between 0 and the cap is returned unchanged", () => {
228
+ expect(clampMonitorRetryCount(1)).toBe(1);
229
+ expect(clampMonitorRetryCount(2)).toBe(2);
230
+ });
231
+
232
+ test("a value exactly at the cap is returned, not clamped", () => {
233
+ expect(clampMonitorRetryCount(MAX_MONITOR_RETRY_COUNT)).toBe(
234
+ MAX_MONITOR_RETRY_COUNT,
235
+ );
236
+ });
237
+
238
+ test("one over the cap is clamped down to the cap", () => {
239
+ expect(clampMonitorRetryCount(MAX_MONITOR_RETRY_COUNT + 1)).toBe(
240
+ MAX_MONITOR_RETRY_COUNT,
241
+ );
242
+ });
243
+
244
+ test("positive Infinity is clamped to the cap", () => {
245
+ expect(clampMonitorRetryCount(Infinity)).toBe(MAX_MONITOR_RETRY_COUNT);
246
+ });
247
+ });
248
+
249
+ describe("stability and invariants", () => {
250
+ test("the exported cap and default are non-negative finite numbers", () => {
251
+ expect(Number.isFinite(MAX_MONITOR_RETRY_COUNT)).toBe(true);
252
+ expect(MAX_MONITOR_RETRY_COUNT).toBeGreaterThanOrEqual(0);
253
+ expect(DEFAULT_MONITOR_RETRY_COUNT).toBeGreaterThanOrEqual(0);
254
+ expect(DEFAULT_MONITOR_RETRY_COUNT).toBeLessThanOrEqual(
255
+ MAX_MONITOR_RETRY_COUNT,
256
+ );
257
+ });
258
+
259
+ test("across a sweep the result is always within [0, cap]", () => {
260
+ const samples: Array<number> = [
261
+ -100,
262
+ -1,
263
+ 0,
264
+ 1,
265
+ 2,
266
+ 3,
267
+ 4,
268
+ 1000,
269
+ Infinity,
270
+ NaN,
271
+ ];
272
+ for (const sample of samples) {
273
+ const result: number = clampMonitorRetryCount(sample);
274
+ expect(result).toBeGreaterThanOrEqual(0);
275
+ expect(result).toBeLessThanOrEqual(MAX_MONITOR_RETRY_COUNT);
276
+ }
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import WorkspaceType, {
3
+ getWorkspaceTypeDisplayName,
4
+ } from "../../../Types/Workspace/WorkspaceType";
5
+
6
+ /*
7
+ * getWorkspaceTypeDisplayName turns the WorkspaceType enum into the label shown
8
+ * wherever a workspace integration is named in the UI. The only member whose
9
+ * display name differs from its enum value is MicrosoftTeams ("Microsoft Teams"
10
+ * with a space); the rest fall through to returning the raw value. The suite
11
+ * pins that spacing and guards the fall-through: a new workspace type added to
12
+ * the enum must still yield a non-empty label rather than undefined.
13
+ */
14
+
15
+ describe("getWorkspaceTypeDisplayName", () => {
16
+ test("renders Microsoft Teams with a space", () => {
17
+ // The enum value is "MicrosoftTeams"; the label must be human-spaced.
18
+ expect(getWorkspaceTypeDisplayName(WorkspaceType.MicrosoftTeams)).toBe(
19
+ "Microsoft Teams",
20
+ );
21
+ });
22
+
23
+ test("renders Slack as-is", () => {
24
+ expect(getWorkspaceTypeDisplayName(WorkspaceType.Slack)).toBe("Slack");
25
+ });
26
+
27
+ test("every workspace type yields a non-empty display name", () => {
28
+ for (const type of Object.values(WorkspaceType)) {
29
+ const label: string = getWorkspaceTypeDisplayName(type);
30
+ expect(typeof label).toBe("string");
31
+ expect(label.length).toBeGreaterThan(0);
32
+ }
33
+ });
34
+
35
+ test("display names are unique across workspace types", () => {
36
+ const labels: Array<string> = Object.values(WorkspaceType).map(
37
+ (type: WorkspaceType) => {
38
+ return getWorkspaceTypeDisplayName(type);
39
+ },
40
+ );
41
+ expect(new Set<string>(labels).size).toBe(labels.length);
42
+ });
43
+
44
+ test("an unknown value falls back to the raw string it was given", () => {
45
+ /*
46
+ * The function's default arm returns its argument unchanged. Persisted or
47
+ * mistyped data must therefore round-trip rather than become undefined.
48
+ */
49
+ expect(getWorkspaceTypeDisplayName("Discord" as WorkspaceType)).toBe(
50
+ "Discord",
51
+ );
52
+ });
53
+ });
@@ -0,0 +1,257 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import DashboardComponentType from "../../../../Types/Dashboard/DashboardComponentType";
3
+ import DashboardBaseComponent from "../../../../Types/Dashboard/DashboardComponents/DashboardBaseComponent";
4
+ import ObjectID from "../../../../Types/ObjectID";
5
+ import { ObjectType } from "../../../../Types/JSON";
6
+ import DashboardAlertListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardAlertListComponent";
7
+ import DashboardCephOsdListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardCephOsdListComponent";
8
+ import DashboardCephPoolListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardCephPoolListComponent";
9
+ import DashboardChartComponentUtil from "../../../../Utils/Dashboard/Components/DashboardChartComponent";
10
+ import DashboardClockComponentUtil from "../../../../Utils/Dashboard/Components/DashboardClockComponent";
11
+ import DashboardDataSourceChartComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDataSourceChartComponent";
12
+ import DashboardDataSourceGaugeComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDataSourceGaugeComponent";
13
+ import DashboardDataSourceTableComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDataSourceTableComponent";
14
+ import DashboardDataSourceValueComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDataSourceValueComponent";
15
+ import DashboardDockerContainerListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerContainerListComponent";
16
+ import DashboardDockerHostListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerHostListComponent";
17
+ import DashboardDockerImageListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerImageListComponent";
18
+ import DashboardDockerNetworkListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerNetworkListComponent";
19
+ import DashboardDockerSwarmNodeListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerSwarmNodeListComponent";
20
+ import DashboardDockerSwarmServiceListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerSwarmServiceListComponent";
21
+ import DashboardDockerVolumeListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardDockerVolumeListComponent";
22
+ import DashboardGaugeComponentUtil from "../../../../Utils/Dashboard/Components/DashboardGaugeComponent";
23
+ import DashboardHostListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardHostListComponent";
24
+ import DashboardHtmlComponentUtil from "../../../../Utils/Dashboard/Components/DashboardHtmlComponent";
25
+ import DashboardIncidentListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardIncidentListComponent";
26
+ import DashboardKubernetesCronJobListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesCronJobListComponent";
27
+ import DashboardKubernetesDaemonSetListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesDaemonSetListComponent";
28
+ import DashboardKubernetesDeploymentListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesDeploymentListComponent";
29
+ import DashboardKubernetesJobListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesJobListComponent";
30
+ import DashboardKubernetesNamespaceListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesNamespaceListComponent";
31
+ import DashboardKubernetesNodeListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesNodeListComponent";
32
+ import DashboardKubernetesPodListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesPodListComponent";
33
+ import DashboardKubernetesStatefulSetListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardKubernetesStatefulSetListComponent";
34
+ import DashboardLogChartComponentUtil from "../../../../Utils/Dashboard/Components/DashboardLogChartComponent";
35
+ import DashboardLogStreamComponentUtil from "../../../../Utils/Dashboard/Components/DashboardLogStreamComponent";
36
+ import DashboardMonitorListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardMonitorListComponent";
37
+ import DashboardNetworkMapComponentUtil from "../../../../Utils/Dashboard/Components/DashboardNetworkMapComponent";
38
+ import DashboardPodmanContainerListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardPodmanContainerListComponent";
39
+ import DashboardPodmanHostListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardPodmanHostListComponent";
40
+ import DashboardPodmanImageListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardPodmanImageListComponent";
41
+ import DashboardPodmanNetworkListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardPodmanNetworkListComponent";
42
+ import DashboardPodmanVolumeListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardPodmanVolumeListComponent";
43
+ import DashboardProxmoxGuestListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardProxmoxGuestListComponent";
44
+ import DashboardProxmoxNodeListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardProxmoxNodeListComponent";
45
+ import DashboardSecurityEventsFlowComponentUtil from "../../../../Utils/Dashboard/Components/DashboardSecurityEventsFlowComponent";
46
+ import DashboardSecurityEventsListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardSecurityEventsListComponent";
47
+ import DashboardSloComponentUtil from "../../../../Utils/Dashboard/Components/DashboardSloComponent";
48
+ import DashboardTableComponentUtil from "../../../../Utils/Dashboard/Components/DashboardTableComponent";
49
+ import DashboardTextComponentUtil from "../../../../Utils/Dashboard/Components/DashboardTextComponent";
50
+ import DashboardTraceChartComponentUtil from "../../../../Utils/Dashboard/Components/DashboardTraceChartComponent";
51
+ import DashboardTraceListComponentUtil from "../../../../Utils/Dashboard/Components/DashboardTraceListComponent";
52
+ import DashboardTraceTableComponentUtil from "../../../../Utils/Dashboard/Components/DashboardTraceTableComponent";
53
+ import DashboardValueComponentUtil from "../../../../Utils/Dashboard/Components/DashboardValueComponent";
54
+ import DashboardBaseComponentUtil from "../../../../Utils/Dashboard/Components/DashboardBaseComponent";
55
+
56
+ /*
57
+ * Every dashboard widget type ships a getDefaultComponent() that returns the
58
+ * seed component the editor drops onto the canvas when a user first adds that
59
+ * widget. Unlike getComponentSettingsArguments (dispatched through Index.ts and
60
+ * already covered by DashboardComponentsUtil.test.ts), these defaults have no
61
+ * central dispatcher — each util overrides the method independently. That makes
62
+ * them easy to get subtly wrong in ways the compiler cannot catch:
63
+ *
64
+ * - copy-pasting a util and forgetting to change the componentType field, so
65
+ * two widget types seed the same type and the editor renders the wrong one;
66
+ * - a min dimension larger than the default dimension, so the widget is born
67
+ * smaller than its own minimum and the resize logic clamps it on drop;
68
+ * - a zero/negative width or height, so the widget is invisible on the grid;
69
+ * - a shared componentId constant instead of a freshly generated ObjectID, so
70
+ * two widgets on the same dashboard collide on id.
71
+ *
72
+ * This suite pins those invariants for all widget types at once. The TYPE_TO_UTIL
73
+ * map below must stay in lockstep with the enum; the exhaustiveness test fails
74
+ * loudly if a new widget type is added without a default to guard here.
75
+ */
76
+
77
+ type ComponentUtil = {
78
+ getDefaultComponent: () => DashboardBaseComponent;
79
+ };
80
+
81
+ const TYPE_TO_UTIL: Record<DashboardComponentType, ComponentUtil> = {
82
+ [DashboardComponentType.Chart]: DashboardChartComponentUtil,
83
+ [DashboardComponentType.Text]: DashboardTextComponentUtil,
84
+ [DashboardComponentType.Clock]: DashboardClockComponentUtil,
85
+ [DashboardComponentType.Value]: DashboardValueComponentUtil,
86
+ [DashboardComponentType.Table]: DashboardTableComponentUtil,
87
+ [DashboardComponentType.Gauge]: DashboardGaugeComponentUtil,
88
+ [DashboardComponentType.DataSourceChart]:
89
+ DashboardDataSourceChartComponentUtil,
90
+ [DashboardComponentType.DataSourceValue]:
91
+ DashboardDataSourceValueComponentUtil,
92
+ [DashboardComponentType.DataSourceGauge]:
93
+ DashboardDataSourceGaugeComponentUtil,
94
+ [DashboardComponentType.DataSourceTable]:
95
+ DashboardDataSourceTableComponentUtil,
96
+ [DashboardComponentType.LogStream]: DashboardLogStreamComponentUtil,
97
+ [DashboardComponentType.LogChart]: DashboardLogChartComponentUtil,
98
+ [DashboardComponentType.SecurityEventsList]:
99
+ DashboardSecurityEventsListComponentUtil,
100
+ [DashboardComponentType.SecurityEventsFlow]:
101
+ DashboardSecurityEventsFlowComponentUtil,
102
+ [DashboardComponentType.TraceList]: DashboardTraceListComponentUtil,
103
+ [DashboardComponentType.TraceChart]: DashboardTraceChartComponentUtil,
104
+ [DashboardComponentType.TraceTable]: DashboardTraceTableComponentUtil,
105
+ [DashboardComponentType.IncidentList]: DashboardIncidentListComponentUtil,
106
+ [DashboardComponentType.AlertList]: DashboardAlertListComponentUtil,
107
+ [DashboardComponentType.MonitorList]: DashboardMonitorListComponentUtil,
108
+ [DashboardComponentType.Slo]: DashboardSloComponentUtil,
109
+ [DashboardComponentType.KubernetesPodList]:
110
+ DashboardKubernetesPodListComponentUtil,
111
+ [DashboardComponentType.KubernetesNodeList]:
112
+ DashboardKubernetesNodeListComponentUtil,
113
+ [DashboardComponentType.KubernetesNamespaceList]:
114
+ DashboardKubernetesNamespaceListComponentUtil,
115
+ [DashboardComponentType.KubernetesDeploymentList]:
116
+ DashboardKubernetesDeploymentListComponentUtil,
117
+ [DashboardComponentType.KubernetesStatefulSetList]:
118
+ DashboardKubernetesStatefulSetListComponentUtil,
119
+ [DashboardComponentType.KubernetesDaemonSetList]:
120
+ DashboardKubernetesDaemonSetListComponentUtil,
121
+ [DashboardComponentType.KubernetesJobList]:
122
+ DashboardKubernetesJobListComponentUtil,
123
+ [DashboardComponentType.KubernetesCronJobList]:
124
+ DashboardKubernetesCronJobListComponentUtil,
125
+ [DashboardComponentType.DockerHostList]: DashboardDockerHostListComponentUtil,
126
+ [DashboardComponentType.DockerContainerList]:
127
+ DashboardDockerContainerListComponentUtil,
128
+ [DashboardComponentType.DockerImageList]:
129
+ DashboardDockerImageListComponentUtil,
130
+ [DashboardComponentType.DockerNetworkList]:
131
+ DashboardDockerNetworkListComponentUtil,
132
+ [DashboardComponentType.DockerVolumeList]:
133
+ DashboardDockerVolumeListComponentUtil,
134
+ [DashboardComponentType.PodmanHostList]: DashboardPodmanHostListComponentUtil,
135
+ [DashboardComponentType.PodmanContainerList]:
136
+ DashboardPodmanContainerListComponentUtil,
137
+ [DashboardComponentType.PodmanImageList]:
138
+ DashboardPodmanImageListComponentUtil,
139
+ [DashboardComponentType.PodmanNetworkList]:
140
+ DashboardPodmanNetworkListComponentUtil,
141
+ [DashboardComponentType.PodmanVolumeList]:
142
+ DashboardPodmanVolumeListComponentUtil,
143
+ [DashboardComponentType.HostList]: DashboardHostListComponentUtil,
144
+ [DashboardComponentType.ProxmoxNodeList]:
145
+ DashboardProxmoxNodeListComponentUtil,
146
+ [DashboardComponentType.ProxmoxGuestList]:
147
+ DashboardProxmoxGuestListComponentUtil,
148
+ [DashboardComponentType.DockerSwarmNodeList]:
149
+ DashboardDockerSwarmNodeListComponentUtil,
150
+ [DashboardComponentType.DockerSwarmServiceList]:
151
+ DashboardDockerSwarmServiceListComponentUtil,
152
+ [DashboardComponentType.CephOsdList]: DashboardCephOsdListComponentUtil,
153
+ [DashboardComponentType.CephPoolList]: DashboardCephPoolListComponentUtil,
154
+ [DashboardComponentType.NetworkMap]: DashboardNetworkMapComponentUtil,
155
+ [DashboardComponentType.Html]: DashboardHtmlComponentUtil,
156
+ };
157
+
158
+ const ALL_TYPES: Array<DashboardComponentType> = Object.values(
159
+ DashboardComponentType,
160
+ );
161
+
162
+ describe("Dashboard component getDefaultComponent()", () => {
163
+ test("every DashboardComponentType has a default component guarded here", () => {
164
+ // If a new widget type is added to the enum, this fails until it is mapped.
165
+ for (const type of ALL_TYPES) {
166
+ expect(TYPE_TO_UTIL[type]).toBeDefined();
167
+ }
168
+ // And nothing stale: the map must not carry keys the enum dropped.
169
+ expect(Object.keys(TYPE_TO_UTIL).sort()).toEqual([...ALL_TYPES].sort());
170
+ });
171
+
172
+ test.each(ALL_TYPES)(
173
+ "%s default is a well-formed dashboard component",
174
+ (type: DashboardComponentType) => {
175
+ const component: DashboardBaseComponent =
176
+ TYPE_TO_UTIL[type].getDefaultComponent();
177
+
178
+ // It must be tagged as a dashboard component so serialization routes it right.
179
+ expect(component._type).toBe(ObjectType.DashboardComponent);
180
+
181
+ /*
182
+ * The seeded componentType must match the type the editor asked for; a
183
+ * copy-paste that leaves the wrong type here makes the widget render as a
184
+ * different kind than the one the user picked.
185
+ */
186
+ expect(component.componentType).toBe(type);
187
+
188
+ // A zero/negative footprint means the widget is invisible on the grid.
189
+ expect(component.widthInDashboardUnits).toBeGreaterThan(0);
190
+ expect(component.heightInDashboardUnits).toBeGreaterThan(0);
191
+ expect(component.minWidthInDashboardUnits).toBeGreaterThan(0);
192
+ expect(component.minHeightInDashboardUnits).toBeGreaterThan(0);
193
+
194
+ /*
195
+ * The default size must not start below its own minimum, or the resize
196
+ * clamp shrinks the widget the instant it is dropped.
197
+ */
198
+ expect(component.widthInDashboardUnits).toBeGreaterThanOrEqual(
199
+ component.minWidthInDashboardUnits,
200
+ );
201
+ expect(component.heightInDashboardUnits).toBeGreaterThanOrEqual(
202
+ component.minHeightInDashboardUnits,
203
+ );
204
+
205
+ // A widget is dropped at the origin; negative offsets place it off-grid.
206
+ expect(component.topInDashboardUnits).toBeGreaterThanOrEqual(0);
207
+ expect(component.leftInDashboardUnits).toBeGreaterThanOrEqual(0);
208
+
209
+ // The id must be a real, generated ObjectID (UUID), not a placeholder.
210
+ expect(component.componentId).toBeInstanceOf(ObjectID);
211
+ expect(ObjectID.isValidUUID(component.componentId.toString())).toBe(true);
212
+ },
213
+ );
214
+
215
+ test("no two widget types seed the same componentType", () => {
216
+ /*
217
+ * Guards the classic copy-paste bug: duplicating a util and forgetting to
218
+ * change componentType. Every default must report a distinct type.
219
+ */
220
+ const seededTypes: Array<DashboardComponentType> = ALL_TYPES.map(
221
+ (type: DashboardComponentType) => {
222
+ return TYPE_TO_UTIL[type].getDefaultComponent().componentType;
223
+ },
224
+ );
225
+ expect(new Set<DashboardComponentType>(seededTypes).size).toBe(
226
+ seededTypes.length,
227
+ );
228
+ });
229
+
230
+ test("each call generates a fresh componentId so widgets never collide", () => {
231
+ /*
232
+ * Two of the same widget added to one dashboard must not share an id, or
233
+ * selection/deletion in the editor would act on both at once. This only
234
+ * holds if getDefaultComponent() calls ObjectID.generate() per call rather
235
+ * than reusing a module-level constant.
236
+ */
237
+ for (const type of ALL_TYPES) {
238
+ const first: DashboardBaseComponent =
239
+ TYPE_TO_UTIL[type].getDefaultComponent();
240
+ const second: DashboardBaseComponent =
241
+ TYPE_TO_UTIL[type].getDefaultComponent();
242
+ expect(first.componentId.toString()).not.toBe(
243
+ second.componentId.toString(),
244
+ );
245
+ }
246
+ });
247
+
248
+ test("the base component util refuses to seed a component", () => {
249
+ /*
250
+ * The base class intentionally throws so a util that forgets to override
251
+ * getDefaultComponent() fails fast instead of returning a typeless stub.
252
+ */
253
+ expect(() => {
254
+ return DashboardBaseComponentUtil.getDefaultComponent();
255
+ }).toThrow();
256
+ });
257
+ });
@@ -10,7 +10,7 @@
10
10
  * worse than quoting no price at all, so there is exactly one copy.
11
11
  */
12
12
 
13
- // Telemetry ingest - logs, traces, metrics and profiles.
13
+ // Telemetry ingest - logs, traces, metrics, profiles and security events.
14
14
  export const TELEMETRY_PRICE_IN_USD_PER_GB: number = 0.1;
15
15
 
16
16
  /*
@@ -1423,6 +1423,12 @@ export class BillingService extends BaseService {
1423
1423
  }
1424
1424
  return "price_1U0iWJANuQdJ93r7iVAXwhdP";
1425
1425
  }
1426
+ if (productType === ProductType.SecurityEvents) {
1427
+ if (this.isTestEnvironment()) {
1428
+ return "price_1U7efaANuQdJ93r74hFVOgdS";
1429
+ }
1430
+ return "price_1U7edTANuQdJ93r7hR9jpBfv";
1431
+ }
1426
1432
  throw new BadDataException("Plan with productType " + productType + " not found");
1427
1433
  }
1428
1434
  /*