@oneuptime/common 11.7.2 → 11.7.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 (60) hide show
  1. package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
  2. package/Models/DatabaseModels/TableView.ts +40 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
  6. package/Tests/Types/JSONFunctions.test.ts +103 -1
  7. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
  8. package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
  9. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
  10. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
  11. package/Tests/Utils/UserPreferences.test.ts +563 -0
  12. package/Types/JSONFunctions.ts +7 -2
  13. package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
  14. package/UI/Components/MasterPage/MasterPage.tsx +65 -0
  15. package/UI/Components/ModelTable/BaseModelTable.tsx +387 -4
  16. package/UI/Components/ModelTable/Column.ts +17 -0
  17. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
  18. package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
  19. package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
  20. package/UI/Components/ModelTable/TableView.tsx +24 -2
  21. package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
  22. package/UI/Components/SideMenu/SideMenu.tsx +24 -4
  23. package/UI/Components/Table/Table.tsx +14 -1
  24. package/UI/Components/Table/TableRow.tsx +180 -175
  25. package/UI/Components/Table/Types/Column.ts +2 -0
  26. package/Utils/UserPreferences.ts +53 -0
  27. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
  28. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
  29. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  30. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  35. package/build/dist/Types/JSONFunctions.js +8 -3
  36. package/build/dist/Types/JSONFunctions.js.map +1 -1
  37. package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
  38. package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
  39. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  40. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +239 -7
  41. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  42. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
  43. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
  44. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
  45. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
  46. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
  47. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
  48. package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
  49. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  50. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
  51. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
  52. package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
  53. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  54. package/build/dist/UI/Components/Table/Table.js +15 -2
  55. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  56. package/build/dist/UI/Components/Table/TableRow.js +11 -6
  57. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  58. package/build/dist/Utils/UserPreferences.js +33 -0
  59. package/build/dist/Utils/UserPreferences.js.map +1 -1
  60. package/package.json +1 -1
@@ -348,6 +348,12 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
348
348
  title: "CPU Core Usage Average",
349
349
  description: "Average CPU cores actually used over the window.",
350
350
  },
351
+ {
352
+ key: "cpuCoreLimitAverage",
353
+ title: "CPU Core Limit Average",
354
+ description:
355
+ "Average CPU core limit over the window. 0 when no limit is set — which is also what a row ingested before this column reads back.",
356
+ },
351
357
  {
352
358
  key: "cpuCost",
353
359
  title: "CPU Cost",
@@ -378,6 +384,18 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
378
384
  title: "RAM Bytes Usage Average",
379
385
  description: "Average RAM bytes actually used over the window.",
380
386
  },
387
+ {
388
+ key: "ramBytesLimitAverage",
389
+ title: "RAM Bytes Limit Average",
390
+ description:
391
+ "Average RAM byte limit over the window. 0 when no limit is set — which is also what a row ingested before this column reads back.",
392
+ },
393
+ {
394
+ key: "ramBytesUsageMax",
395
+ title: "RAM Bytes Usage Max",
396
+ description:
397
+ "Peak RAM bytes used during the window, from Prometheus rather than the cost engine — the engine only reports averages, and an hourly mean hides the burst that OOMKills a container. 0 when the agent has no Prometheus configured, when the scrape had no data, or on rows ingested before this column; treat 0 as 'unknown', never as a real peak.",
398
+ },
381
399
  {
382
400
  key: "ramCost",
383
401
  title: "RAM Cost",
@@ -472,6 +490,49 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
472
490
  },
473
491
  });
474
492
 
493
+ /*
494
+ * Delivery bookkeeping, not workload data. A window wider than the
495
+ * agent's batch size arrives as several independent ingest jobs, so the
496
+ * ingest service needs to tell "another chunk of the delivery I am
497
+ * already ingesting" (accept) from "a window a previous delivery already
498
+ * ingested" (drop, or a restarted agent double-counts spend). shipmentId
499
+ * is the agent's content hash of the window — identical across restarts,
500
+ * different when the engine's answer for that window changed.
501
+ *
502
+ * Empty / 0 on rows written before these columns existed and on rows
503
+ * from agents older than the shipment contract; the ingest service falls
504
+ * back to the whole-window guard for those.
505
+ */
506
+ const shipmentIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
507
+ key: "shipmentId",
508
+ title: "Shipment ID",
509
+ description:
510
+ "Content hash identifying the agent delivery this row arrived in. Used to deduplicate re-shipped windows; empty for rows ingested before the shipment contract.",
511
+ required: true,
512
+ defaultValue: "",
513
+ type: TableColumnType.Text,
514
+ accessControl: {
515
+ read: readPermissions,
516
+ create: createPermissions,
517
+ update: [],
518
+ },
519
+ });
520
+
521
+ const shipmentChunkColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
522
+ key: "shipmentChunk",
523
+ title: "Shipment Chunk",
524
+ description:
525
+ "Index of the request within its shipment that carried this row. 0 for single-request shipments and for rows ingested before the shipment contract.",
526
+ required: true,
527
+ defaultValue: 0,
528
+ type: TableColumnType.Number,
529
+ accessControl: {
530
+ read: readPermissions,
531
+ create: createPermissions,
532
+ update: [],
533
+ },
534
+ });
535
+
475
536
  const retentionDateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
