@oneuptime/common 14.0.6 → 14.0.7
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/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { compareCodePoints } from "../../../Types/Topology/TopologyTypeRules";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* compareCodePoints is how the Dashboard breaks the ties the server breaks
|
|
6
|
+
* with COLLATE "C" (code-point order on a UTF-8 database). It must equal
|
|
7
|
+
* JavaScript's `<` for every string made of characters up to U+FFFF, and
|
|
8
|
+
* order characters beyond U+FFFF (surrogate pairs) after U+E000-U+FFFF,
|
|
9
|
+
* which `<` does not.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function sign(value: number): number {
|
|
13
|
+
return value < 0 ? -1 : value > 0 ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* An independent reference: compare the strings' code points one by one. */
|
|
17
|
+
function referenceCompare(a: string, b: string): number {
|
|
18
|
+
const left: Array<number> = Array.from(a).map((char: string): number => {
|
|
19
|
+
return char.codePointAt(0)!;
|
|
20
|
+
});
|
|
21
|
+
const right: Array<number> = Array.from(b).map((char: string): number => {
|
|
22
|
+
return char.codePointAt(0)!;
|
|
23
|
+
});
|
|
24
|
+
const length: number = Math.min(left.length, right.length);
|
|
25
|
+
for (let index: number = 0; index < length; index++) {
|
|
26
|
+
if (left[index] !== right[index]) {
|
|
27
|
+
return left[index]! < right[index]! ? -1 : 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sign(left.length - right.length);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function codeUnitCompare(a: string, b: string): number {
|
|
34
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* A small deterministic generator, so a failure names its inputs. */
|
|
38
|
+
function makeRandom(seed: number): () => number {
|
|
39
|
+
let state: number = seed >>> 0;
|
|
40
|
+
return (): number => {
|
|
41
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
42
|
+
return state / 4294967296;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Characters from the ranges where the two orders meet or part ways. */
|
|
47
|
+
const ALPHABET: Array<string> = [
|
|
48
|
+
"a",
|
|
49
|
+
"b",
|
|
50
|
+
"z",
|
|
51
|
+
"0",
|
|
52
|
+
"-",
|
|
53
|
+
"é",
|
|
54
|
+
"中",
|
|
55
|
+
"",
|
|
56
|
+
"",
|
|
57
|
+
"",
|
|
58
|
+
"fi",
|
|
59
|
+
"�",
|
|
60
|
+
"",
|
|
61
|
+
"\u{10000}",
|
|
62
|
+
"\u{1F600}",
|
|
63
|
+
"\u{1F601}",
|
|
64
|
+
"\u{10FFFF}",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function randomString(random: () => number, alphabet: Array<string>): string {
|
|
68
|
+
const length: number = Math.floor(random() * 5);
|
|
69
|
+
let out: string = "";
|
|
70
|
+
for (let index: number = 0; index < length; index++) {
|
|
71
|
+
out += alphabet[Math.floor(random() * alphabet.length)]!;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("compareCodePoints", () => {
|
|
77
|
+
test("equal strings compare equal", () => {
|
|
78
|
+
expect(compareCodePoints("", "")).toBe(0);
|
|
79
|
+
expect(compareCodePoints("node-a", "node-a")).toBe(0);
|
|
80
|
+
expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("inside the Basic Multilingual Plane it orders exactly like `<`", () => {
|
|
84
|
+
const pairs: Array<[string, string]> = [
|
|
85
|
+
["a", "b"],
|
|
86
|
+
["node-a", "node-b"],
|
|
87
|
+
["node", "node-a"],
|
|
88
|
+
["", "a"],
|
|
89
|
+
["Z", "a"],
|
|
90
|
+
["a", "é"],
|
|
91
|
+
["", ""],
|
|
92
|
+
["", "�"],
|
|
93
|
+
["�", ""],
|
|
94
|
+
["0123456789abcdef", "0123456789abcdf0"],
|
|
95
|
+
];
|
|
96
|
+
for (const [a, b] of pairs) {
|
|
97
|
+
expect(compareCodePoints(a, b)).toBe(-1);
|
|
98
|
+
expect(compareCodePoints(b, a)).toBe(1);
|
|
99
|
+
expect(sign(compareCodePoints(a, b))).toBe(codeUnitCompare(a, b));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a character beyond U+FFFF sorts after U+E000..U+FFFF, where `<` puts it before", () => {
|
|
104
|
+
for (const high of ["", "", "�", ""]) {
|
|
105
|
+
for (const astral of ["\u{10000}", "\u{1F600}", "\u{10FFFF}"]) {
|
|
106
|
+
expect(compareCodePoints(high, astral)).toBe(-1);
|
|
107
|
+
expect(compareCodePoints(astral, high)).toBe(1);
|
|
108
|
+
expect(compareCodePoints(`node-${high}`, `node-${astral}`)).toBe(-1);
|
|
109
|
+
expect(compareCodePoints(`node-${astral}x`, `node-${high}`)).toBe(1);
|
|
110
|
+
/* The order `<` gives, which is what this exists to correct. */
|
|
111
|
+
expect(codeUnitCompare(high, astral)).toBe(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a character beyond U+FFFF still sorts after everything below U+D800", () => {
|
|
117
|
+
expect(compareCodePoints("", "\u{10000}")).toBe(-1);
|
|
118
|
+
expect(compareCodePoints("z", "\u{1F600}")).toBe(-1);
|
|
119
|
+
expect(compareCodePoints("\u{1F600}", "z")).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("two characters beyond U+FFFF compare by code point, high or low half", () => {
|
|
123
|
+
/* Different high surrogates (U+10000 is D800 DC00, U+1F600 is D83D DE00). */
|
|
124
|
+
expect(compareCodePoints("\u{10000}", "\u{1F600}")).toBe(-1);
|
|
125
|
+
/* Same high surrogate, different low surrogates. */
|
|
126
|
+
expect(compareCodePoints("\u{1F600}", "\u{1F601}")).toBe(-1);
|
|
127
|
+
expect(compareCodePoints("x\u{1F601}", "x\u{1F600}")).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a prefix sorts first", () => {
|
|
131
|
+
expect(compareCodePoints("node", "node-\u{1F600}")).toBe(-1);
|
|
132
|
+
expect(compareCodePoints("node-\u{1F600}", "node")).toBe(1);
|
|
133
|
+
expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(-1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("agrees with a code-point reference on thousands of mixed strings", () => {
|
|
137
|
+
const random: () => number = makeRandom(3973);
|
|
138
|
+
let disagreementsWithCodeUnits: number = 0;
|
|
139
|
+
for (let round: number = 0; round < 20000; round++) {
|
|
140
|
+
const a: string = randomString(random, ALPHABET);
|
|
141
|
+
const b: string = randomString(random, ALPHABET);
|
|
142
|
+
const expected: number = referenceCompare(a, b);
|
|
143
|
+
if (sign(compareCodePoints(a, b)) !== expected) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`compareCodePoints(${JSON.stringify(a)}, ${JSON.stringify(b)}) should be ${expected}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (codeUnitCompare(a, b) !== expected) {
|
|
149
|
+
disagreementsWithCodeUnits++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/* The inputs really did reach the cases where `<` is wrong. */
|
|
153
|
+
expect(disagreementsWithCodeUnits).toBeGreaterThan(100);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('sorts a list the way COLLATE "C" would', () => {
|
|
157
|
+
const keys: Array<string> = [
|
|
158
|
+
"node-\u{1F600}",
|
|
159
|
+
"node-�",
|
|
160
|
+
"node-b",
|
|
161
|
+
"node-",
|
|
162
|
+
"node-a",
|
|
163
|
+
"node-\u{10000}",
|
|
164
|
+
"node",
|
|
165
|
+
];
|
|
166
|
+
expect([...keys].sort(compareCodePoints)).toEqual([
|
|
167
|
+
"node",
|
|
168
|
+
"node-a",
|
|
169
|
+
"node-b",
|
|
170
|
+
"node-",
|
|
171
|
+
"node-�",
|
|
172
|
+
"node-\u{10000}",
|
|
173
|
+
"node-\u{1F600}",
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "@jest/globals";
|
|
2
2
|
import IncidentFromAlerts, {
|
|
3
|
+
AlertForAcknowledgement,
|
|
3
4
|
AlertForIncidentPrefill,
|
|
5
|
+
AlertsToAcknowledge,
|
|
6
|
+
AlertStateForAcknowledgement,
|
|
4
7
|
INCIDENT_PREFILL_RESOURCE_KEYS,
|
|
5
8
|
IncidentPrefillFromAlerts,
|
|
6
9
|
NamedResource,
|
|
@@ -694,3 +697,302 @@ describe("IncidentFromAlerts", () => {
|
|
|
694
697
|
});
|
|
695
698
|
});
|
|
696
699
|
});
|
|
700
|
+
|
|
701
|
+
/*
|
|
702
|
+
* Which alerts the create page offers to acknowledge as the incident is
|
|
703
|
+
* declared. States are compared by order, as the server does: anything at or
|
|
704
|
+
* after the project's Acknowledged state (Resolved, or a custom state in
|
|
705
|
+
* between) has been acknowledged already. An alert whose state cannot be
|
|
706
|
+
* placed is offered - the server skips it if it turns out to be acknowledged.
|
|
707
|
+
*/
|
|
708
|
+
describe("IncidentFromAlerts.getAlertsToAcknowledge", () => {
|
|
709
|
+
const CREATED: AlertStateForAcknowledgement = {
|
|
710
|
+
id: "state-created",
|
|
711
|
+
order: 1,
|
|
712
|
+
isAcknowledgedState: false,
|
|
713
|
+
};
|
|
714
|
+
const ACKNOWLEDGED: AlertStateForAcknowledgement = {
|
|
715
|
+
id: "state-acknowledged",
|
|
716
|
+
order: 2,
|
|
717
|
+
isAcknowledgedState: true,
|
|
718
|
+
};
|
|
719
|
+
const RESOLVED: AlertStateForAcknowledgement = {
|
|
720
|
+
id: "state-resolved",
|
|
721
|
+
order: 3,
|
|
722
|
+
isAcknowledgedState: false,
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const DEFAULT_ALERT_STATES: Array<AlertStateForAcknowledgement> = [
|
|
726
|
+
CREATED,
|
|
727
|
+
ACKNOWLEDGED,
|
|
728
|
+
RESOLVED,
|
|
729
|
+
];
|
|
730
|
+
|
|
731
|
+
type AlertInStateFunction = (
|
|
732
|
+
id: string,
|
|
733
|
+
currentAlertStateId?: string | undefined,
|
|
734
|
+
) => AlertForAcknowledgement;
|
|
735
|
+
|
|
736
|
+
const alertIn: AlertInStateFunction = (
|
|
737
|
+
id: string,
|
|
738
|
+
currentAlertStateId?: string | undefined,
|
|
739
|
+
): AlertForAcknowledgement => {
|
|
740
|
+
return { id: id, currentAlertStateId: currentAlertStateId };
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
type GetFunction = (
|
|
744
|
+
alerts: Array<AlertForAcknowledgement>,
|
|
745
|
+
alertStates?: Array<AlertStateForAcknowledgement>,
|
|
746
|
+
) => AlertsToAcknowledge | null;
|
|
747
|
+
|
|
748
|
+
const get: GetFunction = (
|
|
749
|
+
alerts: Array<AlertForAcknowledgement>,
|
|
750
|
+
alertStates?: Array<AlertStateForAcknowledgement>,
|
|
751
|
+
): AlertsToAcknowledge | null => {
|
|
752
|
+
return IncidentFromAlerts.getAlertsToAcknowledge({
|
|
753
|
+
alerts: alerts,
|
|
754
|
+
alertStates: alertStates || DEFAULT_ALERT_STATES,
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
describe("when nothing could be acknowledged", () => {
|
|
759
|
+
test("is null when the project has no alert states at all", () => {
|
|
760
|
+
expect(get([alertIn("a1", CREATED.id)], [])).toBeNull();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("is null when no state is the Acknowledged state", () => {
|
|
764
|
+
expect(
|
|
765
|
+
get(
|
|
766
|
+
[alertIn("a1", CREATED.id)],
|
|
767
|
+
[CREATED, { ...ACKNOWLEDGED, isAcknowledgedState: false }, RESOLVED],
|
|
768
|
+
),
|
|
769
|
+
).toBeNull();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("is null when the Acknowledged flag is missing rather than false", () => {
|
|
773
|
+
expect(
|
|
774
|
+
get(
|
|
775
|
+
[alertIn("a1", CREATED.id)],
|
|
776
|
+
[
|
|
777
|
+
CREATED,
|
|
778
|
+
{ id: ACKNOWLEDGED.id, order: ACKNOWLEDGED.order },
|
|
779
|
+
RESOLVED,
|
|
780
|
+
],
|
|
781
|
+
),
|
|
782
|
+
).toBeNull();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("is null when the Acknowledged state has no order to compare by", () => {
|
|
786
|
+
expect(
|
|
787
|
+
get(
|
|
788
|
+
[alertIn("a1", CREATED.id)],
|
|
789
|
+
[CREATED, { ...ACKNOWLEDGED, order: undefined }, RESOLVED],
|
|
790
|
+
),
|
|
791
|
+
).toBeNull();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("is null without an Acknowledged state even when there are no alerts", () => {
|
|
795
|
+
expect(get([], [CREATED, RESOLVED])).toBeNull();
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
describe("comparing by order", () => {
|
|
800
|
+
test("offers an alert whose state comes before Acknowledged", () => {
|
|
801
|
+
expect(get([alertIn("a1", CREATED.id)])).toEqual({
|
|
802
|
+
alertIds: ["a1"],
|
|
803
|
+
alreadyAcknowledgedCount: 0,
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("leaves out an alert that is in the Acknowledged state itself", () => {
|
|
808
|
+
expect(get([alertIn("a1", ACKNOWLEDGED.id)])).toEqual({
|
|
809
|
+
alertIds: [],
|
|
810
|
+
alreadyAcknowledgedCount: 1,
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("leaves out an alert that is resolved", () => {
|
|
815
|
+
expect(get([alertIn("a1", RESOLVED.id)])).toEqual({
|
|
816
|
+
alertIds: [],
|
|
817
|
+
alreadyAcknowledgedCount: 1,
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("treats a custom state between Acknowledged and Resolved as acknowledged already", () => {
|
|
822
|
+
const investigating: AlertStateForAcknowledgement = {
|
|
823
|
+
id: "state-investigating",
|
|
824
|
+
order: 3,
|
|
825
|
+
};
|
|
826
|
+
const resolved: AlertStateForAcknowledgement = {
|
|
827
|
+
id: "state-resolved",
|
|
828
|
+
order: 4,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
expect(
|
|
832
|
+
get(
|
|
833
|
+
[alertIn("a1", investigating.id), alertIn("a2", resolved.id)],
|
|
834
|
+
[CREATED, ACKNOWLEDGED, investigating, resolved],
|
|
835
|
+
),
|
|
836
|
+
).toEqual({ alertIds: [], alreadyAcknowledgedCount: 2 });
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("offers an alert in a custom state before Acknowledged", () => {
|
|
840
|
+
const triaged: AlertStateForAcknowledgement = {
|
|
841
|
+
id: "state-triaged",
|
|
842
|
+
order: 2,
|
|
843
|
+
};
|
|
844
|
+
const acknowledged: AlertStateForAcknowledgement = {
|
|
845
|
+
id: "state-acknowledged",
|
|
846
|
+
order: 3,
|
|
847
|
+
isAcknowledgedState: true,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
expect(
|
|
851
|
+
get(
|
|
852
|
+
[alertIn("a1", triaged.id), alertIn("a2", CREATED.id)],
|
|
853
|
+
[CREATED, triaged, acknowledged, { ...RESOLVED, order: 4 }],
|
|
854
|
+
),
|
|
855
|
+
).toEqual({ alertIds: ["a1", "a2"], alreadyAcknowledgedCount: 0 });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("compares by order, not by where the state sits in the list", () => {
|
|
859
|
+
// Listed newest-first: the list position must not decide anything.
|
|
860
|
+
const reversed: Array<AlertStateForAcknowledgement> = [
|
|
861
|
+
RESOLVED,
|
|
862
|
+
ACKNOWLEDGED,
|
|
863
|
+
CREATED,
|
|
864
|
+
];
|
|
865
|
+
|
|
866
|
+
expect(
|
|
867
|
+
get(
|
|
868
|
+
[
|
|
869
|
+
alertIn("a1", RESOLVED.id),
|
|
870
|
+
alertIn("a2", CREATED.id),
|
|
871
|
+
alertIn("a3", ACKNOWLEDGED.id),
|
|
872
|
+
],
|
|
873
|
+
reversed,
|
|
874
|
+
),
|
|
875
|
+
).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("an Acknowledged order of 0 is still an order", () => {
|
|
879
|
+
const acknowledgedAtZero: AlertStateForAcknowledgement = {
|
|
880
|
+
id: "state-acknowledged",
|
|
881
|
+
order: 0,
|
|
882
|
+
isAcknowledgedState: true,
|
|
883
|
+
};
|
|
884
|
+
const before: AlertStateForAcknowledgement = {
|
|
885
|
+
id: "state-before",
|
|
886
|
+
order: -1,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
expect(
|
|
890
|
+
get(
|
|
891
|
+
[
|
|
892
|
+
alertIn("a1", acknowledgedAtZero.id),
|
|
893
|
+
alertIn("a2", before.id),
|
|
894
|
+
alertIn("a3", RESOLVED.id),
|
|
895
|
+
],
|
|
896
|
+
[before, acknowledgedAtZero, RESOLVED],
|
|
897
|
+
),
|
|
898
|
+
).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
describe("alerts whose state cannot be placed", () => {
|
|
903
|
+
test("offers an alert whose current state is not one of the project's states", () => {
|
|
904
|
+
expect(get([alertIn("a1", "state-from-another-project")])).toEqual({
|
|
905
|
+
alertIds: ["a1"],
|
|
906
|
+
alreadyAcknowledgedCount: 0,
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test("offers an alert with no current state id", () => {
|
|
911
|
+
expect(get([alertIn("a1", undefined), alertIn("a2", "")])).toEqual({
|
|
912
|
+
alertIds: ["a1", "a2"],
|
|
913
|
+
alreadyAcknowledgedCount: 0,
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("offers an alert whose current state has no order", () => {
|
|
918
|
+
const unordered: AlertStateForAcknowledgement = {
|
|
919
|
+
id: "state-unordered",
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
expect(
|
|
923
|
+
get(
|
|
924
|
+
[alertIn("a1", unordered.id)],
|
|
925
|
+
[CREATED, ACKNOWLEDGED, RESOLVED, unordered],
|
|
926
|
+
),
|
|
927
|
+
).toEqual({ alertIds: ["a1"], alreadyAcknowledgedCount: 0 });
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("matches state ids case-insensitively and ignoring surrounding spaces", () => {
|
|
932
|
+
const upperAcknowledged: AlertStateForAcknowledgement = {
|
|
933
|
+
id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000002",
|
|
934
|
+
order: 2,
|
|
935
|
+
isAcknowledgedState: true,
|
|
936
|
+
};
|
|
937
|
+
const upperCreated: AlertStateForAcknowledgement = {
|
|
938
|
+
id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000001",
|
|
939
|
+
order: 1,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
expect(
|
|
943
|
+
get(
|
|
944
|
+
[
|
|
945
|
+
alertIn("a1", " aaaaaaaa-aaaa-4aaa-8aaa-000000000002 "),
|
|
946
|
+
alertIn("a2", "aaaaaaaa-aaaa-4aaa-8aaa-000000000001"),
|
|
947
|
+
],
|
|
948
|
+
[upperCreated, upperAcknowledged],
|
|
949
|
+
),
|
|
950
|
+
).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 1 });
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
test("keeps the alerts in the order they were given and counts the rest", () => {
|
|
954
|
+
const result: AlertsToAcknowledge | null = get([
|
|
955
|
+
alertIn("a5", CREATED.id),
|
|
956
|
+
alertIn("a1", ACKNOWLEDGED.id),
|
|
957
|
+
alertIn("a4", undefined),
|
|
958
|
+
alertIn("a2", RESOLVED.id),
|
|
959
|
+
alertIn("a3", CREATED.id),
|
|
960
|
+
alertIn("a6", RESOLVED.id),
|
|
961
|
+
]);
|
|
962
|
+
|
|
963
|
+
expect(result).toEqual({
|
|
964
|
+
alertIds: ["a5", "a4", "a3"],
|
|
965
|
+
alreadyAcknowledgedCount: 3,
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test("returns the alert ids exactly as given", () => {
|
|
970
|
+
expect(get([alertIn("ALERT-One", CREATED.id)])!.alertIds).toEqual([
|
|
971
|
+
"ALERT-One",
|
|
972
|
+
]);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
test("is empty, not null, when there are no alerts", () => {
|
|
976
|
+
expect(get([])).toEqual({ alertIds: [], alreadyAcknowledgedCount: 0 });
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("does not change its input", () => {
|
|
980
|
+
const alerts: Array<AlertForAcknowledgement> = [
|
|
981
|
+
alertIn("a1", CREATED.id),
|
|
982
|
+
alertIn("a2", ACKNOWLEDGED.id),
|
|
983
|
+
];
|
|
984
|
+
const alertStates: Array<AlertStateForAcknowledgement> = [
|
|
985
|
+
RESOLVED,
|
|
986
|
+
CREATED,
|
|
987
|
+
ACKNOWLEDGED,
|
|
988
|
+
];
|
|
989
|
+
|
|
990
|
+
get(alerts, alertStates);
|
|
991
|
+
|
|
992
|
+
expect(alerts).toEqual([
|
|
993
|
+
{ id: "a1", currentAlertStateId: CREATED.id },
|
|
994
|
+
{ id: "a2", currentAlertStateId: ACKNOWLEDGED.id },
|
|
995
|
+
]);
|
|
996
|
+
expect(alertStates).toEqual([RESOLVED, CREATED, ACKNOWLEDGED]);
|
|
997
|
+
});
|
|
998
|
+
});
|
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export const INCIDENT_ALERT_IDS_TO_LINK_KEY: string = "alertIdsToLink";
|
|
14
14
|
|
|
15
|
+
/*
|
|
16
|
+
* Declaring an incident from alerts does not, on its own, change the alerts:
|
|
17
|
+
* they keep escalating until somebody acknowledges them. Sending
|
|
18
|
+
* `miscDataProps[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY] = true` along with
|
|
19
|
+
* INCIDENT_ALERT_IDS_TO_LINK_KEY also acknowledges them, as the declaring
|
|
20
|
+
* user, once they are linked - which is what stops their on-call escalation.
|
|
21
|
+
* Alerts already acknowledged (or past it) are left as they are, and the
|
|
22
|
+
* caller must be allowed to change the state of every alert that is not.
|
|
23
|
+
* Off unless asked for.
|
|
24
|
+
*
|
|
25
|
+
* One exception: when the incident is declared straight into an
|
|
26
|
+
* acknowledged or resolved state and the project's linked-alert switches
|
|
27
|
+
* already move linked alerts along with it, those alerts are left to the
|
|
28
|
+
* switches (one writer per alert) - acknowledged or resolved as the switch
|
|
29
|
+
* does it, not credited to the declaring user.
|
|
30
|
+
*/
|
|
31
|
+
export const INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY: string =
|
|
32
|
+
"acknowledgeAlertsToLink";
|
|
33
|
+
|
|
15
34
|
/*
|
|
16
35
|
* The create-incident page reads the alerts to declare an incident from out of
|
|
17
36
|
* this query string parameter, as a comma separated list of alert ids.
|
|
@@ -54,3 +54,29 @@ export const getInventoryLivenessState: (
|
|
|
54
54
|
|
|
55
55
|
return InventoryLiveness.Stale;
|
|
56
56
|
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `getInventoryLivenessState` as a Postgres expression over the row aliased
|
|
60
|
+
* `tableAlias`, which must already be quoted (`"InventoryItem"`).
|
|
61
|
+
*
|
|
62
|
+
* The one SQL definition of the rule: the `inventoryStatus` virtual column
|
|
63
|
+
* selects and filters with it, and the Inventory Overview counts "Gone
|
|
64
|
+
* Quiet" with it, so the number on that tile and the rows the Stale facet
|
|
65
|
+
* returns cannot disagree about what stale means.
|
|
66
|
+
*
|
|
67
|
+
* Measured against the database clock. The function above floors elapsed
|
|
68
|
+
* minutes before applying inclusive thresholds — 30m59s is still Live and
|
|
69
|
+
* 24h00m59s is still Recent — and strict comparisons against the following
|
|
70
|
+
* minute preserve exactly those boundaries.
|
|
71
|
+
*/
|
|
72
|
+
export const getInventoryLivenessSql: (tableAlias: string) => string = (
|
|
73
|
+
tableAlias: string,
|
|
74
|
+
): string => {
|
|
75
|
+
return `CASE
|
|
76
|
+
WHEN ${tableAlias}."source" IS DISTINCT FROM '${EntitySource.Discovered}' THEN '${InventoryLiveness.NotTracked}'
|
|
77
|
+
WHEN ${tableAlias}."lastSeenAt" IS NULL THEN '${InventoryLiveness.Never}'
|
|
78
|
+
WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_LIVE_WINDOW_MINUTES + 1} minutes' THEN '${InventoryLiveness.Live}'
|
|
79
|
+
WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_STALE_AFTER_MINUTES + 1} minutes' THEN '${InventoryLiveness.Recent}'
|
|
80
|
+
ELSE '${InventoryLiveness.Stale}'
|
|
81
|
+
END`;
|
|
82
|
+
};
|