@oneuptime/common 11.3.19 → 11.3.21

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 (77) hide show
  1. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +39 -0
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Server/API/DashboardAPI.ts +221 -0
  5. package/Server/API/EnterpriseLicenseAPI.ts +279 -12
  6. package/Server/API/GlobalConfigAPI.ts +81 -16
  7. package/Server/API/StatusPageAPI.ts +894 -791
  8. package/Server/EnvironmentConfig.ts +9 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.ts +23 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  12. package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Services/IoTDeviceService.ts +99 -4
  15. package/Server/Services/StatusPageService.ts +104 -45
  16. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
  17. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
  18. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
  19. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
  20. package/ServiceRoute.ts +4 -0
  21. package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
  22. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
  23. package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
  24. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
  25. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
  26. package/Types/Monitor/MonitorStep.ts +32 -0
  27. package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
  28. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
  29. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
  30. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
  31. package/build/dist/Models/DatabaseModels/GlobalConfig.js +39 -0
  32. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  33. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  34. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  35. package/build/dist/Server/API/DashboardAPI.js +166 -16
  36. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  37. package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
  38. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  39. package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
  40. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  41. package/build/dist/Server/API/StatusPageAPI.js +74 -31
  42. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  43. package/build/dist/Server/EnvironmentConfig.js +6 -0
  44. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js +14 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js.map +1 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
  52. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
  53. package/build/dist/Server/Services/Index.js +2 -0
  54. package/build/dist/Server/Services/Index.js.map +1 -1
  55. package/build/dist/Server/Services/IoTDeviceService.js +66 -4
  56. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  57. package/build/dist/Server/Services/StatusPageService.js +91 -43
  58. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  59. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
  60. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  61. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
  62. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  63. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
  64. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  65. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
  66. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  67. package/build/dist/ServiceRoute.js +1 -0
  68. package/build/dist/ServiceRoute.js.map +1 -1
  69. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
  70. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
  71. package/build/dist/Types/Monitor/MonitorStep.js +25 -0
  72. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  73. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
  74. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  75. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
  76. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
  77. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