476
537
  key: "retentionDate",
477
538
  codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
@@ -517,6 +578,8 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
517
578
  labelKeysColumn,
518
579
  ...measureColumns,
519
580
  currencyColumn,
581
+ shipmentIdColumn,
582
+ shipmentChunkColumn,
520
583
  retentionDateColumn,
521
584
  ],
522
585
  sortKeys: [
@@ -688,6 +751,14 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
688
751
  this.setColumnValue("cpuCoreUsageAverage", v);
689
752
  }
690
753
 
754
+ public get cpuCoreLimitAverage(): number | undefined {
755
+ return this.getColumnValue("cpuCoreLimitAverage") as number | undefined;
756
+ }
757
+
758
+ public set cpuCoreLimitAverage(v: number | undefined) {
759
+ this.setColumnValue("cpuCoreLimitAverage", v);
760
+ }
761
+
691
762
  public get cpuCost(): number | undefined {
692
763
  return this.getColumnValue("cpuCost") as number | undefined;
693
764
  }
@@ -736,6 +807,22 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
736
807
  this.setColumnValue("ramBytesUsageAverage", v);
737
808
  }
738
809
 
810
+ public get ramBytesLimitAverage(): number | undefined {
811
+ return this.getColumnValue("ramBytesLimitAverage") as number | undefined;
812
+ }
813
+
814
+ public set ramBytesLimitAverage(v: number | undefined) {
815
+ this.setColumnValue("ramBytesLimitAverage", v);
816
+ }
817
+
818
+ public get ramBytesUsageMax(): number | undefined {
819
+ return this.getColumnValue("ramBytesUsageMax") as number | undefined;
820
+ }
821
+
822
+ public set ramBytesUsageMax(v: number | undefined) {
823
+ this.setColumnValue("ramBytesUsageMax", v);
824
+ }
825
+
739
826
  public get ramCost(): number | undefined {
740
827
  return this.getColumnValue("ramCost") as number | undefined;
741
828
  }
@@ -832,6 +919,22 @@ export default class KubernetesCostAllocation extends AnalyticsBaseModel {
832
919
  this.setColumnValue("currency", v);
833
920
  }
834
921
 
922
+ public get shipmentId(): string | undefined {
923
+ return this.getColumnValue("shipmentId") as string | undefined;
924
+ }
925
+
926
+ public set shipmentId(v: string | undefined) {
927
+ this.setColumnValue("shipmentId", v);
928
+ }
929
+
930
+ public get shipmentChunk(): number | undefined {
931
+ return this.getColumnValue("shipmentChunk") as number | undefined;
932
+ }
933
+
934
+ public set shipmentChunk(v: number | undefined) {
935
+ this.setColumnValue("shipmentChunk", v);
936
+ }
937
+
835
938
  public get retentionDate(): Date | undefined {
836
939
  return this.getColumnValue("retentionDate") as Date | undefined;
837
940
  }
@@ -555,4 +555,44 @@ export default class TableView extends BaseModel {
555
555
  nullable: true,
556
556
  })
557
557
  public facets?: JSONObject = undefined;
