@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
|
@@ -1,14 +1,70 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
RenderResult,
|
|
16
|
+
screen,
|
|
17
|
+
waitFor,
|
|
18
|
+
within,
|
|
19
|
+
} from "@testing-library/react";
|
|
4
20
|
import * as React from "react";
|
|
5
|
-
import
|
|
6
|
-
import InventoryItemRelationship from "../../../Models/DatabaseModels/InventoryItemRelationship";
|
|
7
|
-
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
8
|
-
import EntityRelationshipType from "../../../Types/Telemetry/EntityRelationshipType";
|
|
9
|
-
import EntityDetailPanel from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/EntityDetailPanel";
|
|
21
|
+
import type { SpyInstance } from "jest-mock";
|
|
10
22
|
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
11
23
|
|
|
24
|
+
/*
|
|
25
|
+
* The topology detail drawer. It opens on what the map already knows (the
|
|
26
|
+
* header renders at once), then asks the server for the entity's full row
|
|
27
|
+
* and its connections — classified, ordered and counted over the whole
|
|
28
|
+
* inventory — and pages further rows on "Show more". Pinned here against a
|
|
29
|
+
* fake Topology API behind the real request and decoding code:
|
|
30
|
+
*
|
|
31
|
+
* - the header before the data, a busy status while loading;
|
|
32
|
+
* - exact server totals ("Calls (N)", "N+" when the scan stopped early),
|
|
33
|
+
* server order, and paging that appends and moves focus;
|
|
34
|
+
* - connections to resources nothing reported as static text;
|
|
35
|
+
* - a key no item has any more, failures with an in-drawer retry;
|
|
36
|
+
* - never a row fetched for another entity or range;
|
|
37
|
+
* - best-effort links resolved once, from the full row;
|
|
38
|
+
* - where each row leads.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
42
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
43
|
+
|
|
44
|
+
// The arrow wrappers are load bearing: jest.mock is hoisted above the mocks.
|
|
45
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: {
|
|
49
|
+
post: (...args: Array<any>) => {
|
|
50
|
+
return postMock(...args);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
56
|
+
return {
|
|
57
|
+
__esModule: true,
|
|
58
|
+
default: {
|
|
59
|
+
getList: (...args: Array<any>) => {
|
|
60
|
+
return getListMock(...args);
|
|
61
|
+
},
|
|
62
|
+
getCommonHeaders: () => {
|
|
63
|
+
return { tenantid: "project-1" };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
12
68
|
jest.mock("../../../UI/Utils/Translation", () => {
|
|
13
69
|
return {
|
|
14
70
|
__esModule: true,
|
|
@@ -28,119 +84,546 @@ jest.mock("../../../UI/Components/SideOver/SideOver", () => {
|
|
|
28
84
|
return {
|
|
29
85
|
__esModule: true,
|
|
30
86
|
SideOverSize: { Small: "small" },
|
|
31
|
-
default: (props: {
|
|
32
|
-
|
|
87
|
+
default: (props: {
|
|
88
|
+
title: string;
|
|
89
|
+
description: string;
|
|
90
|
+
children: React.ReactNode;
|
|
91
|
+
}) => {
|
|
92
|
+
return (
|
|
93
|
+
<section aria-label={props.title}>
|
|
94
|
+
<p data-testid="drawer-description">{props.description}</p>
|
|
95
|
+
{props.children}
|
|
96
|
+
</section>
|
|
97
|
+
);
|
|
33
98
|
},
|
|
34
99
|
};
|
|
35
100
|
});
|
|
36
101
|
|
|
37
|
-
|
|
102
|
+
import EntityDetailPanel, {
|
|
103
|
+
ComponentProps,
|
|
104
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/EntityDetailPanel";
|
|
105
|
+
import { EntityDetailTarget } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
|
|
106
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
107
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
108
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
109
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
110
|
+
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
111
|
+
import {
|
|
112
|
+
TOPOLOGY_API_FORMAT_VERSION,
|
|
113
|
+
TopologyConnectionRowJSON,
|
|
114
|
+
TopologyConnectionSection,
|
|
115
|
+
TopologyConnectionSectionJSON,
|
|
116
|
+
TopologyEntityConnectionsResponseJSON,
|
|
117
|
+
TopologyEntityDetailJSON,
|
|
118
|
+
TopologyEntityResponseJSON,
|
|
119
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
120
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
121
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
122
|
+
|
|
123
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
124
|
+
"5b2f5b1c-0000-4000-8000-000000000001",
|
|
125
|
+
);
|
|
126
|
+
const SERVICE_ID: string = "5e7f0000-0000-4000-8000-000000000001";
|
|
127
|
+
const ITEM_ID: string = "1a1a0000-0000-4000-8000-000000000001";
|
|
128
|
+
const RANGE_START: Date = new Date("2026-09-26T10:00:00.000Z");
|
|
129
|
+
const LATER_RANGE_START: Date = new Date("2026-09-26T11:00:00.000Z");
|
|
130
|
+
|
|
131
|
+
interface PostRequest {
|
|
132
|
+
url: { toString: () => string };
|
|
133
|
+
data: JSONObject;
|
|
134
|
+
options: { signal?: AbortSignal };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type Answer = JSONObject | HTTPErrorResponse | Promise<JSONObject>;
|
|
138
|
+
|
|
139
|
+
interface Deferred {
|
|
140
|
+
promise: Promise<JSONObject>;
|
|
141
|
+
resolve: (value: JSONObject) => void;
|
|
142
|
+
reject: (error: unknown) => void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function deferred(): Deferred {
|
|
146
|
+
let resolve: (value: JSONObject) => void = () => {
|
|
147
|
+
return undefined;
|
|
148
|
+
};
|
|
149
|
+
let reject: (error: unknown) => void = () => {
|
|
150
|
+
return undefined;
|
|
151
|
+
};
|
|
152
|
+
const promise: Promise<JSONObject> = new Promise<JSONObject>(
|
|
153
|
+
(res: (value: JSONObject) => void, rej: (error: unknown) => void) => {
|
|
154
|
+
resolve = res;
|
|
155
|
+
reject = rej;
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
return { promise, resolve, reject };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function asJSON(value: unknown): JSONObject {
|
|
162
|
+
return JSON.parse(JSON.stringify(value)) as JSONObject;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function entityRow(
|
|
166
|
+
overrides: Partial<TopologyEntityDetailJSON> = {},
|
|
167
|
+
): TopologyEntityDetailJSON {
|
|
38
168
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
169
|
+
id: ITEM_ID,
|
|
170
|
+
key: "svc",
|
|
171
|
+
type: EntityType.Service,
|
|
172
|
+
name: "Checkout API",
|
|
173
|
+
source: "telemetry",
|
|
174
|
+
lastSeenAt: Date.parse("2026-09-26T10:14:00.000Z"),
|
|
175
|
+
firstSeenAt: Date.parse("2026-08-01T00:00:00.000Z"),
|
|
176
|
+
resourceType: "Service",
|
|
177
|
+
resourceId: SERVICE_ID,
|
|
178
|
+
identifyingAttributes: { "service.name": "Checkout API" },
|
|
179
|
+
descriptiveAttributes: { "telemetry.sdk.language": "nodejs" },
|
|
180
|
+
...overrides,
|
|
181
|
+
};
|
|
43
182
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
183
|
+
|
|
184
|
+
function connection(
|
|
185
|
+
overrides: Partial<TopologyConnectionRowJSON>,
|
|
186
|
+
): TopologyConnectionRowJSON {
|
|
187
|
+
return {
|
|
188
|
+
relationshipType: "depends-on",
|
|
189
|
+
direction: "out",
|
|
190
|
+
otherKey: "other",
|
|
191
|
+
otherKnown: true,
|
|
192
|
+
otherName: "Other",
|
|
193
|
+
otherType: EntityType.Service,
|
|
194
|
+
callCount: null,
|
|
195
|
+
errorCount: null,
|
|
196
|
+
avgDurationMs: null,
|
|
197
|
+
lastSeenAt: null,
|
|
198
|
+
...overrides,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sectionOf(
|
|
203
|
+
rows: Array<TopologyConnectionRowJSON>,
|
|
204
|
+
overrides: Partial<TopologyConnectionSectionJSON> = {},
|
|
205
|
+
): TopologyConnectionSectionJSON {
|
|
206
|
+
return {
|
|
207
|
+
total: rows.length,
|
|
208
|
+
unknownTotal: 0,
|
|
209
|
+
rows: rows,
|
|
210
|
+
nextOffset: null,
|
|
211
|
+
...overrides,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pod(index: number): TopologyConnectionRowJSON {
|
|
216
|
+
return connection({
|
|
217
|
+
relationshipType: "runs-on",
|
|
218
|
+
otherKey: `pod-${index}`,
|
|
219
|
+
otherName: `checkout-pod-${index}`,
|
|
220
|
+
otherType: EntityType.KubernetesPod,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pods(from: number, to: number): Array<TopologyConnectionRowJSON> {
|
|
225
|
+
const rows: Array<TopologyConnectionRowJSON> = [];
|
|
226
|
+
for (let index: number = from; index <= to; index++) {
|
|
227
|
+
rows.push(pod(index));
|
|
228
|
+
}
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const EMPTY: Record<TopologyConnectionSection, TopologyConnectionSectionJSON> =
|
|
61
233
|
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
):
|
|
75
|
-
|
|
234
|
+
calls: sectionOf([]),
|
|
235
|
+
calledBy: sectionOf([]),
|
|
236
|
+
runsOn: sectionOf([]),
|
|
237
|
+
related: sectionOf([]),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function entityResponse(
|
|
241
|
+
entity: TopologyEntityDetailJSON | null,
|
|
242
|
+
sections: Partial<
|
|
243
|
+
Record<TopologyConnectionSection, TopologyConnectionSectionJSON>
|
|
244
|
+
> = {},
|
|
245
|
+
isScanLimited: boolean = false,
|
|
246
|
+
): JSONObject {
|
|
247
|
+
const response: TopologyEntityResponseJSON = {
|
|
248
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
249
|
+
rangeStart: RANGE_START.toISOString(),
|
|
250
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
251
|
+
entity: entity,
|
|
252
|
+
sections: { ...EMPTY, ...sections },
|
|
253
|
+
isScanLimited: isScanLimited,
|
|
254
|
+
};
|
|
255
|
+
return asJSON(response);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function connectionsResponse(
|
|
259
|
+
section: TopologyConnectionSection,
|
|
260
|
+
connections: TopologyConnectionSectionJSON,
|
|
261
|
+
isScanLimited: boolean = false,
|
|
262
|
+
): JSONObject {
|
|
263
|
+
const response: TopologyEntityConnectionsResponseJSON = {
|
|
264
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
265
|
+
rangeStart: RANGE_START.toISOString(),
|
|
266
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
267
|
+
section: section,
|
|
268
|
+
connections: connections,
|
|
269
|
+
isScanLimited: isScanLimited,
|
|
270
|
+
};
|
|
271
|
+
return asJSON(response);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* The service the drawer usually shows: calls, callers and where it runs. */
|
|
275
|
+
function checkoutResponse(): JSONObject {
|
|
276
|
+
return entityResponse(entityRow(), {
|
|
277
|
+
calls: sectionOf([
|
|
278
|
+
connection({
|
|
279
|
+
otherKey: "db",
|
|
280
|
+
otherName: "Orders database",
|
|
281
|
+
otherType: EntityType.Database,
|
|
282
|
+
callCount: 100,
|
|
283
|
+
errorCount: 2,
|
|
284
|
+
avgDurationMs: 30,
|
|
285
|
+
}),
|
|
286
|
+
connection({
|
|
287
|
+
otherKey: "missing",
|
|
288
|
+
otherKnown: false,
|
|
289
|
+
otherName: null,
|
|
290
|
+
otherType: null,
|
|
291
|
+
}),
|
|
292
|
+
]),
|
|
293
|
+
calledBy: sectionOf([
|
|
294
|
+
connection({
|
|
295
|
+
direction: "in",
|
|
296
|
+
otherKey: "web",
|
|
297
|
+
otherName: "Web frontend",
|
|
298
|
+
}),
|
|
299
|
+
]),
|
|
300
|
+
runsOn: sectionOf(pods(1, 25), {
|
|
301
|
+
total: 40,
|
|
302
|
+
unknownTotal: 3,
|
|
303
|
+
nextOffset: 25,
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isConnectionsRequest(request: PostRequest): boolean {
|
|
309
|
+
return request.url.toString().endsWith("/entity/connections");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function requests(): Array<PostRequest> {
|
|
313
|
+
return postMock.mock.calls.map((call: Array<unknown>): PostRequest => {
|
|
314
|
+
return call[0] as PostRequest;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function entityRequests(): Array<PostRequest> {
|
|
319
|
+
return requests().filter((request: PostRequest): boolean => {
|
|
320
|
+
return !isConnectionsRequest(request);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* Answer each request by its SHAPE (which endpoint, which key), so tests pin
|
|
326
|
+
* what was asked for rather than the order requests were issued in.
|
|
327
|
+
*/
|
|
328
|
+
function serve(answer: (request: PostRequest) => Answer): void {
|
|
329
|
+
postMock.mockImplementation(async (...args: Array<unknown>) => {
|
|
330
|
+
const request: PostRequest = args[0] as PostRequest;
|
|
331
|
+
const result: Answer = answer(request);
|
|
332
|
+
if (result instanceof HTTPErrorResponse) {
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
return new HTTPResponse<JSONObject>(200, await result, {});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function serveEntity(response: JSONObject): void {
|
|
340
|
+
serve((request: PostRequest): Answer => {
|
|
341
|
+
if (isConnectionsRequest(request)) {
|
|
342
|
+
throw new Error("No page is served in this test.");
|
|
343
|
+
}
|
|
344
|
+
return response;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const CHECKOUT: EntityDetailTarget = {
|
|
349
|
+
entityKey: "svc",
|
|
350
|
+
entityType: EntityType.Service,
|
|
351
|
+
displayName: "Checkout API",
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
function renderPanel(overrides: Partial<ComponentProps> = {}): RenderResult {
|
|
355
|
+
return render(
|
|
76
356
|
<EntityDetailPanel
|
|
77
|
-
entity={
|
|
78
|
-
|
|
79
|
-
entityByKey={
|
|
80
|
-
new Map<string, InventoryItem>([
|
|
81
|
-
["api", API],
|
|
82
|
-
["db", DATABASE],
|
|
83
|
-
["web", WEB],
|
|
84
|
-
])
|
|
85
|
-
}
|
|
357
|
+
entity={CHECKOUT}
|
|
358
|
+
rangeStart={RANGE_START}
|
|
86
359
|
metricsWindowSeconds={60}
|
|
87
360
|
onClose={() => {
|
|
88
361
|
return undefined;
|
|
89
362
|
}}
|
|
90
|
-
|
|
91
|
-
options.onFocus ||
|
|
92
|
-
(() => {
|
|
93
|
-
return undefined;
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
onSelectEntity={options.onSelectEntity}
|
|
97
|
-
focusButtonLabel={options.focusButtonLabel}
|
|
363
|
+
{...overrides}
|
|
98
364
|
/>,
|
|
99
365
|
);
|
|
100
366
|
}
|
|
101
367
|
|
|
368
|
+
async function waitForConnections(): Promise<void> {
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
beforeEach(() => {
|
|
375
|
+
postMock.mockReset();
|
|
376
|
+
getListMock.mockReset();
|
|
377
|
+
getListMock.mockImplementation(async () => {
|
|
378
|
+
return { data: [], count: 0 };
|
|
379
|
+
});
|
|
380
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
381
|
+
});
|
|
382
|
+
|
|
102
383
|
afterEach(() => {
|
|
103
384
|
cleanup();
|
|
385
|
+
jest.restoreAllMocks();
|
|
104
386
|
});
|
|
105
387
|
|
|
106
|
-
describe("
|
|
107
|
-
test("
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
388
|
+
describe("opening the drawer", () => {
|
|
389
|
+
test("the header renders from the preview before the server answers", async () => {
|
|
390
|
+
const answer: Deferred = deferred();
|
|
391
|
+
serve((): Answer => {
|
|
392
|
+
return answer.promise;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
renderPanel();
|
|
396
|
+
|
|
397
|
+
expect(
|
|
398
|
+
screen.getByRole("region", { name: "Checkout API" }),
|
|
399
|
+
).toBeInTheDocument();
|
|
400
|
+
expect(screen.getByTestId("drawer-description")).toHaveTextContent(
|
|
401
|
+
"Service",
|
|
402
|
+
);
|
|
403
|
+
const status: HTMLElement = screen.getByRole("status");
|
|
404
|
+
expect(status).toHaveAttribute("aria-busy", "true");
|
|
405
|
+
expect(status).toHaveTextContent("Loading connections…");
|
|
406
|
+
expect(screen.queryByTestId("entity-detail-calls")).not.toBeInTheDocument();
|
|
407
|
+
expect(screen.queryByText("Inventory details")).not.toBeInTheDocument();
|
|
408
|
+
|
|
409
|
+
await act(async () => {
|
|
410
|
+
answer.resolve(checkoutResponse());
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
414
|
+
expect(screen.getByTestId("entity-detail-calls")).toBeInTheDocument();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("asks for the entity once, with the key, the known type and the map's range start", async () => {
|
|
418
|
+
serveEntity(checkoutResponse());
|
|
419
|
+
|
|
420
|
+
renderPanel();
|
|
421
|
+
await waitForConnections();
|
|
422
|
+
|
|
423
|
+
expect(entityRequests()).toHaveLength(1);
|
|
424
|
+
expect(entityRequests()[0]!.url.toString()).toMatch(
|
|
425
|
+
/\/telemetry\/topology\/entity$/,
|
|
112
426
|
);
|
|
113
|
-
expect(
|
|
114
|
-
|
|
427
|
+
expect(entityRequests()[0]!.data).toEqual({
|
|
428
|
+
rangeStart: RANGE_START.toISOString(),
|
|
429
|
+
entityKey: "svc",
|
|
430
|
+
entityType: EntityType.Service,
|
|
431
|
+
});
|
|
115
432
|
});
|
|
116
433
|
|
|
117
|
-
test("
|
|
118
|
-
|
|
434
|
+
test("a deep link that knows only the key shows the key until the row arrives", async () => {
|
|
435
|
+
const answer: Deferred = deferred();
|
|
436
|
+
serve((): Answer => {
|
|
437
|
+
return answer.promise;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
renderPanel({ entity: { entityKey: "pod-key" } });
|
|
441
|
+
|
|
442
|
+
expect(screen.getByRole("region", { name: "pod-key" })).toBeInTheDocument();
|
|
443
|
+
expect(entityRequests()[0]!.data).toEqual({
|
|
444
|
+
rangeStart: RANGE_START.toISOString(),
|
|
445
|
+
entityKey: "pod-key",
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await act(async () => {
|
|
449
|
+
answer.resolve(
|
|
450
|
+
entityResponse(
|
|
451
|
+
entityRow({
|
|
452
|
+
key: "pod-key",
|
|
453
|
+
type: EntityType.KubernetesPod,
|
|
454
|
+
name: "checkout-7d9f",
|
|
455
|
+
}),
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
119
460
|
expect(
|
|
120
|
-
screen.getByRole("
|
|
461
|
+
screen.getByRole("region", { name: "checkout-7d9f" }),
|
|
121
462
|
).toBeInTheDocument();
|
|
463
|
+
expect(screen.getByTestId("drawer-description")).toHaveTextContent(
|
|
464
|
+
"K8s Pod",
|
|
465
|
+
);
|
|
122
466
|
});
|
|
123
467
|
|
|
124
|
-
test("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
468
|
+
test("without a range start yet, nothing is requested and the drawer keeps loading", () => {
|
|
469
|
+
serveEntity(checkoutResponse());
|
|
470
|
+
|
|
471
|
+
renderPanel({ rangeStart: null });
|
|
472
|
+
|
|
473
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
474
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
475
|
+
"Loading connections…",
|
|
129
476
|
);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("closing the drawer mid-load cancels the request", async () => {
|
|
480
|
+
const answer: Deferred = deferred();
|
|
481
|
+
serve((): Answer => {
|
|
482
|
+
return answer.promise;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const view: RenderResult = renderPanel();
|
|
486
|
+
const signal: AbortSignal | undefined = entityRequests()[0]!.options.signal;
|
|
487
|
+
expect(signal?.aborted).toBe(false);
|
|
488
|
+
|
|
489
|
+
view.unmount();
|
|
490
|
+
|
|
491
|
+
expect(signal?.aborted).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("connection sections", () => {
|
|
496
|
+
test("headers carry the server's exact totals, not the rows it sent", async () => {
|
|
497
|
+
serveEntity(checkoutResponse());
|
|
498
|
+
|
|
499
|
+
renderPanel();
|
|
500
|
+
await waitForConnections();
|
|
501
|
+
|
|
502
|
+
expect(
|
|
503
|
+
within(screen.getByTestId("entity-detail-calls")).getByRole("heading"),
|
|
504
|
+
).toHaveTextContent("Calls (2)");
|
|
505
|
+
expect(
|
|
506
|
+
within(screen.getByTestId("entity-detail-called-by")).getByRole(
|
|
507
|
+
"heading",
|
|
508
|
+
),
|
|
509
|
+
).toHaveTextContent("Called by (1)");
|
|
510
|
+
expect(
|
|
511
|
+
within(screen.getByTestId("entity-detail-runs-on")).getByRole("heading"),
|
|
512
|
+
).toHaveTextContent("Runs on (40)");
|
|
513
|
+
expect(
|
|
514
|
+
within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
515
|
+
"listitem",
|
|
516
|
+
),
|
|
517
|
+
).toHaveLength(25);
|
|
518
|
+
expect(screen.getByText("Showing 25 of 40")).toBeInTheDocument();
|
|
519
|
+
expect(
|
|
520
|
+
screen.queryByTestId("entity-detail-related"),
|
|
521
|
+
).not.toBeInTheDocument();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("large totals are grouped, and a limited scan marks every total as a lower bound", async () => {
|
|
525
|
+
serveEntity(
|
|
526
|
+
entityResponse(
|
|
527
|
+
entityRow(),
|
|
528
|
+
{
|
|
529
|
+
related: sectionOf(pods(1, 25), {
|
|
530
|
+
total: 100_000,
|
|
531
|
+
nextOffset: 25,
|
|
532
|
+
}),
|
|
533
|
+
calledBy: sectionOf(
|
|
534
|
+
[connection({ direction: "in", otherKey: "web" })],
|
|
535
|
+
{ total: 1 },
|
|
536
|
+
),
|
|
537
|
+
},
|
|
538
|
+
true,
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
renderPanel();
|
|
543
|
+
await waitForConnections();
|
|
544
|
+
|
|
545
|
+
expect(
|
|
546
|
+
within(screen.getByTestId("entity-detail-related")).getByRole("heading"),
|
|
547
|
+
).toHaveTextContent("Related infrastructure (100,000+)");
|
|
548
|
+
expect(
|
|
549
|
+
within(screen.getByTestId("entity-detail-called-by")).getByRole(
|
|
550
|
+
"heading",
|
|
551
|
+
),
|
|
552
|
+
).toHaveTextContent("Called by (1+)");
|
|
553
|
+
expect(screen.getByTestId("entity-detail-scan-limited")).toHaveTextContent(
|
|
554
|
+
"totals are lower bounds",
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("an exact response says nothing about lower bounds", async () => {
|
|
559
|
+
serveEntity(checkoutResponse());
|
|
560
|
+
|
|
561
|
+
renderPanel();
|
|
562
|
+
await waitForConnections();
|
|
563
|
+
|
|
564
|
+
expect(
|
|
565
|
+
screen.queryByTestId("entity-detail-scan-limited"),
|
|
566
|
+
).not.toBeInTheDocument();
|
|
567
|
+
expect(screen.queryByText(/\+\)/)).not.toBeInTheDocument();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("rows keep the server's order", async () => {
|
|
571
|
+
serveEntity(
|
|
572
|
+
entityResponse(entityRow(), {
|
|
573
|
+
related: sectionOf([
|
|
574
|
+
connection({
|
|
575
|
+
relationshipType: "member-of",
|
|
576
|
+
otherKey: "z",
|
|
577
|
+
otherName: "Zulu",
|
|
578
|
+
otherType: EntityType.KubernetesNamespace,
|
|
579
|
+
}),
|
|
580
|
+
connection({
|
|
581
|
+
relationshipType: "member-of",
|
|
582
|
+
otherKey: "a",
|
|
583
|
+
otherName: "Alpha",
|
|
584
|
+
otherType: EntityType.KubernetesNamespace,
|
|
585
|
+
}),
|
|
586
|
+
]),
|
|
587
|
+
}),
|
|
133
588
|
);
|
|
134
|
-
|
|
589
|
+
|
|
590
|
+
renderPanel();
|
|
591
|
+
await waitForConnections();
|
|
592
|
+
|
|
593
|
+
const items: Array<HTMLElement> = within(
|
|
594
|
+
screen.getByTestId("entity-detail-related"),
|
|
595
|
+
).getAllByRole("listitem");
|
|
596
|
+
expect(items[0]).toHaveTextContent("Checkout API member of Zulu");
|
|
597
|
+
expect(items[1]).toHaveTextContent("Checkout API member of Alpha");
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("outbound rows read from the entity, inbound rows towards it, with call metrics", async () => {
|
|
601
|
+
serveEntity(checkoutResponse());
|
|
602
|
+
|
|
603
|
+
renderPanel();
|
|
604
|
+
await waitForConnections();
|
|
605
|
+
|
|
606
|
+
expect(
|
|
607
|
+
screen.getByText("Checkout API depends on Orders database"),
|
|
608
|
+
).toBeInTheDocument();
|
|
609
|
+
expect(
|
|
610
|
+
screen.getByText("Web frontend depends on Checkout API"),
|
|
611
|
+
).toBeInTheDocument();
|
|
135
612
|
expect(screen.getByText(/100\/min/)).toHaveTextContent("2.0% errors");
|
|
613
|
+
expect(screen.getByText(/100\/min/)).toHaveTextContent("avg 30ms");
|
|
614
|
+
expect(screen.getByText(/100\/min/)).toHaveTextContent("Database");
|
|
136
615
|
});
|
|
137
616
|
|
|
138
|
-
test("
|
|
617
|
+
test("connections to resources nothing reported are static text, never broken actions", async () => {
|
|
618
|
+
serveEntity(checkoutResponse());
|
|
619
|
+
|
|
139
620
|
renderPanel({
|
|
140
621
|
onSelectEntity: () => {
|
|
141
622
|
return undefined;
|
|
142
623
|
},
|
|
143
624
|
});
|
|
625
|
+
await waitForConnections();
|
|
626
|
+
|
|
144
627
|
expect(
|
|
145
628
|
screen.getByText("Checkout API depends on Undiscovered resource"),
|
|
146
629
|
).toBeInTheDocument();
|
|
@@ -149,180 +632,1416 @@ describe("topology connection details", () => {
|
|
|
149
632
|
).not.toBeInTheDocument();
|
|
150
633
|
});
|
|
151
634
|
|
|
152
|
-
test("
|
|
635
|
+
test("where it runs and related infrastructure say how many ends are no longer in inventory", async () => {
|
|
636
|
+
serveEntity(
|
|
637
|
+
entityResponse(entityRow(), {
|
|
638
|
+
runsOn: sectionOf(pods(1, 2), { total: 5, unknownTotal: 3 }),
|
|
639
|
+
related: sectionOf(
|
|
640
|
+
[
|
|
641
|
+
connection({
|
|
642
|
+
relationshipType: "member-of",
|
|
643
|
+
otherKey: "gone",
|
|
644
|
+
otherKnown: false,
|
|
645
|
+
otherName: null,
|
|
646
|
+
otherType: null,
|
|
647
|
+
}),
|
|
648
|
+
],
|
|
649
|
+
{ unknownTotal: 1 },
|
|
650
|
+
),
|
|
651
|
+
calls: sectionOf(
|
|
652
|
+
[
|
|
653
|
+
connection({
|
|
654
|
+
otherKey: "gone-dep",
|
|
655
|
+
otherKnown: false,
|
|
656
|
+
otherName: null,
|
|
657
|
+
}),
|
|
658
|
+
],
|
|
659
|
+
{ unknownTotal: 1 },
|
|
660
|
+
),
|
|
661
|
+
}),
|
|
662
|
+
);
|
|
663
|
+
|
|
153
664
|
renderPanel();
|
|
665
|
+
await waitForConnections();
|
|
666
|
+
|
|
154
667
|
expect(
|
|
155
|
-
screen.
|
|
668
|
+
within(screen.getByTestId("entity-detail-runs-on")).getByText(
|
|
669
|
+
"3 no longer in inventory",
|
|
670
|
+
),
|
|
156
671
|
).toBeInTheDocument();
|
|
157
672
|
expect(
|
|
158
|
-
screen.
|
|
159
|
-
|
|
160
|
-
|
|
673
|
+
within(screen.getByTestId("entity-detail-related")).getByText(
|
|
674
|
+
"1 no longer in inventory",
|
|
675
|
+
),
|
|
676
|
+
).toBeInTheDocument();
|
|
677
|
+
/* Calls name their unknown ends row by row instead. */
|
|
678
|
+
expect(
|
|
679
|
+
within(screen.getByTestId("entity-detail-calls")).queryByText(
|
|
680
|
+
/no longer in inventory/,
|
|
681
|
+
),
|
|
161
682
|
).not.toBeInTheDocument();
|
|
162
683
|
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("service map details", () => {
|
|
166
|
-
const SERVICE: InventoryItem = {
|
|
167
|
-
entityKey: "svc",
|
|
168
|
-
displayName: "api",
|
|
169
|
-
entityType: EntityType.Service,
|
|
170
|
-
descriptiveAttributes: { "telemetry.sdk.language": "nodejs" },
|
|
171
|
-
} as unknown as InventoryItem;
|
|
172
|
-
const POD: InventoryItem = {
|
|
173
|
-
entityKey: "pod",
|
|
174
|
-
displayName: "api-6d4f8b9c7d-x2k9p",
|
|
175
|
-
entityType: EntityType.KubernetesPod,
|
|
176
|
-
} as InventoryItem;
|
|
177
|
-
const POSTGRES: InventoryItem = {
|
|
178
|
-
entityKey: "pg",
|
|
179
|
-
displayName: "orders",
|
|
180
|
-
entityType: EntityType.Database,
|
|
181
|
-
identifyingAttributes: { "db.system.name": "postgresql" },
|
|
182
|
-
} as unknown as InventoryItem;
|
|
183
|
-
const byKey: Map<string, InventoryItem> = new Map<string, InventoryItem>([
|
|
184
|
-
["svc", SERVICE],
|
|
185
|
-
["pod", POD],
|
|
186
|
-
["pg", POSTGRES],
|
|
187
|
-
]);
|
|
188
|
-
const relationships: Array<InventoryItemRelationship> = [
|
|
189
|
-
{
|
|
190
|
-
fromEntityKey: "svc",
|
|
191
|
-
toEntityKey: "pg",
|
|
192
|
-
relationshipType: EntityRelationshipType.DependsOn,
|
|
193
|
-
callCount: 60,
|
|
194
|
-
errorCount: 0,
|
|
195
|
-
avgDurationMs: 4,
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
fromEntityKey: "svc",
|
|
199
|
-
toEntityKey: "pod",
|
|
200
|
-
relationshipType: EntityRelationshipType.RunsOn,
|
|
201
|
-
},
|
|
202
|
-
] as Array<InventoryItemRelationship>;
|
|
203
684
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
traffic={{
|
|
217
|
-
inbound: { calls: 0, errors: 0, avgDurationMs: null },
|
|
218
|
-
outbound: { calls: 60, errors: 0, avgDurationMs: 4 },
|
|
219
|
-
statusLabel: "Entry point",
|
|
220
|
-
statusColor: "#6366f1",
|
|
221
|
-
subtitle: "Service · Node.js",
|
|
222
|
-
}}
|
|
223
|
-
onClose={() => {
|
|
224
|
-
return undefined;
|
|
225
|
-
}}
|
|
226
|
-
onFocus={() => {
|
|
227
|
-
return undefined;
|
|
228
|
-
}}
|
|
229
|
-
onSelectEntity={options.onSelectEntity}
|
|
230
|
-
onOpenInfrastructure={options.onOpenInfrastructure}
|
|
231
|
-
/>,
|
|
685
|
+
test("a known resource without a name is unnamed, not undiscovered", async () => {
|
|
686
|
+
serveEntity(
|
|
687
|
+
entityResponse(entityRow(), {
|
|
688
|
+
related: sectionOf([
|
|
689
|
+
connection({
|
|
690
|
+
relationshipType: "member-of",
|
|
691
|
+
otherKey: "ns",
|
|
692
|
+
otherName: null,
|
|
693
|
+
otherType: EntityType.KubernetesNamespace,
|
|
694
|
+
}),
|
|
695
|
+
]),
|
|
696
|
+
}),
|
|
232
697
|
);
|
|
233
|
-
}
|
|
234
698
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
699
|
+
renderPanel({
|
|
700
|
+
onSelectEntity: () => {
|
|
701
|
+
return undefined;
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
await waitForConnections();
|
|
705
|
+
|
|
240
706
|
expect(
|
|
241
|
-
screen.
|
|
242
|
-
).toHaveTextContent("
|
|
243
|
-
expect(screen.getByText("Calls it made").parentElement).toHaveTextContent(
|
|
244
|
-
"60/min",
|
|
245
|
-
);
|
|
246
|
-
expect(screen.getByText("Calls it made").parentElement).toHaveTextContent(
|
|
247
|
-
"4ms",
|
|
248
|
-
);
|
|
707
|
+
screen.getByRole("button", { name: "View details for Unnamed resource" }),
|
|
708
|
+
).toHaveTextContent("Checkout API member of Unnamed resource");
|
|
249
709
|
});
|
|
250
710
|
|
|
251
|
-
test("
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
259
|
-
expect(screen.getByTestId("entity-detail-runs-on")).toHaveTextContent(
|
|
260
|
-
"api runs on api-6d4f8b9c7d-x2k9p",
|
|
711
|
+
test("a self-loop appears once, as an outbound row", async () => {
|
|
712
|
+
serveEntity(
|
|
713
|
+
entityResponse(entityRow(), {
|
|
714
|
+
calls: sectionOf([
|
|
715
|
+
connection({ otherKey: "svc", otherName: "Checkout API" }),
|
|
716
|
+
]),
|
|
717
|
+
}),
|
|
261
718
|
);
|
|
719
|
+
|
|
720
|
+
renderPanel();
|
|
721
|
+
await waitForConnections();
|
|
722
|
+
|
|
723
|
+
expect(
|
|
724
|
+
within(screen.getByTestId("entity-detail-calls")).getAllByRole(
|
|
725
|
+
"listitem",
|
|
726
|
+
),
|
|
727
|
+
).toHaveLength(1);
|
|
728
|
+
expect(
|
|
729
|
+
screen.getByText("Checkout API depends on Checkout API"),
|
|
730
|
+
).toBeInTheDocument();
|
|
262
731
|
expect(
|
|
263
732
|
screen.queryByTestId("entity-detail-called-by"),
|
|
264
733
|
).not.toBeInTheDocument();
|
|
265
734
|
});
|
|
266
735
|
|
|
267
|
-
test("
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
screen.getByRole("button", {
|
|
273
|
-
name: "View details for api-6d4f8b9c7d-x2k9p",
|
|
274
|
-
}),
|
|
275
|
-
);
|
|
276
|
-
expect(onOpenInfrastructure).toHaveBeenCalledWith("pod");
|
|
277
|
-
fireEvent.click(
|
|
278
|
-
screen.getByRole("button", { name: "View details for orders" }),
|
|
279
|
-
);
|
|
280
|
-
expect(onSelectEntity).toHaveBeenCalledWith("pg");
|
|
281
|
-
});
|
|
736
|
+
test("an entity with no connections in range says so", async () => {
|
|
737
|
+
serveEntity(entityResponse(entityRow()));
|
|
738
|
+
|
|
739
|
+
renderPanel();
|
|
740
|
+
await waitForConnections();
|
|
282
741
|
|
|
283
|
-
test("a resource lists the services running on it as related infrastructure", () => {
|
|
284
|
-
render(
|
|
285
|
-
<EntityDetailPanel
|
|
286
|
-
entity={POD}
|
|
287
|
-
relationships={relationships}
|
|
288
|
-
entityByKey={byKey}
|
|
289
|
-
metricsWindowSeconds={60}
|
|
290
|
-
onClose={() => {
|
|
291
|
-
return undefined;
|
|
292
|
-
}}
|
|
293
|
-
onFocus={() => {
|
|
294
|
-
return undefined;
|
|
295
|
-
}}
|
|
296
|
-
/>,
|
|
297
|
-
);
|
|
298
|
-
expect(screen.getByTestId("entity-detail-related")).toHaveTextContent(
|
|
299
|
-
"api runs on api-6d4f8b9c7d-x2k9p",
|
|
300
|
-
);
|
|
301
742
|
expect(
|
|
302
|
-
screen.
|
|
303
|
-
).
|
|
743
|
+
screen.getByText("No connections in the selected time range."),
|
|
744
|
+
).toBeInTheDocument();
|
|
304
745
|
});
|
|
746
|
+
});
|
|
305
747
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
748
|
+
describe("Show more", () => {
|
|
749
|
+
function servePaging(
|
|
750
|
+
page: (request: PostRequest) => Answer = (): Answer => {
|
|
751
|
+
return connectionsResponse(
|
|
752
|
+
"runsOn",
|
|
753
|
+
sectionOf(pods(26, 40), {
|
|
754
|
+
total: 40,
|
|
755
|
+
unknownTotal: 3,
|
|
756
|
+
nextOffset: null,
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
759
|
+
},
|
|
760
|
+
): void {
|
|
761
|
+
serve((request: PostRequest): Answer => {
|
|
762
|
+
return isConnectionsRequest(request) ? page(request) : checkoutResponse();
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
test("fetches the next page of that section, appends it, and moves focus to the first new row", async () => {
|
|
767
|
+
servePaging();
|
|
768
|
+
const onOpenInfrastructure: MockFunction = getJestMockFunction();
|
|
769
|
+
|
|
770
|
+
renderPanel({ onOpenInfrastructure });
|
|
771
|
+
await waitForConnections();
|
|
772
|
+
|
|
773
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
774
|
+
|
|
775
|
+
await waitFor(() => {
|
|
776
|
+
expect(
|
|
777
|
+
within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
778
|
+
"listitem",
|
|
779
|
+
),
|
|
780
|
+
).toHaveLength(40);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const pageRequests: Array<PostRequest> =
|
|
784
|
+
requests().filter(isConnectionsRequest);
|
|
785
|
+
expect(pageRequests).toHaveLength(1);
|
|
786
|
+
expect(pageRequests[0]!.url.toString()).toMatch(
|
|
787
|
+
/\/telemetry\/topology\/entity\/connections$/,
|
|
788
|
+
);
|
|
789
|
+
expect(pageRequests[0]!.data).toEqual({
|
|
790
|
+
rangeStart: RANGE_START.toISOString(),
|
|
791
|
+
entityKey: "svc",
|
|
792
|
+
entityType: EntityType.Service,
|
|
793
|
+
section: "runsOn",
|
|
794
|
+
offset: 25,
|
|
795
|
+
limit: 25,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
await waitFor(() => {
|
|
799
|
+
expect(document.activeElement).toBe(
|
|
800
|
+
screen.getByRole("button", {
|
|
801
|
+
name: "View details for checkout-pod-26",
|
|
802
|
+
}),
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
/* The last page reached the end: no more paging. */
|
|
806
|
+
expect(
|
|
807
|
+
screen.queryByRole("button", { name: "Show more: Runs on" }),
|
|
808
|
+
).not.toBeInTheDocument();
|
|
809
|
+
expect(screen.queryByText(/^Showing/)).not.toBeInTheDocument();
|
|
810
|
+
/* Only that section was fetched again; the entity was not. */
|
|
811
|
+
expect(entityRequests()).toHaveLength(1);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("when the first new row cannot be opened, focus lands on the row itself", async () => {
|
|
815
|
+
servePaging((): Answer => {
|
|
816
|
+
return connectionsResponse(
|
|
817
|
+
"runsOn",
|
|
818
|
+
sectionOf(
|
|
819
|
+
[
|
|
820
|
+
connection({
|
|
821
|
+
relationshipType: "runs-on",
|
|
822
|
+
otherKey: "gone-pod",
|
|
823
|
+
otherKnown: false,
|
|
824
|
+
otherName: null,
|
|
825
|
+
otherType: null,
|
|
826
|
+
}),
|
|
827
|
+
],
|
|
828
|
+
{ total: 40, unknownTotal: 3, nextOffset: 26 },
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
renderPanel({
|
|
834
|
+
onSelectEntity: () => {
|
|
835
|
+
return undefined;
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
await waitForConnections();
|
|
839
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
840
|
+
|
|
841
|
+
const row: HTMLElement = await screen.findByText(
|
|
842
|
+
"Checkout API runs on Undiscovered resource",
|
|
843
|
+
);
|
|
844
|
+
await waitFor(() => {
|
|
845
|
+
expect(document.activeElement).toBe(row.closest("li"));
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("keeps paging from the offset the server returned", async () => {
|
|
850
|
+
servePaging((request: PostRequest): Answer => {
|
|
851
|
+
const offset: number = request.data["offset"] as number;
|
|
852
|
+
return connectionsResponse(
|
|
853
|
+
"runsOn",
|
|
854
|
+
sectionOf(pods(offset + 1, offset + 5), {
|
|
855
|
+
total: 40,
|
|
856
|
+
nextOffset: offset + 5,
|
|
857
|
+
}),
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
renderPanel();
|
|
862
|
+
await waitForConnections();
|
|
863
|
+
|
|
864
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
865
|
+
await screen.findByText("Showing 30 of 40");
|
|
866
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
867
|
+
await screen.findByText("Showing 35 of 40");
|
|
868
|
+
|
|
869
|
+
expect(
|
|
870
|
+
requests()
|
|
871
|
+
.filter(isConnectionsRequest)
|
|
872
|
+
.map((request: PostRequest): unknown => {
|
|
873
|
+
return request.data["offset"];
|
|
874
|
+
}),
|
|
875
|
+
).toEqual([25, 30]);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("the button is busy while its page loads and cannot fire twice", async () => {
|
|
879
|
+
const page: Deferred = deferred();
|
|
880
|
+
servePaging((): Answer => {
|
|
881
|
+
return page.promise;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
renderPanel();
|
|
885
|
+
await waitForConnections();
|
|
886
|
+
|
|
887
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
888
|
+
name: "Show more: Runs on",
|
|
889
|
+
});
|
|
890
|
+
fireEvent.click(button);
|
|
891
|
+
await waitFor(() => {
|
|
892
|
+
expect(button).toBeDisabled();
|
|
893
|
+
});
|
|
894
|
+
expect(button).toHaveTextContent("Loading…");
|
|
895
|
+
fireEvent.click(button);
|
|
896
|
+
expect(requests().filter(isConnectionsRequest)).toHaveLength(1);
|
|
897
|
+
|
|
898
|
+
await act(async () => {
|
|
899
|
+
page.resolve(
|
|
900
|
+
connectionsResponse(
|
|
901
|
+
"runsOn",
|
|
902
|
+
sectionOf(pods(26, 30), { total: 40, nextOffset: 30 }),
|
|
903
|
+
),
|
|
904
|
+
);
|
|
905
|
+
});
|
|
906
|
+
expect(button).not.toBeDisabled();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("a failed page is reported in its section and can be retried", async () => {
|
|
910
|
+
let fail: boolean = true;
|
|
911
|
+
servePaging((): Answer => {
|
|
912
|
+
if (fail) {
|
|
913
|
+
return new HTTPErrorResponse(500, { message: "Database busy" }, {});
|
|
914
|
+
}
|
|
915
|
+
return connectionsResponse(
|
|
916
|
+
"runsOn",
|
|
917
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
918
|
+
);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
renderPanel();
|
|
922
|
+
await waitForConnections();
|
|
923
|
+
|
|
924
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
925
|
+
const alert: HTMLElement = await within(
|
|
926
|
+
screen.getByTestId("entity-detail-runs-on"),
|
|
927
|
+
).findByRole("alert");
|
|
928
|
+
expect(alert).toHaveTextContent("Could not load more connections.");
|
|
929
|
+
expect(alert).toHaveTextContent("Database busy");
|
|
930
|
+
/* What was already shown stays. */
|
|
931
|
+
expect(
|
|
932
|
+
within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
933
|
+
"listitem",
|
|
934
|
+
),
|
|
935
|
+
).toHaveLength(25);
|
|
936
|
+
|
|
937
|
+
fail = false;
|
|
938
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
939
|
+
await waitFor(() => {
|
|
940
|
+
expect(
|
|
941
|
+
within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
942
|
+
"listitem",
|
|
943
|
+
),
|
|
944
|
+
).toHaveLength(40);
|
|
945
|
+
});
|
|
946
|
+
expect(
|
|
947
|
+
within(screen.getByTestId("entity-detail-runs-on")).queryByRole("alert"),
|
|
948
|
+
).not.toBeInTheDocument();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("a page that reports a limited scan turns the totals into lower bounds", async () => {
|
|
952
|
+
servePaging((): Answer => {
|
|
953
|
+
return connectionsResponse(
|
|
954
|
+
"runsOn",
|
|
955
|
+
sectionOf(pods(26, 30), { total: 41, nextOffset: 30 }),
|
|
956
|
+
true,
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
renderPanel();
|
|
961
|
+
await waitForConnections();
|
|
962
|
+
|
|
963
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
964
|
+
|
|
965
|
+
await waitFor(() => {
|
|
966
|
+
expect(
|
|
967
|
+
within(screen.getByTestId("entity-detail-runs-on")).getByRole(
|
|
968
|
+
"heading",
|
|
969
|
+
),
|
|
970
|
+
).toHaveTextContent("Runs on (41+)");
|
|
971
|
+
});
|
|
972
|
+
expect(
|
|
973
|
+
within(screen.getByTestId("entity-detail-calls")).getByRole("heading"),
|
|
974
|
+
).toHaveTextContent("Calls (2+)");
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("a page that arrives after the drawer moved to another range is dropped", async () => {
|
|
978
|
+
const page: Deferred = deferred();
|
|
979
|
+
servePaging((): Answer => {
|
|
980
|
+
return page.promise;
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const view: RenderResult = renderPanel();
|
|
984
|
+
await waitForConnections();
|
|
985
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
986
|
+
const pageSignal: AbortSignal | undefined =
|
|
987
|
+
requests().filter(isConnectionsRequest)[0]!.options.signal;
|
|
988
|
+
|
|
989
|
+
view.rerender(
|
|
990
|
+
<EntityDetailPanel
|
|
991
|
+
entity={CHECKOUT}
|
|
992
|
+
rangeStart={LATER_RANGE_START}
|
|
993
|
+
metricsWindowSeconds={60}
|
|
994
|
+
onClose={() => {
|
|
995
|
+
return undefined;
|
|
996
|
+
}}
|
|
997
|
+
/>,
|
|
998
|
+
);
|
|
999
|
+
expect(pageSignal?.aborted).toBe(true);
|
|
1000
|
+
await waitForConnections();
|
|
1001
|
+
|
|
1002
|
+
await act(async () => {
|
|
1003
|
+
page.resolve(
|
|
1004
|
+
connectionsResponse(
|
|
1005
|
+
"runsOn",
|
|
1006
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
1007
|
+
),
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
expect(
|
|
1012
|
+
within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
1013
|
+
"listitem",
|
|
1014
|
+
),
|
|
1015
|
+
).toHaveLength(25);
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
describe("a list that changed while the user paged it", () => {
|
|
1020
|
+
/*
|
|
1021
|
+
* Paging is by offset over a ranking the server recomputes per request,
|
|
1022
|
+
* and the ranking moves while the drawer is open. The worked example:
|
|
1023
|
+
* pod-3 is pruned after the first page, so it now sorts last as unknown
|
|
1024
|
+
* and pod-26 moves up to rank 25 — the page at offset 25 starts at pod-27,
|
|
1025
|
+
* repeats pod-3 at its end, and pod-26 is never returned.
|
|
1026
|
+
*/
|
|
1027
|
+
function driftedPage(): JSONObject {
|
|
1028
|
+
return connectionsResponse(
|
|
1029
|
+
"runsOn",
|
|
1030
|
+
sectionOf(
|
|
1031
|
+
[
|
|
1032
|
+
...pods(27, 40),
|
|
1033
|
+
connection({
|
|
1034
|
+
relationshipType: "runs-on",
|
|
1035
|
+
otherKey: "pod-3",
|
|
1036
|
+
otherKnown: false,
|
|
1037
|
+
otherName: null,
|
|
1038
|
+
otherType: null,
|
|
1039
|
+
}),
|
|
1040
|
+
],
|
|
1041
|
+
{ total: 40, unknownTotal: 4, nextOffset: null },
|
|
1042
|
+
),
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function runsOnSection(): HTMLElement {
|
|
1047
|
+
return screen.getByTestId("entity-detail-runs-on");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function runsOnRows(): Array<HTMLElement> {
|
|
1051
|
+
return within(runsOnSection()).getAllByRole("listitem");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function changedNote(): HTMLElement | null {
|
|
1055
|
+
return screen.queryByTestId("entity-detail-runs-on-changed");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
test("a page that repeats rows and ends short of the total says so, and offers a reload", async () => {
|
|
1059
|
+
let reloaded: boolean = false;
|
|
1060
|
+
serve((request: PostRequest): Answer => {
|
|
1061
|
+
if (!isConnectionsRequest(request)) {
|
|
1062
|
+
return checkoutResponse();
|
|
1063
|
+
}
|
|
1064
|
+
if (request.data["offset"] === 0) {
|
|
1065
|
+
reloaded = true;
|
|
1066
|
+
return connectionsResponse(
|
|
1067
|
+
"runsOn",
|
|
1068
|
+
sectionOf(pods(1, 40), { total: 40, nextOffset: null }),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return driftedPage();
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
renderPanel({
|
|
1075
|
+
onSelectEntity: () => {
|
|
1076
|
+
return undefined;
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
await waitForConnections();
|
|
1080
|
+
expect(changedNote()).toBeNull();
|
|
1081
|
+
|
|
1082
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1083
|
+
|
|
1084
|
+
await waitFor(() => {
|
|
1085
|
+
expect(changedNote()).not.toBeNull();
|
|
1086
|
+
});
|
|
1087
|
+
expect(changedNote()).toHaveTextContent(
|
|
1088
|
+
"This list changed while you were browsing.",
|
|
1089
|
+
);
|
|
1090
|
+
/* 39 rows under "Runs on (40)", and no way to page on. */
|
|
1091
|
+
expect(runsOnRows()).toHaveLength(39);
|
|
1092
|
+
expect(within(runsOnSection()).getByRole("heading")).toHaveTextContent(
|
|
1093
|
+
"Runs on (40)",
|
|
1094
|
+
);
|
|
1095
|
+
expect(
|
|
1096
|
+
screen.queryByRole("button", { name: "Show more: Runs on" }),
|
|
1097
|
+
).not.toBeInTheDocument();
|
|
1098
|
+
/* The page still added rows: focus is on the first of them. */
|
|
1099
|
+
await waitFor(() => {
|
|
1100
|
+
expect(document.activeElement).toBe(
|
|
1101
|
+
screen.getByRole("button", {
|
|
1102
|
+
name: "View details for checkout-pod-27",
|
|
1103
|
+
}),
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
fireEvent.click(
|
|
1108
|
+
screen.getByRole("button", { name: "Reload list: Runs on" }),
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
await waitFor(() => {
|
|
1112
|
+
expect(changedNote()).toBeNull();
|
|
1113
|
+
});
|
|
1114
|
+
expect(reloaded).toBe(true);
|
|
1115
|
+
const reload: PostRequest = requests().filter(isConnectionsRequest)[1]!;
|
|
1116
|
+
/* From the start, as many rows as it showed plus a page. */
|
|
1117
|
+
expect(reload.data).toEqual({
|
|
1118
|
+
rangeStart: RANGE_START.toISOString(),
|
|
1119
|
+
entityKey: "svc",
|
|
1120
|
+
entityType: EntityType.Service,
|
|
1121
|
+
section: "runsOn",
|
|
1122
|
+
offset: 0,
|
|
1123
|
+
limit: 39 + 25,
|
|
1124
|
+
});
|
|
1125
|
+
expect(runsOnRows()).toHaveLength(40);
|
|
1126
|
+
expect(
|
|
1127
|
+
screen.getByRole("button", { name: "View details for checkout-pod-26" }),
|
|
1128
|
+
).toBeInTheDocument();
|
|
1129
|
+
/* The reload replaced the list: pod-3 is back in its place, once. */
|
|
1130
|
+
expect(runsOnRows()[2]).toHaveTextContent("checkout-pod-3");
|
|
1131
|
+
await waitFor(() => {
|
|
1132
|
+
expect(document.activeElement).toBe(
|
|
1133
|
+
screen.getByRole("button", {
|
|
1134
|
+
name: "View details for checkout-pod-1",
|
|
1135
|
+
}),
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
/* Only that section was asked for again; the entity was not. */
|
|
1139
|
+
expect(entityRequests()).toHaveLength(1);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test("a last page that ends short of its total says so even with nothing repeated", async () => {
|
|
1143
|
+
serve((request: PostRequest): Answer => {
|
|
1144
|
+
return isConnectionsRequest(request)
|
|
1145
|
+
? connectionsResponse(
|
|
1146
|
+
"runsOn",
|
|
1147
|
+
sectionOf(pods(27, 40), { total: 40, nextOffset: null }),
|
|
1148
|
+
)
|
|
1149
|
+
: checkoutResponse();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
renderPanel();
|
|
1153
|
+
await waitForConnections();
|
|
1154
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1155
|
+
|
|
1156
|
+
await waitFor(() => {
|
|
1157
|
+
expect(runsOnRows()).toHaveLength(39);
|
|
1158
|
+
});
|
|
1159
|
+
expect(changedNote()).toHaveTextContent(
|
|
1160
|
+
"This list changed while you were browsing.",
|
|
1161
|
+
);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
test("a page of nothing but repeated rows moves focus to the reload", async () => {
|
|
1165
|
+
serve((request: PostRequest): Answer => {
|
|
1166
|
+
return isConnectionsRequest(request)
|
|
1167
|
+
? connectionsResponse(
|
|
1168
|
+
"runsOn",
|
|
1169
|
+
sectionOf(pods(21, 25), { total: 40, nextOffset: 30 }),
|
|
1170
|
+
)
|
|
1171
|
+
: checkoutResponse();
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
renderPanel();
|
|
1175
|
+
await waitForConnections();
|
|
1176
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1177
|
+
|
|
1178
|
+
const reload: HTMLElement = await screen.findByRole("button", {
|
|
1179
|
+
name: "Reload list: Runs on",
|
|
1180
|
+
});
|
|
1181
|
+
await waitFor(() => {
|
|
1182
|
+
expect(document.activeElement).toBe(reload);
|
|
1183
|
+
});
|
|
1184
|
+
expect(runsOnRows()).toHaveLength(25);
|
|
1185
|
+
/* Paging can still go on; the note stays until the list is reloaded. */
|
|
1186
|
+
expect(
|
|
1187
|
+
screen.getByRole("button", { name: "Show more: Runs on" }),
|
|
1188
|
+
).toBeInTheDocument();
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("a page that lines up with what is shown says nothing", async () => {
|
|
1192
|
+
serve((request: PostRequest): Answer => {
|
|
1193
|
+
return isConnectionsRequest(request)
|
|
1194
|
+
? connectionsResponse(
|
|
1195
|
+
"runsOn",
|
|
1196
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
1197
|
+
)
|
|
1198
|
+
: checkoutResponse();
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
renderPanel();
|
|
1202
|
+
await waitForConnections();
|
|
1203
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1204
|
+
|
|
1205
|
+
await waitFor(() => {
|
|
1206
|
+
expect(runsOnRows()).toHaveLength(40);
|
|
1207
|
+
});
|
|
1208
|
+
expect(changedNote()).toBeNull();
|
|
1209
|
+
expect(
|
|
1210
|
+
screen.queryByRole("button", { name: /Reload list/ }),
|
|
1211
|
+
).not.toBeInTheDocument();
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
test("a failed reload keeps the list and the offer to reload it", async () => {
|
|
1215
|
+
let failReload: boolean = true;
|
|
1216
|
+
serve((request: PostRequest): Answer => {
|
|
1217
|
+
if (!isConnectionsRequest(request)) {
|
|
1218
|
+
return checkoutResponse();
|
|
1219
|
+
}
|
|
1220
|
+
if (request.data["offset"] === 0) {
|
|
1221
|
+
return failReload
|
|
1222
|
+
? new HTTPErrorResponse(500, { message: "Database busy" }, {})
|
|
1223
|
+
: connectionsResponse(
|
|
1224
|
+
"runsOn",
|
|
1225
|
+
sectionOf(pods(1, 40), { total: 40, nextOffset: null }),
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
return driftedPage();
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
renderPanel();
|
|
1232
|
+
await waitForConnections();
|
|
1233
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1234
|
+
fireEvent.click(
|
|
1235
|
+
await screen.findByRole("button", { name: "Reload list: Runs on" }),
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
const alert: HTMLElement =
|
|
1239
|
+
await within(runsOnSection()).findByRole("alert");
|
|
1240
|
+
expect(alert).toHaveTextContent("Could not reload this list.");
|
|
1241
|
+
expect(alert).toHaveTextContent("Database busy");
|
|
1242
|
+
expect(runsOnRows()).toHaveLength(39);
|
|
1243
|
+
|
|
1244
|
+
failReload = false;
|
|
1245
|
+
fireEvent.click(
|
|
1246
|
+
screen.getByRole("button", { name: "Reload list: Runs on" }),
|
|
1247
|
+
);
|
|
1248
|
+
await waitFor(() => {
|
|
1249
|
+
expect(runsOnRows()).toHaveLength(40);
|
|
1250
|
+
});
|
|
1251
|
+
expect(changedNote()).toBeNull();
|
|
1252
|
+
expect(
|
|
1253
|
+
within(runsOnSection()).queryByRole("alert"),
|
|
1254
|
+
).not.toBeInTheDocument();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
test("reloading the drawer for a new range drops the note", async () => {
|
|
1258
|
+
serve((request: PostRequest): Answer => {
|
|
1259
|
+
return isConnectionsRequest(request) ? driftedPage() : checkoutResponse();
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const view: RenderResult = renderPanel();
|
|
1263
|
+
await waitForConnections();
|
|
1264
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1265
|
+
await waitFor(() => {
|
|
1266
|
+
expect(changedNote()).not.toBeNull();
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
view.rerender(
|
|
1270
|
+
<EntityDetailPanel
|
|
1271
|
+
entity={CHECKOUT}
|
|
1272
|
+
rangeStart={LATER_RANGE_START}
|
|
1273
|
+
metricsWindowSeconds={60}
|
|
1274
|
+
onClose={() => {
|
|
1275
|
+
return undefined;
|
|
1276
|
+
}}
|
|
1277
|
+
/>,
|
|
1278
|
+
);
|
|
1279
|
+
await waitForConnections();
|
|
1280
|
+
|
|
1281
|
+
expect(runsOnRows()).toHaveLength(25);
|
|
1282
|
+
expect(changedNote()).toBeNull();
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
describe("a resource that is gone, and failures", () => {
|
|
1287
|
+
test("a key no item has any more keeps the header and says it is gone", async () => {
|
|
1288
|
+
serveEntity(entityResponse(null));
|
|
1289
|
+
const onFocus: MockFunction = getJestMockFunction();
|
|
1290
|
+
|
|
1291
|
+
renderPanel({ onFocus });
|
|
1292
|
+
await waitForConnections();
|
|
1293
|
+
|
|
1294
|
+
expect(
|
|
1295
|
+
screen.getByRole("region", { name: "Checkout API" }),
|
|
1296
|
+
).toBeInTheDocument();
|
|
1297
|
+
expect(
|
|
1298
|
+
screen.getByText("This resource is no longer in Inventory."),
|
|
1299
|
+
).toBeInTheDocument();
|
|
1300
|
+
expect(
|
|
1301
|
+
screen.queryByRole("button", { name: "Explore connections" }),
|
|
1302
|
+
).not.toBeInTheDocument();
|
|
1303
|
+
expect(screen.queryByText("Inventory details")).not.toBeInTheDocument();
|
|
1304
|
+
expect(screen.queryByText("Details")).not.toBeInTheDocument();
|
|
1305
|
+
expect(
|
|
1306
|
+
screen.queryByText("No connections in the selected time range."),
|
|
1307
|
+
).not.toBeInTheDocument();
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test("a failed load is an alert with an in-drawer retry", async () => {
|
|
1311
|
+
let fail: boolean = true;
|
|
1312
|
+
serve((): Answer => {
|
|
1313
|
+
if (fail) {
|
|
1314
|
+
return new HTTPErrorResponse(
|
|
1315
|
+
500,
|
|
1316
|
+
{ message: "Inventory is unavailable" },
|
|
1317
|
+
{},
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
return checkoutResponse();
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
renderPanel();
|
|
1324
|
+
|
|
1325
|
+
const alert: HTMLElement = await screen.findByRole("alert");
|
|
1326
|
+
expect(alert).toHaveTextContent(
|
|
1327
|
+
"Could not load this resource's connections.",
|
|
1328
|
+
);
|
|
1329
|
+
expect(alert).toHaveTextContent("Inventory is unavailable");
|
|
1330
|
+
expect(
|
|
1331
|
+
screen.getByRole("region", { name: "Checkout API" }),
|
|
1332
|
+
).toBeInTheDocument();
|
|
1333
|
+
|
|
1334
|
+
fail = false;
|
|
1335
|
+
fireEvent.click(within(alert).getByRole("button", { name: "Try again" }));
|
|
1336
|
+
|
|
1337
|
+
expect(
|
|
1338
|
+
await screen.findByTestId("entity-detail-calls"),
|
|
1339
|
+
).toBeInTheDocument();
|
|
1340
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
1341
|
+
expect(entityRequests()).toHaveLength(2);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
test("a server without the endpoint asks for a reload", async () => {
|
|
1345
|
+
serve((): Answer => {
|
|
1346
|
+
return new HTTPErrorResponse(404, { message: "Not found" }, {});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
renderPanel();
|
|
1350
|
+
|
|
1351
|
+
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
1352
|
+
"Topology was updated. Reload the page.",
|
|
1353
|
+
);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
test("a response in another format asks for a reload too", async () => {
|
|
1357
|
+
serve((): Answer => {
|
|
1358
|
+
return { ...checkoutResponse(), formatVersion: 999 };
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
renderPanel();
|
|
1362
|
+
|
|
1363
|
+
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
1364
|
+
"Topology was updated. Reload the page.",
|
|
1365
|
+
);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
test("an outdated server is offered a page reload, never a retry that cannot work", async () => {
|
|
1369
|
+
const reload: SpyInstance<() => void> = jest
|
|
1370
|
+
.spyOn(Navigation, "reload")
|
|
1371
|
+
.mockImplementation(() => {
|
|
1372
|
+
return undefined;
|
|
1373
|
+
});
|
|
1374
|
+
serve((): Answer => {
|
|
1375
|
+
return { ...checkoutResponse(), formatVersion: 999 };
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
renderPanel();
|
|
1379
|
+
|
|
1380
|
+
const alert: HTMLElement = await screen.findByRole("alert");
|
|
1381
|
+
expect(
|
|
1382
|
+
within(alert).queryByRole("button", { name: "Try again" }),
|
|
1383
|
+
).not.toBeInTheDocument();
|
|
1384
|
+
fireEvent.click(within(alert).getByRole("button", { name: "Reload page" }));
|
|
1385
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
1386
|
+
expect(entityRequests()).toHaveLength(1);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
test("a busy server says so, and trying again works", async () => {
|
|
1390
|
+
const reload: SpyInstance<() => void> = jest
|
|
1391
|
+
.spyOn(Navigation, "reload")
|
|
1392
|
+
.mockImplementation(() => {
|
|
1393
|
+
return undefined;
|
|
1394
|
+
});
|
|
1395
|
+
let busy: boolean = true;
|
|
1396
|
+
serve((): Answer => {
|
|
1397
|
+
return busy
|
|
1398
|
+
? new HTTPErrorResponse(
|
|
1399
|
+
429,
|
|
1400
|
+
{ message: "Too many topology requests are running." },
|
|
1401
|
+
{},
|
|
1402
|
+
)
|
|
1403
|
+
: checkoutResponse();
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
renderPanel();
|
|
1407
|
+
|
|
1408
|
+
const alert: HTMLElement = await screen.findByRole("alert");
|
|
1409
|
+
expect(alert).toHaveTextContent(
|
|
1410
|
+
"Could not load this resource's connections.",
|
|
1411
|
+
);
|
|
1412
|
+
expect(alert).toHaveTextContent(
|
|
1413
|
+
"The topology service is busy. Try again in a moment.",
|
|
1414
|
+
);
|
|
1415
|
+
expect(
|
|
1416
|
+
within(alert).queryByRole("button", { name: "Reload page" }),
|
|
1417
|
+
).not.toBeInTheDocument();
|
|
1418
|
+
|
|
1419
|
+
busy = false;
|
|
1420
|
+
fireEvent.click(within(alert).getByRole("button", { name: "Try again" }));
|
|
1421
|
+
|
|
1422
|
+
expect(
|
|
1423
|
+
await screen.findByTestId("entity-detail-calls"),
|
|
1424
|
+
).toBeInTheDocument();
|
|
1425
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
1426
|
+
expect(entityRequests()).toHaveLength(2);
|
|
1427
|
+
expect(reload).not.toHaveBeenCalled();
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
test("Show more against an outdated server offers a page reload and stops paging that section", async () => {
|
|
1431
|
+
const reload: SpyInstance<() => void> = jest
|
|
1432
|
+
.spyOn(Navigation, "reload")
|
|
1433
|
+
.mockImplementation(() => {
|
|
1434
|
+
return undefined;
|
|
1435
|
+
});
|
|
1436
|
+
serve((request: PostRequest): Answer => {
|
|
1437
|
+
return isConnectionsRequest(request)
|
|
1438
|
+
? new HTTPErrorResponse(404, { message: "Not found" }, {})
|
|
1439
|
+
: checkoutResponse();
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
renderPanel();
|
|
1443
|
+
await waitForConnections();
|
|
1444
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1445
|
+
|
|
1446
|
+
const runsOn: HTMLElement = screen.getByTestId("entity-detail-runs-on");
|
|
1447
|
+
const alert: HTMLElement = await within(runsOn).findByRole("alert");
|
|
1448
|
+
expect(alert).toHaveTextContent(
|
|
1449
|
+
"Could not load more connections. Topology was updated. Reload the page.",
|
|
1450
|
+
);
|
|
1451
|
+
/* A further page would fail the same way: no Show more to press. */
|
|
1452
|
+
expect(
|
|
1453
|
+
screen.queryByRole("button", { name: "Show more: Runs on" }),
|
|
1454
|
+
).not.toBeInTheDocument();
|
|
1455
|
+
expect(within(runsOn).getAllByRole("listitem")).toHaveLength(25);
|
|
1456
|
+
|
|
1457
|
+
fireEvent.click(within(alert).getByRole("button", { name: "Reload page" }));
|
|
1458
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
1459
|
+
expect(requests().filter(isConnectionsRequest)).toHaveLength(1);
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
test("Show more against a busy server says so, and can be pressed again", async () => {
|
|
1463
|
+
let busy: boolean = true;
|
|
1464
|
+
serve((request: PostRequest): Answer => {
|
|
1465
|
+
if (!isConnectionsRequest(request)) {
|
|
1466
|
+
return checkoutResponse();
|
|
1467
|
+
}
|
|
1468
|
+
return busy
|
|
1469
|
+
? new HTTPErrorResponse(429, { message: "Busy" }, {})
|
|
1470
|
+
: connectionsResponse(
|
|
1471
|
+
"runsOn",
|
|
1472
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
1473
|
+
);
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
renderPanel();
|
|
1477
|
+
await waitForConnections();
|
|
1478
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1479
|
+
|
|
1480
|
+
const runsOn: HTMLElement = screen.getByTestId("entity-detail-runs-on");
|
|
1481
|
+
const alert: HTMLElement = await within(runsOn).findByRole("alert");
|
|
1482
|
+
expect(alert).toHaveTextContent(
|
|
1483
|
+
"Could not load more connections. The topology service is busy. Try again in a moment.",
|
|
1484
|
+
);
|
|
1485
|
+
expect(
|
|
1486
|
+
within(alert).queryByRole("button", { name: "Reload page" }),
|
|
1487
|
+
).not.toBeInTheDocument();
|
|
1488
|
+
|
|
1489
|
+
busy = false;
|
|
1490
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
1491
|
+
await waitFor(() => {
|
|
1492
|
+
expect(within(runsOn).getAllByRole("listitem")).toHaveLength(40);
|
|
1493
|
+
});
|
|
1494
|
+
expect(within(runsOn).queryByRole("alert")).not.toBeInTheDocument();
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
describe("switching entities and ranges", () => {
|
|
1499
|
+
function webResponse(): JSONObject {
|
|
1500
|
+
return entityResponse(
|
|
1501
|
+
entityRow({
|
|
1502
|
+
id: "2b2b0000-0000-4000-8000-000000000002",
|
|
1503
|
+
key: "web",
|
|
1504
|
+
name: "Web frontend",
|
|
1505
|
+
resourceId: null,
|
|
1506
|
+
resourceType: null,
|
|
1507
|
+
}),
|
|
1508
|
+
{
|
|
1509
|
+
calls: sectionOf([
|
|
1510
|
+
connection({ otherKey: "svc", otherName: "Checkout API" }),
|
|
1511
|
+
]),
|
|
1512
|
+
},
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
test("a new entity never shows the previous entity's rows, even without a remount", async () => {
|
|
1517
|
+
const web: Deferred = deferred();
|
|
1518
|
+
serve((request: PostRequest): Answer => {
|
|
1519
|
+
return request.data["entityKey"] === "web"
|
|
1520
|
+
? web.promise
|
|
1521
|
+
: checkoutResponse();
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
/*
|
|
1525
|
+
* What the page showed at each commit, tagged with the entity it had
|
|
1526
|
+
* asked the drawer for. A sibling's layout effect runs once React has
|
|
1527
|
+
* written a commit to the DOM and before any passive effect — including
|
|
1528
|
+
* the drawer's own reset to "loading" — so it sees the FIRST commit for
|
|
1529
|
+
* the new entity: the one frame in which rows fetched for the previous
|
|
1530
|
+
* entity could still be painted under the new header. Assertions made
|
|
1531
|
+
* after rerender() cannot: act() has flushed that reset by then.
|
|
1532
|
+
*/
|
|
1533
|
+
const commits: Array<{ entityKey: string; text: string }> = [];
|
|
1534
|
+
const Probe: React.FunctionComponent<{ entityKey: string }> = (props: {
|
|
1535
|
+
entityKey: string;
|
|
1536
|
+
}): null => {
|
|
1537
|
+
React.useLayoutEffect(() => {
|
|
1538
|
+
commits.push({
|
|
1539
|
+
entityKey: props.entityKey,
|
|
1540
|
+
text: document.body.textContent || "",
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
return null;
|
|
1544
|
+
};
|
|
1545
|
+
const page: (target: EntityDetailTarget) => React.ReactElement = (
|
|
1546
|
+
target: EntityDetailTarget,
|
|
1547
|
+
): React.ReactElement => {
|
|
1548
|
+
return (
|
|
1549
|
+
<>
|
|
1550
|
+
<EntityDetailPanel
|
|
1551
|
+
entity={target}
|
|
1552
|
+
rangeStart={RANGE_START}
|
|
1553
|
+
metricsWindowSeconds={60}
|
|
1554
|
+
onClose={() => {
|
|
1555
|
+
return undefined;
|
|
1556
|
+
}}
|
|
1557
|
+
/>
|
|
1558
|
+
<Probe entityKey={target.entityKey} />
|
|
1559
|
+
</>
|
|
1560
|
+
);
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
const view: RenderResult = render(page(CHECKOUT));
|
|
1564
|
+
await screen.findByText("Checkout API depends on Orders database");
|
|
1565
|
+
|
|
1566
|
+
view.rerender(
|
|
1567
|
+
page({
|
|
1568
|
+
entityKey: "web",
|
|
1569
|
+
entityType: EntityType.Service,
|
|
1570
|
+
displayName: "Web frontend",
|
|
1571
|
+
}),
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
const webCommits: Array<{ entityKey: string; text: string }> =
|
|
1575
|
+
commits.filter((commit: { entityKey: string; text: string }): boolean => {
|
|
1576
|
+
return commit.entityKey === "web";
|
|
1577
|
+
});
|
|
1578
|
+
expect(webCommits.length).toBeGreaterThan(0);
|
|
1579
|
+
for (const commit of webCommits) {
|
|
1580
|
+
expect(commit.text).not.toContain("Orders database");
|
|
1581
|
+
expect(commit.text).not.toContain("Calls (2)");
|
|
1582
|
+
expect(commit.text).not.toContain("Runs on (40)");
|
|
1583
|
+
expect(commit.text).toContain("Loading connections…");
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
expect(
|
|
1587
|
+
screen.queryByText("Checkout API depends on Orders database"),
|
|
1588
|
+
).not.toBeInTheDocument();
|
|
1589
|
+
expect(screen.queryByText(/Orders database/)).not.toBeInTheDocument();
|
|
1590
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
1591
|
+
expect(
|
|
1592
|
+
screen.getByRole("region", { name: "Web frontend" }),
|
|
1593
|
+
).toBeInTheDocument();
|
|
1594
|
+
|
|
1595
|
+
await act(async () => {
|
|
1596
|
+
web.resolve(webResponse());
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
expect(
|
|
1600
|
+
screen.getByText("Web frontend depends on Checkout API"),
|
|
1601
|
+
).toBeInTheDocument();
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
test("an answer for the previous entity that arrives late is ignored", async () => {
|
|
1605
|
+
const checkout: Deferred = deferred();
|
|
1606
|
+
serve((request: PostRequest): Answer => {
|
|
1607
|
+
return request.data["entityKey"] === "web"
|
|
1608
|
+
? webResponse()
|
|
1609
|
+
: checkout.promise;
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
const view: RenderResult = renderPanel();
|
|
1613
|
+
const firstSignal: AbortSignal | undefined =
|
|
1614
|
+
entityRequests()[0]!.options.signal;
|
|
1615
|
+
|
|
1616
|
+
view.rerender(
|
|
1617
|
+
<EntityDetailPanel
|
|
1618
|
+
entity={{ entityKey: "web", displayName: "Web frontend" }}
|
|
1619
|
+
rangeStart={RANGE_START}
|
|
1620
|
+
metricsWindowSeconds={60}
|
|
1621
|
+
onClose={() => {
|
|
1622
|
+
return undefined;
|
|
315
1623
|
}}
|
|
316
|
-
|
|
1624
|
+
/>,
|
|
1625
|
+
);
|
|
1626
|
+
expect(firstSignal?.aborted).toBe(true);
|
|
1627
|
+
await screen.findByText("Web frontend depends on Checkout API");
|
|
1628
|
+
|
|
1629
|
+
await act(async () => {
|
|
1630
|
+
checkout.resolve(checkoutResponse());
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
expect(
|
|
1634
|
+
screen.queryByText("Checkout API depends on Orders database"),
|
|
1635
|
+
).not.toBeInTheDocument();
|
|
1636
|
+
expect(
|
|
1637
|
+
screen.getByText("Web frontend depends on Checkout API"),
|
|
1638
|
+
).toBeInTheDocument();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
test("a new range reloads the connections instead of showing the old range's", async () => {
|
|
1642
|
+
const later: Deferred = deferred();
|
|
1643
|
+
serve((request: PostRequest): Answer => {
|
|
1644
|
+
return request.data["rangeStart"] === LATER_RANGE_START.toISOString()
|
|
1645
|
+
? later.promise
|
|
1646
|
+
: checkoutResponse();
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
const view: RenderResult = renderPanel();
|
|
1650
|
+
await screen.findByText("Checkout API depends on Orders database");
|
|
1651
|
+
|
|
1652
|
+
view.rerender(
|
|
1653
|
+
<EntityDetailPanel
|
|
1654
|
+
entity={CHECKOUT}
|
|
1655
|
+
rangeStart={new Date(LATER_RANGE_START.getTime())}
|
|
1656
|
+
metricsWindowSeconds={60}
|
|
1657
|
+
onClose={() => {
|
|
1658
|
+
return undefined;
|
|
1659
|
+
}}
|
|
1660
|
+
/>,
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
expect(
|
|
1664
|
+
screen.queryByText("Checkout API depends on Orders database"),
|
|
1665
|
+
).not.toBeInTheDocument();
|
|
1666
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
1667
|
+
|
|
1668
|
+
await act(async () => {
|
|
1669
|
+
later.resolve(
|
|
1670
|
+
entityResponse(entityRow(), {
|
|
1671
|
+
calledBy: sectionOf([
|
|
1672
|
+
connection({
|
|
1673
|
+
direction: "in",
|
|
1674
|
+
otherKey: "cron",
|
|
1675
|
+
otherName: "cron",
|
|
1676
|
+
}),
|
|
1677
|
+
]),
|
|
1678
|
+
}),
|
|
1679
|
+
);
|
|
1680
|
+
});
|
|
1681
|
+
expect(
|
|
1682
|
+
screen.getByText("cron depends on Checkout API"),
|
|
1683
|
+
).toBeInTheDocument();
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
test("re-rendering with an equal preview and range does not refetch", async () => {
|
|
1687
|
+
serveEntity(checkoutResponse());
|
|
1688
|
+
|
|
1689
|
+
const view: RenderResult = renderPanel();
|
|
1690
|
+
await waitForConnections();
|
|
1691
|
+
|
|
1692
|
+
view.rerender(
|
|
1693
|
+
<EntityDetailPanel
|
|
1694
|
+
entity={{ ...CHECKOUT }}
|
|
1695
|
+
rangeStart={new Date(RANGE_START.getTime())}
|
|
1696
|
+
metricsWindowSeconds={60}
|
|
1697
|
+
onClose={() => {
|
|
1698
|
+
return undefined;
|
|
1699
|
+
}}
|
|
1700
|
+
/>,
|
|
1701
|
+
);
|
|
1702
|
+
await waitForConnections();
|
|
1703
|
+
|
|
1704
|
+
expect(entityRequests()).toHaveLength(1);
|
|
1705
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
describe("best-effort links from the full row", () => {
|
|
1709
|
+
function serviceLookups(): Array<unknown> {
|
|
1710
|
+
return getListMock.mock.calls.filter((call: Array<unknown>): boolean => {
|
|
1711
|
+
return (
|
|
1712
|
+
(call[0] as { modelType: { name: string } }).modelType.name ===
|
|
1713
|
+
"Service"
|
|
1714
|
+
);
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
test("the traces link uses the row's own Service pointer, with no lookup", async () => {
|
|
1719
|
+
serveEntity(checkoutResponse());
|
|
1720
|
+
|
|
1721
|
+
renderPanel();
|
|
1722
|
+
|
|
1723
|
+
const link: HTMLElement = await screen.findByText(
|
|
1724
|
+
"Traces for this service",
|
|
1725
|
+
);
|
|
1726
|
+
expect(link.closest("a")?.getAttribute("href")).toContain(SERVICE_ID);
|
|
1727
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
test("without a pointer the Service is found by name — once per row, never from the preview", async () => {
|
|
1731
|
+
const first: Deferred = deferred();
|
|
1732
|
+
let served: number = 0;
|
|
1733
|
+
serve((): Answer => {
|
|
1734
|
+
served++;
|
|
1735
|
+
return served === 1
|
|
1736
|
+
? first.promise
|
|
1737
|
+
: entityResponse(entityRow({ resourceType: null, resourceId: null }));
|
|
1738
|
+
});
|
|
1739
|
+
getListMock.mockImplementation(async () => {
|
|
1740
|
+
return { data: [{ _id: SERVICE_ID }], count: 1 };
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
const view: RenderResult = renderPanel();
|
|
1744
|
+
/* The preview is a named service, but it is not the row. */
|
|
1745
|
+
expect(serviceLookups()).toHaveLength(0);
|
|
1746
|
+
|
|
1747
|
+
await act(async () => {
|
|
1748
|
+
first.resolve(
|
|
1749
|
+
entityResponse(entityRow({ resourceType: null, resourceId: null })),
|
|
1750
|
+
);
|
|
1751
|
+
});
|
|
1752
|
+
const link: HTMLElement = await screen.findByText(
|
|
1753
|
+
"Traces for this service",
|
|
1754
|
+
);
|
|
1755
|
+
expect(link.closest("a")?.getAttribute("href")).toContain(SERVICE_ID);
|
|
1756
|
+
expect(serviceLookups()).toHaveLength(1);
|
|
1757
|
+
expect(
|
|
1758
|
+
(serviceLookups()[0] as Array<{ query: JSONObject }>)[0]!.query,
|
|
1759
|
+
).toEqual({ projectId: PROJECT_ID, name: "Checkout API" });
|
|
1760
|
+
|
|
1761
|
+
/* A new range reloads the row; the same row is not looked up again. */
|
|
1762
|
+
view.rerender(
|
|
1763
|
+
<EntityDetailPanel
|
|
1764
|
+
entity={CHECKOUT}
|
|
1765
|
+
rangeStart={LATER_RANGE_START}
|
|
1766
|
+
metricsWindowSeconds={60}
|
|
1767
|
+
onClose={() => {
|
|
317
1768
|
return undefined;
|
|
318
1769
|
}}
|
|
319
1770
|
/>,
|
|
320
1771
|
);
|
|
321
|
-
|
|
322
|
-
|
|
1772
|
+
await waitForConnections();
|
|
1773
|
+
await screen.findByText("Traces for this service");
|
|
1774
|
+
|
|
1775
|
+
expect(entityRequests()).toHaveLength(2);
|
|
1776
|
+
expect(serviceLookups()).toHaveLength(1);
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
test("a host is matched to the network device of the same name", async () => {
|
|
1780
|
+
serveEntity(
|
|
1781
|
+
entityResponse(
|
|
1782
|
+
entityRow({
|
|
1783
|
+
key: "host-key",
|
|
1784
|
+
type: EntityType.Host,
|
|
1785
|
+
name: "web-1.example.com",
|
|
1786
|
+
resourceType: null,
|
|
1787
|
+
resourceId: null,
|
|
1788
|
+
}),
|
|
1789
|
+
),
|
|
323
1790
|
);
|
|
324
|
-
|
|
325
|
-
|
|
1791
|
+
getListMock.mockImplementation(async () => {
|
|
1792
|
+
return {
|
|
1793
|
+
data: [
|
|
1794
|
+
{ _id: "d0d0d0d0-0000-4000-8000-000000000001", name: "core-sw" },
|
|
1795
|
+
{
|
|
1796
|
+
_id: "d0d0d0d0-0000-4000-8000-000000000002",
|
|
1797
|
+
name: "rack-7",
|
|
1798
|
+
hostname: "WEB-1",
|
|
1799
|
+
},
|
|
1800
|
+
],
|
|
1801
|
+
count: 2,
|
|
1802
|
+
};
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
renderPanel({
|
|
1806
|
+
entity: { entityKey: "host-key", entityType: EntityType.Host },
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
const link: HTMLElement = await screen.findByText(/Network device:/);
|
|
1810
|
+
expect(link).toHaveTextContent("Network device: rack-7");
|
|
1811
|
+
expect(getListMock).toHaveBeenCalledTimes(1);
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
test("the inventory link opens the row the server returned", async () => {
|
|
1815
|
+
serveEntity(checkoutResponse());
|
|
1816
|
+
|
|
1817
|
+
renderPanel();
|
|
1818
|
+
|
|
1819
|
+
const link: HTMLElement = await screen.findByText("Inventory details");
|
|
1820
|
+
expect(link.closest("a")?.getAttribute("href")).toContain(ITEM_ID);
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
test("details come from the full row's attributes", async () => {
|
|
1824
|
+
serveEntity(
|
|
1825
|
+
entityResponse(
|
|
1826
|
+
entityRow({
|
|
1827
|
+
key: "pg",
|
|
1828
|
+
type: EntityType.Database,
|
|
1829
|
+
name: "orders",
|
|
1830
|
+
resourceType: null,
|
|
1831
|
+
resourceId: null,
|
|
1832
|
+
identifyingAttributes: { "db.system.name": "postgresql" },
|
|
1833
|
+
descriptiveAttributes: {
|
|
1834
|
+
"k8s.pod.name": "orders",
|
|
1835
|
+
"host.ip": 10,
|
|
1836
|
+
},
|
|
1837
|
+
}),
|
|
1838
|
+
),
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
renderPanel({
|
|
1842
|
+
entity: { entityKey: "pg", entityType: EntityType.Database },
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
expect(
|
|
1846
|
+
(await screen.findByText("Database engine")).parentElement,
|
|
1847
|
+
).toHaveTextContent("postgresql");
|
|
1848
|
+
/* A value equal to the name, or not a string, is not repeated. */
|
|
1849
|
+
expect(screen.queryByText("Pod name")).not.toBeInTheDocument();
|
|
1850
|
+
expect(screen.queryByText("IP Address")).not.toBeInTheDocument();
|
|
1851
|
+
expect(screen.getByText("First seen")).toBeInTheDocument();
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
describe("where rows lead", () => {
|
|
1856
|
+
test("dependencies and callers open in place, with what the drawer knows about them", async () => {
|
|
1857
|
+
serveEntity(checkoutResponse());
|
|
1858
|
+
const onSelectEntity: MockFunction = getJestMockFunction();
|
|
1859
|
+
|
|
1860
|
+
renderPanel({ onSelectEntity });
|
|
1861
|
+
await waitForConnections();
|
|
1862
|
+
|
|
1863
|
+
fireEvent.click(
|
|
1864
|
+
screen.getByRole("button", { name: "View details for Orders database" }),
|
|
1865
|
+
);
|
|
1866
|
+
expect(onSelectEntity).toHaveBeenLastCalledWith({
|
|
1867
|
+
entityKey: "db",
|
|
1868
|
+
entityType: EntityType.Database,
|
|
1869
|
+
displayName: "Orders database",
|
|
1870
|
+
});
|
|
1871
|
+
fireEvent.click(
|
|
1872
|
+
screen.getByRole("button", { name: "View details for Web frontend" }),
|
|
1873
|
+
);
|
|
1874
|
+
expect(onSelectEntity).toHaveBeenLastCalledWith({
|
|
1875
|
+
entityKey: "web",
|
|
1876
|
+
entityType: EntityType.Service,
|
|
1877
|
+
displayName: "Web frontend",
|
|
1878
|
+
});
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
test("placements open in the Infrastructure view when the page offers it", async () => {
|
|
1882
|
+
serveEntity(checkoutResponse());
|
|
1883
|
+
const onOpenInfrastructure: MockFunction = getJestMockFunction();
|
|
1884
|
+
const onSelectEntity: MockFunction = getJestMockFunction();
|
|
1885
|
+
|
|
1886
|
+
renderPanel({ onOpenInfrastructure, onSelectEntity });
|
|
1887
|
+
await waitForConnections();
|
|
1888
|
+
|
|
1889
|
+
fireEvent.click(
|
|
1890
|
+
screen.getByRole("button", { name: "View details for checkout-pod-1" }),
|
|
1891
|
+
);
|
|
1892
|
+
expect(onOpenInfrastructure).toHaveBeenCalledWith("pod-1");
|
|
1893
|
+
expect(onSelectEntity).not.toHaveBeenCalled();
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
test("without an Infrastructure view, placements open in place too", async () => {
|
|
1897
|
+
serveEntity(checkoutResponse());
|
|
1898
|
+
const onSelectEntity: MockFunction = getJestMockFunction();
|
|
1899
|
+
|
|
1900
|
+
renderPanel({ onSelectEntity });
|
|
1901
|
+
await waitForConnections();
|
|
1902
|
+
|
|
1903
|
+
fireEvent.click(
|
|
1904
|
+
screen.getByRole("button", { name: "View details for checkout-pod-1" }),
|
|
1905
|
+
);
|
|
1906
|
+
expect(onSelectEntity).toHaveBeenCalledWith({
|
|
1907
|
+
entityKey: "pod-1",
|
|
1908
|
+
entityType: EntityType.KubernetesPod,
|
|
1909
|
+
displayName: "checkout-pod-1",
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
test("related infrastructure opens in place, never in the Infrastructure view", async () => {
|
|
1914
|
+
serveEntity(
|
|
1915
|
+
entityResponse(entityRow(), {
|
|
1916
|
+
related: sectionOf([
|
|
1917
|
+
connection({
|
|
1918
|
+
relationshipType: "member-of",
|
|
1919
|
+
otherKey: "ns",
|
|
1920
|
+
otherName: "shop",
|
|
1921
|
+
otherType: EntityType.KubernetesNamespace,
|
|
1922
|
+
}),
|
|
1923
|
+
]),
|
|
1924
|
+
}),
|
|
1925
|
+
);
|
|
1926
|
+
const onOpenInfrastructure: MockFunction = getJestMockFunction();
|
|
1927
|
+
const onSelectEntity: MockFunction = getJestMockFunction();
|
|
1928
|
+
|
|
1929
|
+
renderPanel({ onOpenInfrastructure, onSelectEntity });
|
|
1930
|
+
await waitForConnections();
|
|
1931
|
+
|
|
1932
|
+
fireEvent.click(
|
|
1933
|
+
screen.getByRole("button", { name: "View details for shop" }),
|
|
1934
|
+
);
|
|
1935
|
+
expect(onSelectEntity).toHaveBeenCalledWith({
|
|
1936
|
+
entityKey: "ns",
|
|
1937
|
+
entityType: EntityType.KubernetesNamespace,
|
|
1938
|
+
displayName: "shop",
|
|
1939
|
+
});
|
|
1940
|
+
expect(onOpenInfrastructure).not.toHaveBeenCalled();
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
test("callers without navigation support keep readable static connections", async () => {
|
|
1944
|
+
serveEntity(checkoutResponse());
|
|
1945
|
+
|
|
1946
|
+
renderPanel();
|
|
1947
|
+
await waitForConnections();
|
|
1948
|
+
|
|
1949
|
+
expect(
|
|
1950
|
+
screen.getByText("Checkout API depends on Orders database"),
|
|
1951
|
+
).toBeInTheDocument();
|
|
1952
|
+
expect(
|
|
1953
|
+
screen.queryByRole("button", { name: /View details for/ }),
|
|
1954
|
+
).not.toBeInTheDocument();
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
test("the explore action is offered only by callers that can show it, with their wording", async () => {
|
|
1958
|
+
serveEntity(checkoutResponse());
|
|
1959
|
+
const onFocus: MockFunction = getJestMockFunction();
|
|
1960
|
+
|
|
1961
|
+
const view: RenderResult = renderPanel({ onFocus });
|
|
1962
|
+
fireEvent.click(
|
|
1963
|
+
screen.getByRole("button", { name: "Explore connections" }),
|
|
1964
|
+
);
|
|
1965
|
+
expect(onFocus).toHaveBeenCalledWith("svc");
|
|
1966
|
+
await waitForConnections();
|
|
1967
|
+
view.unmount();
|
|
1968
|
+
|
|
1969
|
+
renderPanel({ onFocus, focusButtonLabel: "Show where it is" });
|
|
1970
|
+
expect(
|
|
1971
|
+
screen.getByRole("button", { name: "Show where it is" }),
|
|
1972
|
+
).toBeInTheDocument();
|
|
1973
|
+
await waitForConnections();
|
|
1974
|
+
cleanup();
|
|
1975
|
+
|
|
1976
|
+
renderPanel();
|
|
1977
|
+
expect(
|
|
1978
|
+
screen.queryByRole("button", { name: "Explore connections" }),
|
|
1979
|
+
).not.toBeInTheDocument();
|
|
1980
|
+
await waitForConnections();
|
|
1981
|
+
});
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
describe("service map context", () => {
|
|
1985
|
+
test("leads with status and the traffic in both directions", async () => {
|
|
1986
|
+
serveEntity(checkoutResponse());
|
|
1987
|
+
|
|
1988
|
+
renderPanel({
|
|
1989
|
+
traffic: {
|
|
1990
|
+
inbound: { calls: 0, errors: 0, avgDurationMs: null },
|
|
1991
|
+
outbound: { calls: 60, errors: 0, avgDurationMs: 4 },
|
|
1992
|
+
statusLabel: "Entry point",
|
|
1993
|
+
statusColor: "#6366f1",
|
|
1994
|
+
subtitle: "Service · Node.js",
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
expect(screen.getByTestId("entity-detail-status")).toHaveTextContent(
|
|
1999
|
+
"Entry point",
|
|
2000
|
+
);
|
|
2001
|
+
expect(screen.getByTestId("drawer-description")).toHaveTextContent(
|
|
2002
|
+
"Service · Node.js",
|
|
2003
|
+
);
|
|
2004
|
+
expect(
|
|
2005
|
+
screen.getByText("Requests it answered").parentElement,
|
|
2006
|
+
).toHaveTextContent("None observed");
|
|
2007
|
+
expect(screen.getByText("Calls it made").parentElement).toHaveTextContent(
|
|
2008
|
+
"60/min",
|
|
2009
|
+
);
|
|
2010
|
+
expect(screen.getByText("Calls it made").parentElement).toHaveTextContent(
|
|
2011
|
+
"4ms",
|
|
2012
|
+
);
|
|
2013
|
+
await waitForConnections();
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
test("lists active incidents and alerts for the service", async () => {
|
|
2017
|
+
serveEntity(checkoutResponse());
|
|
2018
|
+
|
|
2019
|
+
renderPanel({
|
|
2020
|
+
incidentStatus: {
|
|
2021
|
+
serviceId: SERVICE_ID,
|
|
2022
|
+
activeIncidentCount: 1,
|
|
2023
|
+
worstIncidentSeverityName: "Sev 1",
|
|
2024
|
+
worstIncidentSeverityColor: "#f00",
|
|
2025
|
+
incidents: [
|
|
2026
|
+
{
|
|
2027
|
+
id: "a1a1a1a1-0000-4000-8000-000000000001",
|
|
2028
|
+
title: "Checkout is down",
|
|
2029
|
+
severityName: "Sev 1",
|
|
2030
|
+
severityColor: "#f00",
|
|
2031
|
+
},
|
|
2032
|
+
],
|
|
2033
|
+
activeAlertCount: 0,
|
|
2034
|
+
worstAlertSeverityName: null,
|
|
2035
|
+
worstAlertSeverityColor: null,
|
|
2036
|
+
alerts: [],
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
expect(screen.getByText("Checkout is down")).toBeInTheDocument();
|
|
2041
|
+
expect(screen.getByText(/Active incidents/)).toHaveTextContent(
|
|
2042
|
+
"Active incidents (1)",
|
|
326
2043
|
);
|
|
2044
|
+
expect(screen.queryByText(/Active alerts/)).not.toBeInTheDocument();
|
|
2045
|
+
await waitForConnections();
|
|
327
2046
|
});
|
|
328
2047
|
});
|