@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.
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
- package/Models/DatabaseModels/GlobalConfig.ts +39 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Server/API/DashboardAPI.ts +221 -0
- package/Server/API/EnterpriseLicenseAPI.ts +279 -12
- package/Server/API/GlobalConfigAPI.ts +81 -16
- package/Server/API/StatusPageAPI.ts +894 -791
- package/Server/EnvironmentConfig.ts +9 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTDeviceService.ts +99 -4
- package/Server/Services/StatusPageService.ts +104 -45
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
- package/ServiceRoute.ts +4 -0
- package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
- package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
- package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
- package/Types/Monitor/MonitorStep.ts +32 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +39 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +166 -16
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +74 -31
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +66 -4
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +91 -43
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/ServiceRoute.js +1 -0
- package/build/dist/ServiceRoute.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +25 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
- 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
|