558
+
559
+ @ColumnAccessControl({
560
+ create: [
561
+ Permission.ProjectOwner,
562
+ Permission.ProjectAdmin,
563
+ Permission.CreateTableView,
564
+ ],
565
+ read: [
566
+ Permission.ProjectOwner,
567
+ Permission.ProjectAdmin,
568
+ Permission.ProjectMember,
569
+ Permission.Viewer,
570
+ Permission.SettingsAdmin,
571
+ Permission.SettingsMember,
572
+ Permission.SettingsViewer,
573
+ Permission.ReadTableView,
574
+ ],
575
+ update: [
576
+ Permission.ProjectOwner,
577
+ Permission.ProjectAdmin,
578
+ Permission.EditTableView,
579
+ ],
580
+ })
581
+ @TableColumn({
582
+ title: "Columns",
583
+ required: false,
584
+ unique: false,
585
+ type: TableColumnType.JSON,
586
+ canReadOnRelationQuery: true,
587
+ description:
588
+ "Which columns are shown, and in what order, for this table view",
589
+ example:
590
+ '{"order": ["name", "currentMonitorStatus"], "hidden": ["labels"]}',
591
+ })
592
+ @Column({
593
+ type: ColumnType.JSON,
594
+ unique: false,
595
+ nullable: true,
596
+ })
597
+ public columns?: JSONObject = undefined;
558
598
  }