2
+ import EnterpriseLicense from "./EnterpriseLicense";
3
+ import Route from "../../Types/API/Route";
4
+ import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
5
+ import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
6
+ import ColumnLength from "../../Types/Database/ColumnLength";
7
+ import ColumnType from "../../Types/Database/ColumnType";
8
+ import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
9
+ import TableColumn from "../../Types/Database/TableColumn";
10
+ import TableColumnType from "../../Types/Database/TableColumnType";
11
+ import TableMetadata from "../../Types/Database/TableMetadata";
12
+ import IconProp from "../../Types/Icon/IconProp";
13
+ import ObjectID from "../../Types/ObjectID";
14
+ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
15
+
16
+ @TableAccessControl({
17
+ create: [],
18
+ read: [],
19
+ update: [],
20
+ delete: [],
21
+ })
22
+ @CrudApiEndpoint(new Route("/enterprise-license-instance"))
23
+ @TableMetadata({
24
+ tableName: "EnterpriseLicenseInstance",
25
+ singularName: "Enterprise License Instance",
26
+ pluralName: "Enterprise License Instances",
27
+ icon: IconProp.Lock,
28
+ tableDescription:
29
+ "Self-hosted OneUptime instances that report usage against an enterprise license. One license can be used across multiple instances (staging, production, etc.) and users are counted uniquely across all of them.",
30
+ })
31
+ @Entity({
32
+ name: "EnterpriseLicenseInstance",
33
+ })
34
+ // One row per instance per license — upserts rely on this being unique.
35
+ @Index(["enterpriseLicenseId", "instanceId"], { unique: true })
36
+ export default class EnterpriseLicenseInstance extends BaseModel {
37
+ @ColumnAccessControl({
38
+ create: [],
39
+ read: [],
40
+ update: [],
41
+ })
42
+ @TableColumn({
43
+ manyToOneRelationColumn: "enterpriseLicenseId",
44
+ type: TableColumnType.Entity,
45
+ modelType: EnterpriseLicense,
46
+ title: "Enterprise License",
47
+ description: "Enterprise license this instance reports usage against.",
48
+ })
49
+ @ManyToOne(
50
+ () => {
51
+ return EnterpriseLicense;
52
+ },
53
+ {
54
+ eager: false,
55
+ nullable: true,
56
+ onDelete: "CASCADE",
57
+ orphanedRowAction: "nullify",
58
+ },
59
+ )
60
+ @JoinColumn({ name: "enterpriseLicenseId" })
61
+ public enterpriseLicense?: EnterpriseLicense = undefined;
62
+
63
+ @ColumnAccessControl({
64
+ create: [],
65
+ read: [],
66
+ update: [],
67
+ })
68
+ @Index()
69
+ @TableColumn({
70
+ type: TableColumnType.ObjectID,
71
+ required: true,
72
+ canReadOnRelationQuery: true,
73
+ title: "Enterprise License ID",
74
+ description: "ID of the enterprise license this instance belongs to.",
75
+ })
76
+ @Column({
77
+ type: ColumnType.ObjectID,
78
+ nullable: false,
79
+ transformer: ObjectID.getDatabaseTransformer(),
80
+ })
81
+ public enterpriseLicenseId?: ObjectID = undefined;
82
+
83
+ @ColumnAccessControl({
84
+ create: [],
85
+ read: [],
86
+ update: [],
87
+ })
88
+ @Index()
89
+ @TableColumn({
90
+ required: true,
91
+ type: TableColumnType.ShortText,
92
+ title: "Instance ID",
93
+ description:
94
+ "Unique identifier of the self-hosted OneUptime instance (auto-generated on the instance when it is installed).",
95
+ })
96
+ @Column({
97
+ nullable: false,
98
+ type: ColumnType.ShortText,
99
+ length: ColumnLength.ShortText,
100
+ })
101
+ public instanceId?: string = undefined;
102
+
103
+ @ColumnAccessControl({
104
+ create: [],
105
+ read: [],
106
+ update: [],
107
+ })
108
+ @TableColumn({
109
+ required: false,
110
+ type: TableColumnType.ShortText,
111
+ title: "Host",
112
+ description:
113
+ "Host of the self-hosted instance (for example oneuptime.acme.com). Shown to the customer so they can tell instances apart.",
114
+ })
115
+ @Column({
116
+ nullable: true,
117
+ type: ColumnType.ShortText,
118
+ length: ColumnLength.ShortText,
119
+ })
120
+ public host?: string = undefined;
121
+
122
+ @ColumnAccessControl({
123
+ create: [],
124
+ read: [],
125
+ update: [],
126
+ })
127
+ @TableColumn({
128
+ required: false,
129
+ type: TableColumnType.Number,
130
+ title: "User Count",
131
+ description: "Most recent user count reported by this instance.",
132
+ })
133
+ @Column({
134
+ nullable: true,
135
+ type: ColumnType.Number,
136
+ })
137
+ public userCount?: number = undefined;
138
+
139
+ @ColumnAccessControl({
140
+ create: [],
141
+ read: [],
142
+ update: [],
143
+ })
144
+ @TableColumn({
145
+ required: false,
146
+ type: TableColumnType.JSON,
147
+ title: "User Email Hashes",
148
+ description:
149
+ "SHA-256 hashes of the emails of users on this instance. Used to count unique users across all instances of a license without storing raw emails.",
150
+ })
151
+ @Column({
152
+ type: ColumnType.JSON,
153
+ nullable: true,
154
+ })
155
+ public userEmailHashes?: Array<string> = undefined;
156
+
157
+ @ColumnAccessControl({
158
+ create: [],
159
+ read: [],
160
+ update: [],
161
+ })
162
+ @TableColumn({
163
+ required: false,
164
+ type: TableColumnType.Date,
165
+ title: "Last Reported At",
166
+ description: "Timestamp of the most recent report from this instance.",
167
+ })
168
+ @Column({
169
+ nullable: true,
170
+ type: ColumnType.Date,
171
+ })
172
+ public lastReportedAt?: Date = undefined;
173
+ }
@@ -13,6 +13,7 @@ import Email from "../../Types/Email";
13
13
  import MailTransportType from "../../Types/Email/MailTransportType";
