@oneuptime/common 10.2.15 → 10.2.17
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/Server/API/DashboardAPI.ts +0 -6
- package/Server/Services/DockerHostService.ts +91 -0
- package/Server/Services/IncidentService.ts +60 -23
- package/Server/Services/KubernetesClusterService.ts +92 -0
- package/Types/Dashboard/DashboardComponents/DashboardValueComponent.ts +15 -0
- package/Types/Dashboard/DashboardTemplates.ts +260 -971
- package/Types/Dashboard/DashboardVariable.ts +0 -8
- package/UI/Components/Charts/Utils/DataPoint.ts +0 -0
- package/Utils/Dashboard/Components/DashboardValueComponent.ts +36 -2
- package/Utils/ValueFormatter.ts +57 -0
- package/build/dist/Server/API/DashboardAPI.js +0 -3
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +73 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +55 -18
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +74 -0
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardValueComponent.js +14 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardValueComponent.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +240 -928
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +0 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js +31 -1
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +51 -0
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +1 -1
|
@@ -346,11 +346,6 @@ export default class DashboardAPI extends BaseAPI<
|
|
|
346
346
|
throw new BadDataException("attributeKey is required.");
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
const metricNameRaw: string | undefined =
|
|
350
|
-
req.body && (req.body["metricName"] as string);
|
|
351
|
-
const metricName: string | undefined =
|
|
352
|
-
metricNameRaw && metricNameRaw.trim() ? metricNameRaw : undefined;
|
|
353
|
-
|
|
354
349
|
const telemetryTypeRaw: string | undefined =
|
|
355
350
|
req.body && (req.body["telemetryType"] as string);
|
|
356
351
|
let telemetryType: TelemetryType = TelemetryType.Metric;
|
|
@@ -386,7 +381,6 @@ export default class DashboardAPI extends BaseAPI<
|
|
|
386
381
|
projectId: dashboard.projectId,
|
|
387
382
|
telemetryType,
|
|
388
383
|
attributeKey: attributeKey.trim(),
|
|
389
|
-
metricName,
|
|
390
384
|
});
|
|
391
385
|
|
|
392
386
|
return Response.sendJsonObjectResponse(req, res, {
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/DockerHost";
|
|
3
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
3
4
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
6
7
|
import OneUptimeDate from "../../Types/Date";
|
|
7
8
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
9
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
10
|
+
import logger from "../Utils/Logger";
|
|
9
11
|
import crypto from "crypto";
|
|
10
12
|
|
|
11
13
|
const LAST_SEEN_CACHE_NAMESPACE: string = "docker-host-last-seen";
|
|
12
14
|
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
13
15
|
|
|
16
|
+
const LABELS_APPLIED_CACHE_NAMESPACE: string = "docker-host-labels-applied";
|
|
17
|
+
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
18
|
+
|
|
14
19
|
export class Service extends DatabaseService<Model> {
|
|
15
20
|
public constructor() {
|
|
16
21
|
super(Model);
|
|
@@ -145,6 +150,83 @@ export class Service extends DatabaseService<Model> {
|
|
|
145
150
|
});
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Additively attach labels to a Docker host. Existing labels are
|
|
155
|
+
* never removed — manual labels set via the UI survive ingest. The
|
|
156
|
+
* set of labelIds passed in is fingerprinted and cached for 60s so
|
|
157
|
+
* the common case (steady-state collector pushing the same label
|
|
158
|
+
* set every batch) costs one in-memory lookup, not a join-table
|
|
159
|
+
* scan.
|
|
160
|
+
*/
|
|
161
|
+
@CaptureSpan()
|
|
162
|
+
public async attachLabels(data: {
|
|
163
|
+
dockerHostId: ObjectID;
|
|
164
|
+
labelIds: Array<ObjectID>;
|
|
165
|
+
}): Promise<void> {
|
|
166
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cacheKey: string = data.dockerHostId.toString();
|
|
171
|
+
const fingerprint: string = fingerprintLabelIds(data.labelIds);
|
|
172
|
+
const cached: string | null = await GlobalCache.getString(
|
|
173
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
174
|
+
cacheKey,
|
|
175
|
+
);
|
|
176
|
+
if (cached === fingerprint) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const dockerHostIdStr: string = data.dockerHostId.toString();
|
|
182
|
+
const existingLabels: Array<Label> = await this.getRepository()
|
|
183
|
+
.createQueryBuilder()
|
|
184
|
+
.relation(Model, "labels")
|
|
185
|
+
.of(dockerHostIdStr)
|
|
186
|
+
.loadMany();
|
|
187
|
+
|
|
188
|
+
const existingIds: Set<string> = new Set();
|
|
189
|
+
for (const lbl of existingLabels) {
|
|
190
|
+
const idStr: string | undefined = lbl._id?.toString();
|
|
191
|
+
if (idStr) {
|
|
192
|
+
existingIds.add(idStr);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const toAddIds: Array<string> = [];
|
|
197
|
+
const seen: Set<string> = new Set();
|
|
198
|
+
for (const id of data.labelIds) {
|
|
199
|
+
const idStr: string = id.toString();
|
|
200
|
+
if (existingIds.has(idStr) || seen.has(idStr)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
seen.add(idStr);
|
|
204
|
+
toAddIds.push(idStr);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (toAddIds.length > 0) {
|
|
208
|
+
await this.getRepository()
|
|
209
|
+
.createQueryBuilder()
|
|
210
|
+
.relation(Model, "labels")
|
|
211
|
+
.of(dockerHostIdStr)
|
|
212
|
+
.add(toAddIds);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await GlobalCache.setString(
|
|
216
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
217
|
+
cacheKey,
|
|
218
|
+
fingerprint,
|
|
219
|
+
{ expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS },
|
|
220
|
+
);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
logger.warn(
|
|
223
|
+
`DockerHostService.attachLabels failed for docker host ${data.dockerHostId.toString()}: ${
|
|
224
|
+
err instanceof Error ? err.message : String(err)
|
|
225
|
+
}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
148
230
|
@CaptureSpan()
|
|
149
231
|
public async markDisconnectedHosts(): Promise<void> {
|
|
150
232
|
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
|
@@ -183,4 +265,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
183
265
|
}
|
|
184
266
|
}
|
|
185
267
|
|
|
268
|
+
function fingerprintLabelIds(labelIds: Array<ObjectID>): string {
|
|
269
|
+
const sorted: Array<string> = labelIds
|
|
270
|
+
.map((id: ObjectID) => {
|
|
271
|
+
return id.toString();
|
|
272
|
+
})
|
|
273
|
+
.sort();
|
|
274
|
+
return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
|
|
275
|
+
}
|
|
276
|
+
|
|
186
277
|
export default new Service();
|
|
@@ -49,6 +49,7 @@ import Metric, {
|
|
|
49
49
|
import OneUptimeDate from "../../Types/Date";
|
|
50
50
|
import TelemetryUtil from "../Utils/Telemetry/Telemetry";
|
|
51
51
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
52
|
+
import NotEqual from "../../Types/BaseDatabase/NotEqual";
|
|
52
53
|
import IncidentFeedService from "./IncidentFeedService";
|
|
53
54
|
import IncidentSlaService from "./IncidentSlaService";
|
|
54
55
|
import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
|
|
@@ -2563,12 +2564,25 @@ ${incidentSeverity.name}
|
|
|
2563
2564
|
const firstIncidentStateTimeline: IncidentStateTimeline | undefined =
|
|
2564
2565
|
incidentStateTimelines[0];
|
|
2565
2566
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2567
|
+
/*
|
|
2568
|
+
* Delete the existing metrics for this incident so the time-varying
|
|
2569
|
+
* ones (TimeToAcknowledge / TimeToResolve / IncidentDuration /
|
|
2570
|
+
* TimeInState) get rewritten with the latest state-timeline values
|
|
2571
|
+
* on this refresh. IncidentCount is excluded from the delete: it is
|
|
2572
|
+
* a constant `value = 1` keyed by `serviceId + bucketTime` that
|
|
2573
|
+
* never changes. Re-emitting it across refreshes inflated the
|
|
2574
|
+
* 1-minute aggregating materialized view (`MetricItemAggMV1m_mv`),
|
|
2575
|
+
* because the MV trigger only fires on inserts — ALTER DELETE
|
|
2576
|
+
* mutations don't roll back the previously-accumulated
|
|
2577
|
+
* `sumState` / `countState`. That's why the Incident Dashboard
|
|
2578
|
+
* sum-of-IncidentCount widget read ~33% higher than the actual
|
|
2579
|
+
* unique-incident count.
|
|
2580
|
+
*/
|
|
2568
2581
|
await MetricService.deleteBy({
|
|
2569
2582
|
query: {
|
|
2570
2583
|
projectId: incident.projectId,
|
|
2571
2584
|
serviceId: data.incidentId,
|
|
2585
|
+
name: new NotEqual<string>(IncidentMetricType.IncidentCount),
|
|
2572
2586
|
},
|
|
2573
2587
|
props: {
|
|
2574
2588
|
isRoot: true,
|
|
@@ -2623,36 +2637,59 @@ ${incidentSeverity.name}
|
|
|
2623
2637
|
ownerTeamNames: ownerTeamNames.join(", "),
|
|
2624
2638
|
};
|
|
2625
2639
|
|
|
2626
|
-
|
|
2640
|
+
/*
|
|
2641
|
+
* Only emit IncidentCount on the very first refresh (i.e. when no
|
|
2642
|
+
* existing IncidentCount row is present for this serviceId). See
|
|
2643
|
+
* the delete comment above — emitting it on every refresh would
|
|
2644
|
+
* accumulate phantom `sumState` entries in the MV that ALTER
|
|
2645
|
+
* DELETE can't undo. By keeping the original row alive and never
|
|
2646
|
+
* re-emitting, the dashboard Sum stays equal to the true count of
|
|
2647
|
+
* distinct incidents.
|
|
2648
|
+
*/
|
|
2649
|
+
const existingIncidentCount: PositiveNumber = await MetricService.countBy({
|
|
2650
|
+
query: {
|
|
2651
|
+
projectId: incident.projectId,
|
|
2652
|
+
serviceId: data.incidentId,
|
|
2653
|
+
name: IncidentMetricType.IncidentCount,
|
|
2654
|
+
},
|
|
2655
|
+
skip: 0,
|
|
2656
|
+
limit: 1,
|
|
2657
|
+
props: {
|
|
2658
|
+
isRoot: true,
|
|
2659
|
+
},
|
|
2660
|
+
});
|
|
2627
2661
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
incidentCountMetric.
|
|
2636
|
-
|
|
2662
|
+
if (existingIncidentCount.toNumber() === 0) {
|
|
2663
|
+
const incidentCountMetric: Metric = new Metric();
|
|
2664
|
+
|
|
2665
|
+
incidentCountMetric.projectId = incident.projectId;
|
|
2666
|
+
incidentCountMetric.serviceId = incident.id!;
|
|
2667
|
+
incidentCountMetric.serviceType = ServiceType.Incident;
|
|
2668
|
+
incidentCountMetric.name = IncidentMetricType.IncidentCount;
|
|
2669
|
+
incidentCountMetric.value = 1;
|
|
2670
|
+
incidentCountMetric.attributes = { ...baseMetricAttributes };
|
|
2671
|
+
incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
|
|
2672
|
+
incidentCountMetric.attributes,
|
|
2673
|
+
);
|
|
2637
2674
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2675
|
+
incidentCountMetric.time = incidentStartsAt;
|
|
2676
|
+
incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
|
|
2677
|
+
incidentCountMetric.time,
|
|
2678
|
+
);
|
|
2679
|
+
incidentCountMetric.metricPointType = MetricPointType.Sum;
|
|
2680
|
+
incidentCountMetric.retentionDate = incidentMetricRetentionDate;
|
|
2644
2681
|
|
|
2645
|
-
|
|
2682
|
+
itemsToSave.push(incidentCountMetric);
|
|
2683
|
+
}
|
|
2646
2684
|
|
|
2647
|
-
//
|
|
2685
|
+
// Always register the metric type so it shows up in the type catalog.
|
|
2648
2686
|
const metricType: MetricType = new MetricType();
|
|
2649
|
-
metricType.name =
|
|
2687
|
+
metricType.name = IncidentMetricType.IncidentCount;
|
|
2650
2688
|
metricType.description = "Number of incidents created";
|
|
2651
2689
|
metricType.unit = "";
|
|
2652
2690
|
metricType.services = [];
|
|
2653
2691
|
|
|
2654
|
-
|
|
2655
|
-
metricTypesMap[incidentCountMetric.name] = metricType;
|
|
2692
|
+
metricTypesMap[IncidentMetricType.IncidentCount] = metricType;
|
|
2656
2693
|
|
|
2657
2694
|
// is the incident acknowledged?
|
|
2658
2695
|
const isIncidentAcknowledged: boolean = incidentStateTimelines.some(
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/KubernetesCluster";
|
|
3
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
3
4
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
6
7
|
import OneUptimeDate from "../../Types/Date";
|
|
7
8
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
9
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
10
|
+
import logger from "../Utils/Logger";
|
|
11
|
+
import crypto from "crypto";
|
|
9
12
|
|
|
10
13
|
const LAST_SEEN_CACHE_NAMESPACE: string = "k8s-cluster-last-seen";
|
|
11
14
|
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
12
15
|
|
|
16
|
+
const LABELS_APPLIED_CACHE_NAMESPACE: string = "k8s-cluster-labels-applied";
|
|
17
|
+
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
18
|
+
|
|
13
19
|
export class Service extends DatabaseService<Model> {
|
|
14
20
|
public constructor() {
|
|
15
21
|
super(Model);
|
|
@@ -116,6 +122,83 @@ export class Service extends DatabaseService<Model> {
|
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Additively attach labels to a Kubernetes cluster. Existing labels
|
|
127
|
+
* are never removed — manual labels set via the UI survive ingest.
|
|
128
|
+
* The set of labelIds passed in is fingerprinted and cached for 60s
|
|
129
|
+
* so the common case (steady-state collector pushing the same label
|
|
130
|
+
* set every batch) costs one in-memory lookup, not a join-table
|
|
131
|
+
* scan.
|
|
132
|
+
*/
|
|
133
|
+
@CaptureSpan()
|
|
134
|
+
public async attachLabels(data: {
|
|
135
|
+
kubernetesClusterId: ObjectID;
|
|
136
|
+
labelIds: Array<ObjectID>;
|
|
137
|
+
}): Promise<void> {
|
|
138
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const cacheKey: string = data.kubernetesClusterId.toString();
|
|
143
|
+
const fingerprint: string = fingerprintLabelIds(data.labelIds);
|
|
144
|
+
const cached: string | null = await GlobalCache.getString(
|
|
145
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
146
|
+
cacheKey,
|
|
147
|
+
);
|
|
148
|
+
if (cached === fingerprint) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const clusterIdStr: string = data.kubernetesClusterId.toString();
|
|
154
|
+
const existingLabels: Array<Label> = await this.getRepository()
|
|
155
|
+
.createQueryBuilder()
|
|
156
|
+
.relation(Model, "labels")
|
|
157
|
+
.of(clusterIdStr)
|
|
158
|
+
.loadMany();
|
|
159
|
+
|
|
160
|
+
const existingIds: Set<string> = new Set();
|
|
161
|
+
for (const lbl of existingLabels) {
|
|
162
|
+
const idStr: string | undefined = lbl._id?.toString();
|
|
163
|
+
if (idStr) {
|
|
164
|
+
existingIds.add(idStr);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const toAddIds: Array<string> = [];
|
|
169
|
+
const seen: Set<string> = new Set();
|
|
170
|
+
for (const id of data.labelIds) {
|
|
171
|
+
const idStr: string = id.toString();
|
|
172
|
+
if (existingIds.has(idStr) || seen.has(idStr)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seen.add(idStr);
|
|
176
|
+
toAddIds.push(idStr);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (toAddIds.length > 0) {
|
|
180
|
+
await this.getRepository()
|
|
181
|
+
.createQueryBuilder()
|
|
182
|
+
.relation(Model, "labels")
|
|
183
|
+
.of(clusterIdStr)
|
|
184
|
+
.add(toAddIds);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await GlobalCache.setString(
|
|
188
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
189
|
+
cacheKey,
|
|
190
|
+
fingerprint,
|
|
191
|
+
{ expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS },
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.warn(
|
|
195
|
+
`KubernetesClusterService.attachLabels failed for cluster ${data.kubernetesClusterId.toString()}: ${
|
|
196
|
+
err instanceof Error ? err.message : String(err)
|
|
197
|
+
}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
119
202
|
@CaptureSpan()
|
|
120
203
|
public async markDisconnectedClusters(): Promise<void> {
|
|
121
204
|
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
|
@@ -154,4 +237,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
154
237
|
}
|
|
155
238
|
}
|
|
156
239
|
|
|
240
|
+
function fingerprintLabelIds(labelIds: Array<ObjectID>): string {
|
|
241
|
+
const sorted: Array<string> = labelIds
|
|
242
|
+
.map((id: ObjectID) => {
|
|
243
|
+
return id.toString();
|
|
244
|
+
})
|
|
245
|
+
.sort();
|
|
246
|
+
return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
|
|
247
|
+
}
|
|
248
|
+
|
|
157
249
|
export default new Service();
|
|
@@ -3,6 +3,20 @@ import ObjectID from "../../ObjectID";
|
|
|
3
3
|
import DashboardComponentType from "../DashboardComponentType";
|
|
4
4
|
import BaseComponent from "./DashboardBaseComponent";
|
|
5
5
|
|
|
6
|
+
/*
|
|
7
|
+
* "Auto" defers to a metric-name heuristic in ValueFormatter
|
|
8
|
+
* (`isHigherWorseMetric`) — that lets templates skip the field unless
|
|
9
|
+
* they need to override. "HigherIsBetter" forces ↑ = green / ↓ = red
|
|
10
|
+
* (e.g. uptime, throughput, success rate). "HigherIsWorse" inverts so a
|
|
11
|
+
* rising incident count, error rate, latency, or restart count is shown
|
|
12
|
+
* in red.
|
|
13
|
+
*/
|
|
14
|
+
export enum DashboardValueTrendDirection {
|
|
15
|
+
Auto = "Auto",
|
|
16
|
+
HigherIsBetter = "HigherIsBetter",
|
|
17
|
+
HigherIsWorse = "HigherIsWorse",
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export default interface DashboardValueComponent extends BaseComponent {
|
|
7
21
|
componentType: DashboardComponentType.Value;
|
|
8
22
|
componentId: ObjectID;
|
|
@@ -11,5 +25,6 @@ export default interface DashboardValueComponent extends BaseComponent {
|
|
|
11
25
|
title: string;
|
|
12
26
|
warningThreshold?: number | undefined;
|
|
13
27
|
criticalThreshold?: number | undefined;
|
|
28
|
+
trendDirection?: DashboardValueTrendDirection | undefined;
|
|
14
29
|
};
|
|
15
30
|
}
|