@@ -0,0 +1,13 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddColumnsToTableView1785241000000 implements MigrationInterface {
4
+ public name: string = "AddColumnsToTableView1785241000000";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "TableView" ADD "columns" jsonb`);
8
+ }
9
+
10
+ public async down(queryRunner: QueryRunner): Promise<void> {
11
+ await queryRunner.query(`ALTER TABLE "TableView" DROP COLUMN "columns"`);
12
+ }
13
+ }
@@ -473,6 +473,7 @@ import { MigrationName1785066759532 } from "./1785066759532-MigrationName";
473
473
  import { AddHotQueryIndexes1785140242697 } from "./1785140242697-AddHotQueryIndexes";
474
474
  import { AddHotQueryIndexesSecondPass1785148065137 } from "./1785148065137-AddHotQueryIndexesSecondPass";
475
475
  import { RepairCrossProjectMonitorStatusReferences1785240000000 } from "./1785240000000-RepairCrossProjectMonitorStatusReferences";
476
+ import { AddColumnsToTableView1785241000000 } from "./1785241000000-AddColumnsToTableView";
476
477
 
477
478
  export default [
478
479
  InitialMigration,
@@ -950,4 +951,5 @@ export default [
950
951
  AddHotQueryIndexes1785140242697,
951
952
  AddHotQueryIndexesSecondPass1785148065137,
952
953
  RepairCrossProjectMonitorStatusReferences1785240000000,
954
+ AddColumnsToTableView1785241000000,
953
955
  ];
@@ -37,12 +37,15 @@ describe("Analytics KubernetesCostAllocation model", () => {
37
37
  "cpuCoreHours",
38
38
  "cpuCoreRequestAverage",
39
39
  "cpuCoreUsageAverage",
40
+ "cpuCoreLimitAverage",
40
41
  "cpuCost",
41
42
  "gpuHours",
42
43
  "gpuCost",
43
44
  "ramByteHours",
44
45
  "ramBytesRequestAverage",
45
46
  "ramBytesUsageAverage",
47
+ "ramBytesLimitAverage",
48
+ "ramBytesUsageMax",
46
49
  "ramCost",
47
50
  "pvByteHours",
48
51
  "pvCost",
@@ -95,6 +98,19 @@ describe("Analytics KubernetesCostAllocation model", () => {
95
98
  );
96
99
  });
97
100
 
101
+ test("carries the shipment identity the ingest dedup relies on", () => {
102
+ const shipmentId: AnalyticsTableColumn | null =
103
+ model.getTableColumn("shipmentId");
104
+ expect(shipmentId?.type).toBe(TableColumnType.Text);
105
+ // Rows written before the shipment contract read back as "no identity".
106
+ expect(shipmentId?.defaultValue).toBe("");
107
+
108
+ const shipmentChunk: AnalyticsTableColumn | null =
109
+ model.getTableColumn("shipmentChunk");
110
+ expect(shipmentChunk?.type).toBe(TableColumnType.Number);
111
+ expect(shipmentChunk?.defaultValue).toBe(0);
112
+ });
113
+
98
114
  test("stores labels as a map with an extracted key column", () => {
99
115
  const labels: AnalyticsTableColumn | null = model.getTableColumn("labels");
100
116
  expect(labels?.type).toBe(TableColumnType.MapStringString);
@@ -1,4 +1,4 @@
1
- import { JSONArray, JSONObject } from "../../Types/JSON";
1
+ import { JSONArray, JSONObject, ObjectType } from "../../Types/JSON";
2
2
  import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
3
3
  import JSONFunctions from "../../Types/JSONFunctions";
4
4
 
@@ -262,4 +262,106 @@ describe("JSONFunctions Class", () => {
262
262
  expect(size).toBeGreaterThanOrEqual(0);
263
263
  });
264
264
  });
265
+
266
+ describe("serialize and serializeArray Methods", () => {
267
+ test("Passes primitives and null through, and drops undefined keys", () => {
268
+ const input: JSONObject = {
269
+ n: 1,
270
+ s: "x",
271
+ b: true,
272
+ nil: null,
273
+ gone: undefined,
274
+ };
275
+ expect(JSONFunctions.serialize(input)).toEqual({
276
+ n: 1,
277
+ s: "x",
278
+ b: true,
279
+ nil: null,
280
+ });
281
+ });
282
+
283
+ test("Tags a Date value with its DateTime object type", () => {
284
+ const serialized: JSONObject = JSONFunctions.serialize({
285
+ when: new Date("2023-01-01T00:00:00.000Z"),
286
+ });
287
+ const when: JSONObject = serialized["when"] as JSONObject;
288
+ expect(when["_type"]).toBe(ObjectType.DateTime);
289
+ expect(typeof when["value"]).toBe("string");
290
+ });
291
+
292
+ test("Serializes nested objects recursively", () => {
293
+ expect(JSONFunctions.serialize({ a: { b: 2 } })).toEqual({ a: { b: 2 } });
294
+ });
295
+
296
+ test("Serializes each value inside an array", () => {
297
+ expect(JSONFunctions.serialize({ arr: [1, "x", null] })).toEqual({
298
+ arr: [1, "x", null],
299
+ });
300
+ });
301
+
302
+ test("serializeArray serializes each object in the array", () => {
303
+ const input: JSONArray = [{ a: 1 }, { b: 2 }];
304
+ expect(JSONFunctions.serializeArray(input)).toEqual([{ a: 1 }, { b: 2 }]);
305
+ });
306
+ });
307
+
308
+ describe("deserialize and deserializeArray Methods", () => {
309
+ test("Passes primitives and null through", () => {
310
+ const input: JSONObject = { n: 1, s: "x", nil: null };
311
+ expect(JSONFunctions.deserialize(input)).toEqual({
312
+ n: 1,
313
+ s: "x",
314
+ nil: null,
315
+ });
316
+ });
317
+
318
+ test("Rebuilds a Buffer-tagged value into a Buffer", () => {
319
+ const input: JSONObject = {
320
+ payload: {
321
+ _type: ObjectType.Buffer,
322
+ value: { type: ObjectType.Buffer, data: [1, 2, 3] },
323
+ },
324
+ };
325
+ const deserialized: JSONObject = JSONFunctions.deserialize(input);
326
+ expect(Buffer.isBuffer(deserialized["payload"])).toBe(true);
327
+ expect(deserialized["payload"]).toEqual(Buffer.from([1, 2, 3]));
328
+ });
329
+
330
+ test("deserializeArray deserializes each object in the array", () => {
331
+ const input: JSONArray = [{ a: 1 }, { b: 2 }];
332
+ expect(JSONFunctions.deserializeArray(input)).toEqual([
333
+ { a: 1 },
334
+ { b: 2 },
335
+ ]);
336
+ });
337
+ });
338
+
339
+ describe("serializeValue and deserializeValue Methods", () => {
340
+ test("Pass numbers and non-empty strings through unchanged", () => {
341
+ expect(JSONFunctions.serializeValue(5)).toBe(5);
342
+ expect(JSONFunctions.serializeValue("hi")).toBe("hi");
343
+ expect(JSONFunctions.deserializeValue(5)).toBe(5);
344
+ expect(JSONFunctions.deserializeValue("hi")).toBe("hi");
345
+ });
346
+
347
+ test("Pass null through unchanged", () => {
348
+ expect(JSONFunctions.serializeValue(null)).toBeNull();
349
+ expect(JSONFunctions.deserializeValue(null)).toBeNull();
350
+ });
351
+ });
352
+
353
+ describe("serialize and deserialize round trip", () => {
354
+ test("A plain object survives a serialize -> JSON -> deserialize round trip", () => {
355
+ const original: JSONObject = {
356
+ a: 1,
357
+ b: "x",
358
+ c: { d: 2 },
359
+ e: [1, 2, 3],
360
+ };
361
+ const roundTripped: JSONObject = JSONFunctions.deserialize(
362
+ JSON.parse(JSON.stringify(JSONFunctions.serialize(original))),
363
+ );
364
+ expect(roundTripped).toEqual(original);
365
+ });
366
+ });
265
367
  });