14
14
  import OAuthProviderType from "../../Types/Email/OAuthProviderType";
15
15
  import SMTPAuthenticationType from "../../Types/Email/SMTPAuthenticationType";
16
+ import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
16
17
  import IconProp from "../../Types/Icon/IconProp";
17
18
  import ObjectID from "../../Types/ObjectID";
18
19
  import Phone from "../../Types/Phone";
@@ -42,6 +43,26 @@ export enum EmailServerType {
42
43
  update: [],
43
44
  })
44
45
  export default class GlobalConfig extends GlobalConfigModel {
46
+ @ColumnAccessControl({
47
+ create: [],
48
+ read: [],
49
+ update: [],
50
+ })
51
+ @TableColumn({
52
+ type: TableColumnType.ObjectID,
53
+ computed: true,
54
+ title: "Instance ID",
55
+ description:
56
+ "Unique identifier for this OneUptime instance. Auto-generated when the instance is installed.",
57
+ })
58
+ @Column({
59
+ type: ColumnType.ObjectID,
60
+ nullable: true,
61
+ unique: true,
62
+ transformer: ObjectID.getDatabaseTransformer(),
63
+ })
64
+ public instanceId?: ObjectID = undefined;
65
+
45
66
  @ColumnAccessControl({
46
67
  create: [],
47
68
  read: [],
@@ -854,6 +875,24 @@ export default class GlobalConfig extends GlobalConfigModel {
854
875
  })
855
876
  public enterpriseLicenseUserCountUpdatedAt?: Date = undefined;
856
877
 
878
+ @ColumnAccessControl({
879
+ create: [],
880
+ read: [],
881
+ update: [],
882
+ })
883
+ @TableColumn({
884
+ type: TableColumnType.JSON,
885
+ title: "Enterprise License Instances",
886
+ description:
887
+ "Instances (staging, production, etc.) that share the validated enterprise license, as last reported by OneUptime. Users are counted uniquely across all of them.",
888
+ })
889
+ @Column({
890
+ type: ColumnType.JSON,
891
+ nullable: true,
892
+ })
893
+ public enterpriseLicenseInstances?: Array<EnterpriseLicenseInstanceSummary> =
894
+ undefined;
895
+
857
896
  @ColumnAccessControl({
858
897
  create: [],
859
898
  read: [],
@@ -182,6 +182,7 @@ import GlobalSSOProject from "./GlobalSsoProject";
182
182
  import GlobalOIDCProject from "./GlobalOidcProject";
183
183
  import PromoCode from "./PromoCode";
184
184
  import EnterpriseLicense from "./EnterpriseLicense";
185
+ import EnterpriseLicenseInstance from "./EnterpriseLicenseInstance";
185
186
  import OpenSourceDeployment from "./OpenSourceDeployment";
186
187
  import Reseller from "./Reseller";
187
188
  import ResellerPlan from "./ResellerPlan";
@@ -651,6 +652,7 @@ const AllModelTypes: Array<{
651
652
 
652
653
  PromoCode,
653
654
  EnterpriseLicense,
655
+ EnterpriseLicenseInstance,
654
656
  OpenSourceDeployment,
655
657
 
656
658
  GlobalConfig,
@@ -375,6 +375,58 @@ const PUBLIC_DASHBOARD_RESOURCES: Record<
375
375
  },
376
376
  };
377
377
 
378
+ type ResolveDashboardIdOrThrowFunction = (
379
+ dashboardIdOrDomain: string,
380
+ ) => Promise<ObjectID>;
381
+
382
+ /*
383
+ * Resolve the :dashboardIdOrDomain route param to a dashboard ID. Accepts
384
+ * either a dashboard ID (UUID) or a verified custom dashboard domain —
385
+ * same contract as the status page overview endpoint.
386
+ */
387
+ const resolveDashboardIdOrThrow: ResolveDashboardIdOrThrowFunction = async (
388
+ dashboardIdOrDomain: string,
389
+ ): Promise<ObjectID> => {
390
+ if (!dashboardIdOrDomain) {
391
+ throw new NotFoundException("Dashboard not found");
392
+ }
393
+
394
+ if (dashboardIdOrDomain.includes(".")) {
395
+ const dashboardDomain: DashboardDomain | null =
396
+ await DashboardDomainService.findOneBy({
397
+ query: {
398
+ fullDomain: dashboardIdOrDomain,
399
+ domain: {
400
+ isVerified: true,
401
+ } as any,
402
+ },
403
+ select: {
404
+ dashboardId: true,
405
+ },
406
+ props: {
407
+ isRoot: true,
408
+ },
409
+ });
410
+
411
+ if (!dashboardDomain || !dashboardDomain.dashboardId) {
412
+ throw new NotFoundException("Dashboard not found");
413
+ }
414
+
415
+ return dashboardDomain.dashboardId;
416
+ }
417
+
418
+ try {
419
+ ObjectID.validateUUID(dashboardIdOrDomain);
420
+ return new ObjectID(dashboardIdOrDomain);
421
+ } catch (err) {
422
+ logger.error(
423
+ `Error converting dashboardIdOrDomain to ObjectID: ${dashboardIdOrDomain}`,
424
+ );
425
+ logger.error(err);
426
+ throw new NotFoundException("Dashboard not found");
427
+ }
428
+ };
429
+
378
430
  export default class DashboardAPI extends BaseAPI<
379
431
  Dashboard,
380
432
  DashboardServiceType
@@ -476,6 +528,111 @@ export default class DashboardAPI extends BaseAPI<
476
528
  },
477
529
  );
478
530
 
531
+ /*
532
+ * Shared handler for the public overview endpoint. Registered for both
533
+ * POST and GET (machine-readable access for crawlers / AI agents that
534
+ * only issue plain GET requests — discovered via llms.txt on public
535
+ * dashboard URLs) so the authentication and authorization path can
536
+ * never drift between the two methods. It does not read req.body.
537
+ */
538
+ const overviewHandler: (
539
+ req: ExpressRequest,
540
+ res: ExpressResponse,
541
+ next: NextFunction,
542
+ ) => Promise<void> = async (
543
+ req: ExpressRequest,
544
+ res: ExpressResponse,
545
+ next: NextFunction,
546
+ ): Promise<void> => {
547
+ try {
548
+ const dashboardId: ObjectID = await resolveDashboardIdOrThrow(
549
+ req.params["dashboardIdOrDomain"] as string,
550
+ );
551
+
552
+ // Check read access (handles public check, IP whitelist, master password)
553
+ const accessResult: {
554
+ hasReadAccess: boolean;
555
+ error?: NotAuthenticatedException | ForbiddenException;
556
+ } = await DashboardService.hasReadAccess({
557
+ dashboardId,
558
+ req,
559
+ });
560
+
561
+ if (!accessResult.hasReadAccess) {
562
+ throw (
563
+ accessResult.error ||
564
+ new BadDataException("Access denied to this dashboard.")
565
+ );
566
+ }
567
+
568
+ const dashboard: Dashboard | null = await DashboardService.findOneById({
569
+ id: dashboardId,
570
+ select: {
571
+ _id: true,
572
+ name: true,
573
+ description: true,
574
+ pageTitle: true,
575
+ pageDescription: true,
576
+ dashboardViewConfig: true,
577
+ },
578
+ props: {
579
+ isRoot: true,
580
+ },
581
+ });
582
+
583
+ if (!dashboard) {
584
+ throw new NotFoundException("Dashboard not found");
585
+ }
586
+
587
+ /*
588
+ * Everything below is derived from columns the public /metadata and
589
+ * /view-config endpoints already serve to anonymous viewers — the
590
+ * overview only reshapes them into a single GET-able document.
591
+ */
592
+ return Response.sendJsonObjectResponse(req, res, {
593
+ _id: dashboard._id?.toString() || "",
594
+ name: dashboard.name || "Dashboard",
595
+ description: dashboard.description || "",
596
+ pageTitle: dashboard.pageTitle || "",
597
+ pageDescription: dashboard.pageDescription || "",
598
+ components: DashboardAPI.buildPublicComponentSummaries(
599
+ dashboard.dashboardViewConfig,
600
+ ),
601
+ metricsUsed: Array.from(
602
+ DashboardAPI.collectDashboardMetricNames(
603
+ dashboard.dashboardViewConfig,
604
+ ),
605
+ ).sort(),
606
+ dashboardViewConfig: dashboard.dashboardViewConfig
607
+ ? JSONFunctions.serialize(dashboard.dashboardViewConfig as any)
608
+ : null,
609
+ });
610
+ } catch (err) {
611
+ next(err);
612
+ }
613
+ };
614
+
615
+ const overviewApiPath: string = `${new this.entityType()
616
+ .getCrudApiPath()
617
+ ?.toString()}/overview/:dashboardIdOrDomain`;
618
+
619
+ this.router.post(
620
+ overviewApiPath,
621
+ UserMiddleware.getUserMiddleware,
622
+ overviewHandler,
623
+ );
624
+
625
+ /*
626
+ * GET variant of the overview endpoint. Same middleware and same handler
627
+ * as the POST route above — non-public dashboards are rejected for
628
+ * unauthenticated callers exactly as they are on POST.
629
+ */
630
+ this.router.get(
631
+ overviewApiPath,
632
+ UserMiddleware.getUserMiddleware,
633
+ overviewHandler,
634
+ );
635
+
479
636
  // Domain resolution endpoint
480
637
  this.router.post(
481
638
  `${new this.entityType().getCrudApiPath()?.toString()}/domain`,
@@ -1079,6 +1236,70 @@ export default class DashboardAPI extends BaseAPI<
1079
1236
  );
1080
1237
  }
1081
1238
 
1239
+ /*
1240
+ * Build a compact machine-readable summary of the widgets on a dashboard
1241
+ * for the public overview endpoint. Derived entirely from the stored
1242
+ * dashboardViewConfig (already served to public viewers by /view-config) —
1243
+ * this only lifts out the human-meaningful fields (widget type, title,
1244
+ * text, metric names). The config comes straight from a JSON column, so
1245
+ * every field is read defensively.
1246
+ */
1247
+ private static buildPublicComponentSummaries(
1248
+ dashboardViewConfig: unknown,
1249
+ ): Array<JSONObject> {
1250
+ const summaries: Array<JSONObject> = [];
1251
+
1252
+ const components: unknown =
1253
+ dashboardViewConfig && typeof dashboardViewConfig === "object"
1254
+ ? (dashboardViewConfig as Record<string, unknown>)["components"]
1255
+ : undefined;
1256
+
1257
+ if (!Array.isArray(components)) {
1258
+ return summaries;
1259
+ }
1260
+
1261
+ for (const component of components) {
1262
+ if (!component || typeof component !== "object") {
1263
+ continue;
1264
+ }
1265
+
1266
+ const componentObject: Record<string, unknown> = component as Record<
1267
+ string,
1268
+ unknown
1269
+ >;
1270
+
1271
+ const argumentsObject: Record<string, unknown> =
1272
+ componentObject["arguments"] &&
1273
+ typeof componentObject["arguments"] === "object"
1274
+ ? (componentObject["arguments"] as Record<string, unknown>)
1275
+ : {};
1276
+
1277
+ const getStringArgument: (key: string) => string | null = (
1278
+ key: string,
1279
+ ): string | null => {
1280
+ const value: unknown = argumentsObject[key];
1281
+ return typeof value === "string" && value.trim().length > 0
1282
+ ? value
1283
+ : null;
1284
+ };
1285
+
1286
+ summaries.push({
1287
+ componentType:
1288
+ typeof componentObject["componentType"] === "string"
1289
+ ? componentObject["componentType"]
1290
+ : "",
1291
+ title: getStringArgument("chartTitle") || getStringArgument("title"),
1292
+ description: getStringArgument("chartDescription"),
1293
+ text: getStringArgument("text"),
1294
+ metricNames: Array.from(
1295
+ DashboardAPI.collectDashboardMetricNames(component),
1296
+ ).sort(),
1297
+ });
1298
+ }
1299
+
1300
+ return summaries;
1301
+ }
1302
+
1082
1303
  /*
1083
1304
  * Walk a stored dashboard view config and collect every metric name it
1084
1305
  * references (chart/value/gauge/table widgets all carry their metric in a