@oneuptime/common 14.0.5 → 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/MetricService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- 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/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- 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/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -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/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -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/MetricService.js +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/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.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/AnalyticsDatabase/StatementGenerator.js +1 -1
- 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/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- 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/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.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/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
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";
|
|
20
|
+
import React, { ReactElement, ReactNode } from "react";
|
|
21
|
+
import type { SpyInstance } from "jest-mock";
|
|
22
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The inventory item page's Connections card. It used to download both
|
|
26
|
+
* directions of the item's relationships itself — 10,000 rows each, sorted
|
|
27
|
+
* by recency — and resolve every other end with one IN list, so a cluster,
|
|
28
|
+
* namespace or node with more connections than that was silently cut off.
|
|
29
|
+
* It now asks the server's all-time Topology endpoint, which counts every
|
|
30
|
+
* connection the inventory holds and returns the first rows of each
|
|
31
|
+
* section. Pinned here against a fake API behind the real request and
|
|
32
|
+
* decoding code:
|
|
33
|
+
*
|
|
34
|
+
* - one all-time request per item: the key and type, never a range;
|
|
35
|
+
* - exact totals per section ("N+" when the server stopped counting);
|
|
36
|
+
* - rows phrased from this item's end, linking to the other item, and
|
|
37
|
+
* ends no longer in inventory shown by key, unlinked;
|
|
38
|
+
* - "Show more" appending the next page and moving focus to it, a list
|
|
39
|
+
* that changed underneath offering a reload, per-section failures;
|
|
40
|
+
* - a failed load that can be retried, or reloaded when outdated;
|
|
41
|
+
* - never the previous item's rows under another item.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
45
|
+
|
|
46
|
+
// The arrow wrappers are load bearing: jest.mock is hoisted above the mocks.
|
|
47
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
48
|
+
return {
|
|
49
|
+
__esModule: true,
|
|
50
|
+
default: {
|
|
51
|
+
post: (...args: Array<any>) => {
|
|
52
|
+
return postMock(...args);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
58
|
+
return {
|
|
59
|
+
__esModule: true,
|
|
60
|
+
default: {
|
|
61
|
+
getCommonHeaders: () => {
|
|
62
|
+
return { tenantid: "project-1" };
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
jest.mock("../../../UI/Utils/Translation", () => {
|
|
68
|
+
return {
|
|
69
|
+
__esModule: true,
|
|
70
|
+
default: () => {
|
|
71
|
+
return {
|
|
72
|
+
translateString: (value: string) => {
|
|
73
|
+
return value;
|
|
74
|
+
},
|
|
75
|
+
translateValue: (value: React.ReactNode) => {
|
|
76
|
+
return value;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
jest.mock(
|
|
83
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AppLink/AppLink",
|
|
84
|
+
() => {
|
|
85
|
+
return {
|
|
86
|
+
__esModule: true,
|
|
87
|
+
default: (props: {
|
|
88
|
+
to?: { toString: () => string };
|
|
89
|
+
children: ReactNode;
|
|
90
|
+
className?: string;
|
|
91
|
+
}): ReactElement => {
|
|
92
|
+
return React.createElement(
|
|
93
|
+
"a",
|
|
94
|
+
{ href: props.to?.toString(), className: props.className },
|
|
95
|
+
props.children,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
import InventoryRelationships, {
|
|
103
|
+
ComponentProps,
|
|
104
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Inventory/InventoryRelationships";
|
|
105
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
106
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
107
|
+
import Route from "../../../Types/API/Route";
|
|
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
|
+
TopologyApiLimits,
|
|
114
|
+
TopologyConnectionRowJSON,
|
|
115
|
+
TopologyConnectionSection,
|
|
116
|
+
TopologyConnectionSectionJSON,
|
|
117
|
+
TopologyEntityAllTimeConnectionsResponseJSON,
|
|
118
|
+
TopologyEntityAllTimeResponseJSON,
|
|
119
|
+
TopologyEntityDetailJSON,
|
|
120
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
121
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
122
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
123
|
+
|
|
124
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
125
|
+
"5b2f5b1c-0000-4000-8000-000000000001",
|
|
126
|
+
);
|
|
127
|
+
const NAMESPACE_ID: string = "1a1a0000-0000-4000-8000-000000000001";
|
|
128
|
+
const PAGE: number = TopologyApiLimits.EntityOtherRows;
|
|
129
|
+
|
|
130
|
+
interface PostRequest {
|
|
131
|
+
url: { toString: () => string };
|
|
132
|
+
data: JSONObject;
|
|
133
|
+
options: { signal?: AbortSignal };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type Answer = JSONObject | HTTPErrorResponse | Promise<JSONObject>;
|
|
137
|
+
|
|
138
|
+
interface Deferred {
|
|
139
|
+
promise: Promise<JSONObject>;
|
|
140
|
+
resolve: (value: JSONObject) => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deferred(): Deferred {
|
|
144
|
+
let resolve: (value: JSONObject) => void = () => {
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
const promise: Promise<JSONObject> = new Promise<JSONObject>(
|
|
148
|
+
(res: (value: JSONObject) => void) => {
|
|
149
|
+
resolve = res;
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
return { promise, resolve };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function asJSON(value: unknown): JSONObject {
|
|
156
|
+
return JSON.parse(JSON.stringify(value)) as JSONObject;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function podId(index: number): string {
|
|
160
|
+
return `2b2b0000-0000-4000-8000-${String(index).padStart(12, "0")}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function entityRow(
|
|
164
|
+
overrides: Partial<TopologyEntityDetailJSON> = {},
|
|
165
|
+
): TopologyEntityDetailJSON {
|
|
166
|
+
return {
|
|
167
|
+
id: NAMESPACE_ID,
|
|
168
|
+
key: "ns-payments",
|
|
169
|
+
type: EntityType.KubernetesNamespace,
|
|
170
|
+
name: "payments",
|
|
171
|
+
source: "discovered",
|
|
172
|
+
lastSeenAt: null,
|
|
173
|
+
firstSeenAt: null,
|
|
174
|
+
resourceType: null,
|
|
175
|
+
resourceId: null,
|
|
176
|
+
identifyingAttributes: null,
|
|
177
|
+
descriptiveAttributes: null,
|
|
178
|
+
...overrides,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function connection(
|
|
183
|
+
overrides: Partial<TopologyConnectionRowJSON>,
|
|
184
|
+
): TopologyConnectionRowJSON {
|
|
185
|
+
return {
|
|
186
|
+
relationshipType: "part-of",
|
|
187
|
+
direction: "in",
|
|
188
|
+
otherKey: "other",
|
|
189
|
+
otherKnown: true,
|
|
190
|
+
otherId: podId(0),
|
|
191
|
+
otherName: "Other",
|
|
192
|
+
otherType: EntityType.KubernetesPod,
|
|
193
|
+
callCount: null,
|
|
194
|
+
errorCount: null,
|
|
195
|
+
avgDurationMs: null,
|
|
196
|
+
lastSeenAt: null,
|
|
197
|
+
...overrides,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* A pod that is part of the namespace. */
|
|
202
|
+
function pod(index: number): TopologyConnectionRowJSON {
|
|
203
|
+
return connection({
|
|
204
|
+
otherKey: `pod-${index}`,
|
|
205
|
+
otherId: podId(index),
|
|
206
|
+
otherName: `api-pod-${index}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pods(from: number, to: number): Array<TopologyConnectionRowJSON> {
|
|
211
|
+
const rows: Array<TopologyConnectionRowJSON> = [];
|
|
212
|
+
for (let index: number = from; index <= to; index++) {
|
|
213
|
+
rows.push(pod(index));
|
|
214
|
+
}
|
|
215
|
+
return rows;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sectionOf(
|
|
219
|
+
rows: Array<TopologyConnectionRowJSON>,
|
|
220
|
+
overrides: Partial<TopologyConnectionSectionJSON> = {},
|
|
221
|
+
): TopologyConnectionSectionJSON {
|
|
222
|
+
return {
|
|
223
|
+
total: rows.length,
|
|
224
|
+
unknownTotal: 0,
|
|
225
|
+
rows: rows,
|
|
226
|
+
nextOffset: null,
|
|
227
|
+
...overrides,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const EMPTY: Record<TopologyConnectionSection, TopologyConnectionSectionJSON> =
|
|
232
|
+
{
|
|
233
|
+
calls: sectionOf([]),
|
|
234
|
+
calledBy: sectionOf([]),
|
|
235
|
+
runsOn: sectionOf([]),
|
|
236
|
+
related: sectionOf([]),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
function entityResponse(
|
|
240
|
+
entity: TopologyEntityDetailJSON | null,
|
|
241
|
+
sections: Partial<
|
|
242
|
+
Record<TopologyConnectionSection, TopologyConnectionSectionJSON>
|
|
243
|
+
> = {},
|
|
244
|
+
isScanLimited: boolean = false,
|
|
245
|
+
): JSONObject {
|
|
246
|
+
const response: TopologyEntityAllTimeResponseJSON = {
|
|
247
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
248
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
249
|
+
entity: entity,
|
|
250
|
+
sections: { ...EMPTY, ...sections },
|
|
251
|
+
isScanLimited: isScanLimited,
|
|
252
|
+
};
|
|
253
|
+
return asJSON(response);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pageResponse(
|
|
257
|
+
section: TopologyConnectionSection,
|
|
258
|
+
connections: TopologyConnectionSectionJSON,
|
|
259
|
+
): JSONObject {
|
|
260
|
+
const response: TopologyEntityAllTimeConnectionsResponseJSON = {
|
|
261
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
262
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
263
|
+
section: section,
|
|
264
|
+
connections: connections,
|
|
265
|
+
isScanLimited: false,
|
|
266
|
+
};
|
|
267
|
+
return asJSON(response);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/*
|
|
271
|
+
* A namespace with far more pods than one page: the first PAGE of 57 rows,
|
|
272
|
+
* one of them long gone from inventory.
|
|
273
|
+
*/
|
|
274
|
+
const HUB_TOTAL: number = PAGE * 2 + 7;
|
|
275
|
+
|
|
276
|
+
function hubResponse(): JSONObject {
|
|
277
|
+
return entityResponse(entityRow(), {
|
|
278
|
+
calledBy: sectionOf([
|
|
279
|
+
connection({
|
|
280
|
+
relationshipType: "depends-on",
|
|
281
|
+
otherKey: "svc-billing",
|
|
282
|
+
otherId: "3c3c0000-0000-4000-8000-000000000001",
|
|
283
|
+
otherName: "billing",
|
|
284
|
+
otherType: EntityType.Service,
|
|
285
|
+
lastSeenAt: Date.parse("2026-09-26T09:00:00.000Z"),
|
|
286
|
+
}),
|
|
287
|
+
]),
|
|
288
|
+
related: sectionOf(
|
|
289
|
+
[
|
|
290
|
+
connection({
|
|
291
|
+
relationshipType: "part-of",
|
|
292
|
+
direction: "out",
|
|
293
|
+
otherKey: "cluster-prod",
|
|
294
|
+
otherId: "4d4d0000-0000-4000-8000-000000000001",
|
|
295
|
+
otherName: "prod",
|
|
296
|
+
otherType: EntityType.KubernetesCluster,
|
|
297
|
+
}),
|
|
298
|
+
...pods(1, PAGE - 2),
|
|
299
|
+
connection({
|
|
300
|
+
otherKey: "0123456789abcdef0123",
|
|
301
|
+
otherKnown: false,
|
|
302
|
+
otherId: null,
|
|
303
|
+
otherName: null,
|
|
304
|
+
otherType: null,
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
{ total: HUB_TOTAL, unknownTotal: 4, nextOffset: PAGE },
|
|
308
|
+
),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isPageRequest(request: PostRequest): boolean {
|
|
313
|
+
return request.url.toString().endsWith("/entity/all-time/connections");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function requests(): Array<PostRequest> {
|
|
317
|
+
return postMock.mock.calls.map((call: Array<unknown>): PostRequest => {
|
|
318
|
+
return call[0] as PostRequest;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pageRequests(): Array<PostRequest> {
|
|
323
|
+
return requests().filter(isPageRequest);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Answer each request by what it asks for, not by the order it came in. */
|
|
327
|
+
function serve(answer: (request: PostRequest) => Answer): void {
|
|
328
|
+
postMock.mockImplementation(async (...args: Array<unknown>) => {
|
|
329
|
+
const request: PostRequest = args[0] as PostRequest;
|
|
330
|
+
const result: Answer = answer(request);
|
|
331
|
+
if (result instanceof HTTPErrorResponse) {
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
return new HTTPResponse<JSONObject>(200, await result, {});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function serveHub(page?: (request: PostRequest) => Answer): void {
|
|
339
|
+
serve((request: PostRequest): Answer => {
|
|
340
|
+
if (isPageRequest(request)) {
|
|
341
|
+
if (!page) {
|
|
342
|
+
throw new Error("No page is served in this test.");
|
|
343
|
+
}
|
|
344
|
+
return page(request);
|
|
345
|
+
}
|
|
346
|
+
return hubResponse();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderPanel(overrides: Partial<ComponentProps> = {}): RenderResult {
|
|
351
|
+
return render(
|
|
352
|
+
<InventoryRelationships
|
|
353
|
+
entityKey="ns-payments"
|
|
354
|
+
entityType={EntityType.KubernetesNamespace}
|
|
355
|
+
{...overrides}
|
|
356
|
+
/>,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function waitForConnections(): Promise<void> {
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(screen.queryByTestId("component-loader")).not.toBeInTheDocument();
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function relatedRows(): Array<HTMLElement> {
|
|
367
|
+
return within(
|
|
368
|
+
screen.getByTestId("inventory-connections-related"),
|
|
369
|
+
).getAllByRole("listitem");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
postMock.mockReset();
|
|
374
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
afterEach(() => {
|
|
378
|
+
cleanup();
|
|
379
|
+
jest.restoreAllMocks();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("loading an item's connections", () => {
|
|
383
|
+
test("asks the all-time endpoint once, with the key and type and never a range", async () => {
|
|
384
|
+
const answer: Deferred = deferred();
|
|
385
|
+
serve((): Answer => {
|
|
386
|
+
return answer.promise;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
renderPanel();
|
|
390
|
+
|
|
391
|
+
expect(screen.getByTestId("component-loader")).toBeInTheDocument();
|
|
392
|
+
await act(async () => {
|
|
393
|
+
answer.resolve(hubResponse());
|
|
394
|
+
});
|
|
395
|
+
await waitForConnections();
|
|
396
|
+
|
|
397
|
+
expect(requests()).toHaveLength(1);
|
|
398
|
+
expect(requests()[0]!.url.toString()).toMatch(
|
|
399
|
+
/\/telemetry\/topology\/entity\/all-time$/,
|
|
400
|
+
);
|
|
401
|
+
expect(requests()[0]!.data).toEqual({
|
|
402
|
+
entityKey: "ns-payments",
|
|
403
|
+
entityType: EntityType.KubernetesNamespace,
|
|
404
|
+
});
|
|
405
|
+
expect(requests()[0]!.options.signal).toBeInstanceOf(AbortSignal);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("every non-empty section shows its exact total, in the drawer's order", async () => {
|
|
409
|
+
serveHub();
|
|
410
|
+
renderPanel();
|
|
411
|
+
await waitForConnections();
|
|
412
|
+
|
|
413
|
+
const headings: Array<string> = screen
|
|
414
|
+
.getAllByRole("heading", { level: 3 })
|
|
415
|
+
.map((heading: HTMLElement): string => {
|
|
416
|
+
return heading.textContent || "";
|
|
417
|
+
});
|
|
418
|
+
expect(headings).toEqual([
|
|
419
|
+
"Called by (1)",
|
|
420
|
+
`Related (${HUB_TOTAL.toLocaleString()})`,
|
|
421
|
+
]);
|
|
422
|
+
// Sections with nothing in them are not rendered at all.
|
|
423
|
+
expect(
|
|
424
|
+
screen.queryByTestId("inventory-connections-calls"),
|
|
425
|
+
).not.toBeInTheDocument();
|
|
426
|
+
expect(
|
|
427
|
+
screen.queryByTestId("inventory-connections-runsOn"),
|
|
428
|
+
).not.toBeInTheDocument();
|
|
429
|
+
expect(relatedRows()).toHaveLength(PAGE);
|
|
430
|
+
expect(
|
|
431
|
+
within(screen.getByTestId("inventory-connections-related")).getByText(
|
|
432
|
+
`Showing ${PAGE} of ${HUB_TOTAL}`,
|
|
433
|
+
),
|
|
434
|
+
).toBeInTheDocument();
|
|
435
|
+
expect(
|
|
436
|
+
screen.queryByTestId("inventory-connections-scan-limited"),
|
|
437
|
+
).not.toBeInTheDocument();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("a total the server stopped counting reads as a lower bound", async () => {
|
|
441
|
+
serve((): Answer => {
|
|
442
|
+
return entityResponse(
|
|
443
|
+
entityRow(),
|
|
444
|
+
{
|
|
445
|
+
related: sectionOf(pods(1, PAGE), {
|
|
446
|
+
total: TopologyApiLimits.EntityConnectionScanLimit,
|
|
447
|
+
nextOffset: PAGE,
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
true,
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
renderPanel();
|
|
454
|
+
await waitForConnections();
|
|
455
|
+
|
|
456
|
+
const total: string = `${TopologyApiLimits.EntityConnectionScanLimit.toLocaleString()}+`;
|
|
457
|
+
expect(
|
|
458
|
+
screen.getByRole("heading", { name: `Related (${total})` }),
|
|
459
|
+
).toBeInTheDocument();
|
|
460
|
+
expect(screen.getByText(`Showing ${PAGE} of ${total}`)).toBeInTheDocument();
|
|
461
|
+
expect(
|
|
462
|
+
screen.getByTestId("inventory-connections-scan-limited"),
|
|
463
|
+
).toHaveTextContent("totals are lower bounds");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("an item with no connections explains where they come from", async () => {
|
|
467
|
+
serve((): Answer => {
|
|
468
|
+
return entityResponse(entityRow());
|
|
469
|
+
});
|
|
470
|
+
renderPanel();
|
|
471
|
+
await waitForConnections();
|
|
472
|
+
|
|
473
|
+
expect(
|
|
474
|
+
screen.getByText(/Nothing is connected to this item yet/),
|
|
475
|
+
).toBeInTheDocument();
|
|
476
|
+
expect(screen.queryByRole("heading", { level: 3 })).not.toBeInTheDocument();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("an item the inventory no longer has says so", async () => {
|
|
480
|
+
serve((): Answer => {
|
|
481
|
+
return entityResponse(null);
|
|
482
|
+
});
|
|
483
|
+
renderPanel();
|
|
484
|
+
await waitForConnections();
|
|
485
|
+
|
|
486
|
+
expect(
|
|
487
|
+
screen.getByText(/This item is no longer in the inventory/),
|
|
488
|
+
).toBeInTheDocument();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("each row", () => {
|
|
493
|
+
test("reads from this item's end and links to the item at the other end", async () => {
|
|
494
|
+
serveHub();
|
|
495
|
+
renderPanel();
|
|
496
|
+
await waitForConnections();
|
|
497
|
+
|
|
498
|
+
const [cluster, firstPod] = relatedRows();
|
|
499
|
+
// The stored edge is ns → cluster (part-of) and pod → ns (part-of).
|
|
500
|
+
expect(cluster).toHaveTextContent("is part of");
|
|
501
|
+
expect(firstPod).toHaveTextContent("contains");
|
|
502
|
+
|
|
503
|
+
const clusterLink: HTMLElement = within(cluster!).getByRole("link", {
|
|
504
|
+
name: "prod",
|
|
505
|
+
});
|
|
506
|
+
expect(clusterLink.getAttribute("href")).toBe(
|
|
507
|
+
`/dashboard/${PROJECT_ID.toString()}/inventory/item/4d4d0000-0000-4000-8000-000000000001`,
|
|
508
|
+
);
|
|
509
|
+
expect(
|
|
510
|
+
within(cluster!).getByText("Kubernetes Cluster"),
|
|
511
|
+
).toBeInTheDocument();
|
|
512
|
+
|
|
513
|
+
const caller: HTMLElement = within(
|
|
514
|
+
screen.getByTestId("inventory-connections-calledBy"),
|
|
515
|
+
).getByRole("listitem");
|
|
516
|
+
expect(caller).toHaveTextContent("is depended on by");
|
|
517
|
+
expect(
|
|
518
|
+
within(caller).getByRole("link", { name: "billing" }),
|
|
519
|
+
).toBeInTheDocument();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("an end no longer in inventory shows its key, unlinked, and the section counts it", async () => {
|
|
523
|
+
serveHub();
|
|
524
|
+
renderPanel();
|
|
525
|
+
await waitForConnections();
|
|
526
|
+
|
|
527
|
+
const gone: HTMLElement = relatedRows()[PAGE - 1]!;
|
|
528
|
+
expect(within(gone).queryByRole("link")).not.toBeInTheDocument();
|
|
529
|
+
// The first 16 characters of the key: the connection is still real.
|
|
530
|
+
expect(within(gone).getByText("0123456789abcdef")).toBeInTheDocument();
|
|
531
|
+
expect(
|
|
532
|
+
within(screen.getByTestId("inventory-connections-related")).getByText(
|
|
533
|
+
"4 no longer in inventory",
|
|
534
|
+
),
|
|
535
|
+
).toBeInTheDocument();
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe("show more", () => {
|
|
540
|
+
test("fetches the next page of that section from where it stopped, appends it and focuses its first row", async () => {
|
|
541
|
+
serveHub((request: PostRequest): Answer => {
|
|
542
|
+
const offset: number = request.data["offset"] as number;
|
|
543
|
+
return pageResponse(
|
|
544
|
+
"related",
|
|
545
|
+
sectionOf(pods(offset, offset + PAGE - 1), {
|
|
546
|
+
total: HUB_TOTAL,
|
|
547
|
+
unknownTotal: 4,
|
|
548
|
+
nextOffset: offset + PAGE,
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
renderPanel();
|
|
553
|
+
await waitForConnections();
|
|
554
|
+
|
|
555
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
556
|
+
|
|
557
|
+
await waitFor(() => {
|
|
558
|
+
expect(relatedRows()).toHaveLength(PAGE * 2);
|
|
559
|
+
});
|
|
560
|
+
expect(pageRequests()).toHaveLength(1);
|
|
561
|
+
expect(pageRequests()[0]!.url.toString()).toMatch(
|
|
562
|
+
/\/telemetry\/topology\/entity\/all-time\/connections$/,
|
|
563
|
+
);
|
|
564
|
+
expect(pageRequests()[0]!.data).toEqual({
|
|
565
|
+
entityKey: "ns-payments",
|
|
566
|
+
entityType: EntityType.KubernetesNamespace,
|
|
567
|
+
section: "related",
|
|
568
|
+
offset: PAGE,
|
|
569
|
+
limit: PAGE,
|
|
570
|
+
});
|
|
571
|
+
expect(
|
|
572
|
+
screen.getByText(`Showing ${PAGE * 2} of ${HUB_TOTAL}`),
|
|
573
|
+
).toBeInTheDocument();
|
|
574
|
+
await waitFor(() => {
|
|
575
|
+
expect(document.activeElement).toBe(relatedRows()[PAGE]);
|
|
576
|
+
});
|
|
577
|
+
// Only that section was fetched again; the item was not.
|
|
578
|
+
expect(requests()).toHaveLength(2);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("the last page takes the control away", async () => {
|
|
582
|
+
serveHub((): Answer => {
|
|
583
|
+
return pageResponse(
|
|
584
|
+
"related",
|
|
585
|
+
sectionOf(pods(PAGE, HUB_TOTAL - 1), {
|
|
586
|
+
total: HUB_TOTAL,
|
|
587
|
+
unknownTotal: 4,
|
|
588
|
+
nextOffset: null,
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
renderPanel();
|
|
593
|
+
await waitForConnections();
|
|
594
|
+
|
|
595
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
596
|
+
|
|
597
|
+
await waitFor(() => {
|
|
598
|
+
expect(relatedRows()).toHaveLength(HUB_TOTAL);
|
|
599
|
+
});
|
|
600
|
+
expect(
|
|
601
|
+
screen.queryByRole("button", { name: "Show more: Related" }),
|
|
602
|
+
).not.toBeInTheDocument();
|
|
603
|
+
expect(screen.queryByText(/^Showing/)).not.toBeInTheDocument();
|
|
604
|
+
expect(
|
|
605
|
+
screen.queryByTestId("inventory-connections-related-changed"),
|
|
606
|
+
).not.toBeInTheDocument();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("is busy while its page loads, and a second click asks nothing more", async () => {
|
|
610
|
+
const answer: Deferred = deferred();
|
|
611
|
+
serveHub((): Answer => {
|
|
612
|
+
return answer.promise;
|
|
613
|
+
});
|
|
614
|
+
renderPanel();
|
|
615
|
+
await waitForConnections();
|
|
616
|
+
|
|
617
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
618
|
+
name: "Show more: Related",
|
|
619
|
+
});
|
|
620
|
+
fireEvent.click(button);
|
|
621
|
+
expect(button).toBeDisabled();
|
|
622
|
+
expect(button).toHaveAttribute("aria-busy", "true");
|
|
623
|
+
expect(button).toHaveTextContent("Loading…");
|
|
624
|
+
fireEvent.click(button);
|
|
625
|
+
expect(pageRequests()).toHaveLength(1);
|
|
626
|
+
|
|
627
|
+
await act(async () => {
|
|
628
|
+
answer.resolve(
|
|
629
|
+
pageResponse(
|
|
630
|
+
"related",
|
|
631
|
+
sectionOf(pods(PAGE, PAGE * 2 - 1), {
|
|
632
|
+
total: HUB_TOTAL,
|
|
633
|
+
unknownTotal: 4,
|
|
634
|
+
nextOffset: PAGE * 2,
|
|
635
|
+
}),
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
expect(
|
|
640
|
+
screen.getByRole("button", { name: "Show more: Related" }),
|
|
641
|
+
).not.toBeDisabled();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("a list that changed while paging says so, and its reload replaces the section from the start", async () => {
|
|
645
|
+
serveHub((request: PostRequest): Answer => {
|
|
646
|
+
if (request.data["offset"] === 0) {
|
|
647
|
+
return pageResponse(
|
|
648
|
+
"related",
|
|
649
|
+
sectionOf(pods(1, HUB_TOTAL - 10), {
|
|
650
|
+
total: HUB_TOTAL - 10,
|
|
651
|
+
nextOffset: null,
|
|
652
|
+
}),
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
// Ten pods were pruned above the offset: the list shrank under us.
|
|
656
|
+
return pageResponse(
|
|
657
|
+
"related",
|
|
658
|
+
sectionOf(pods(PAGE + 10, PAGE * 2 + 9), {
|
|
659
|
+
total: HUB_TOTAL - 10,
|
|
660
|
+
unknownTotal: 4,
|
|
661
|
+
nextOffset: PAGE * 2,
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
renderPanel();
|
|
666
|
+
await waitForConnections();
|
|
667
|
+
|
|
668
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
669
|
+
|
|
670
|
+
const reload: HTMLElement = await screen.findByRole("button", {
|
|
671
|
+
name: "Reload list: Related",
|
|
672
|
+
});
|
|
673
|
+
expect(
|
|
674
|
+
screen.getByTestId("inventory-connections-related-changed"),
|
|
675
|
+
).toHaveTextContent("This list changed while you were browsing.");
|
|
676
|
+
// The rows that did arrive are still shown.
|
|
677
|
+
expect(relatedRows()).toHaveLength(PAGE * 2);
|
|
678
|
+
|
|
679
|
+
fireEvent.click(reload);
|
|
680
|
+
|
|
681
|
+
await waitFor(() => {
|
|
682
|
+
expect(relatedRows()).toHaveLength(HUB_TOTAL - 10);
|
|
683
|
+
});
|
|
684
|
+
expect(pageRequests()[1]!.data).toEqual({
|
|
685
|
+
entityKey: "ns-payments",
|
|
686
|
+
entityType: EntityType.KubernetesNamespace,
|
|
687
|
+
section: "related",
|
|
688
|
+
offset: 0,
|
|
689
|
+
limit: Math.min(
|
|
690
|
+
TopologyApiLimits.EntityConnectionsPageSizeMax,
|
|
691
|
+
PAGE * 2 + PAGE,
|
|
692
|
+
),
|
|
693
|
+
});
|
|
694
|
+
expect(
|
|
695
|
+
screen.queryByTestId("inventory-connections-related-changed"),
|
|
696
|
+
).not.toBeInTheDocument();
|
|
697
|
+
expect(
|
|
698
|
+
screen.getByRole("heading", {
|
|
699
|
+
name: `Related (${(HUB_TOTAL - 10).toLocaleString()})`,
|
|
700
|
+
}),
|
|
701
|
+
).toBeInTheDocument();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("a failed page says so, keeps what is shown, and can be tried again", async () => {
|
|
705
|
+
let fail: boolean = true;
|
|
706
|
+
serveHub((): Answer => {
|
|
707
|
+
if (fail) {
|
|
708
|
+
return new HTTPErrorResponse(
|
|
709
|
+
500,
|
|
710
|
+
{ message: "The database is having a moment." },
|
|
711
|
+
{},
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return pageResponse(
|
|
715
|
+
"related",
|
|
716
|
+
sectionOf(pods(PAGE, PAGE * 2 - 1), {
|
|
717
|
+
total: HUB_TOTAL,
|
|
718
|
+
unknownTotal: 4,
|
|
719
|
+
nextOffset: PAGE * 2,
|
|
720
|
+
}),
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
renderPanel();
|
|
724
|
+
await waitForConnections();
|
|
725
|
+
|
|
726
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
727
|
+
|
|
728
|
+
const alert: HTMLElement = await screen.findByRole("alert");
|
|
729
|
+
expect(alert).toHaveTextContent(
|
|
730
|
+
"Could not load more connections. The database is having a moment.",
|
|
731
|
+
);
|
|
732
|
+
expect(relatedRows()).toHaveLength(PAGE);
|
|
733
|
+
|
|
734
|
+
fail = false;
|
|
735
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
736
|
+
await waitFor(() => {
|
|
737
|
+
expect(relatedRows()).toHaveLength(PAGE * 2);
|
|
738
|
+
});
|
|
739
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("a server that no longer speaks this format offers a page reload instead", async () => {
|
|
743
|
+
const reload: SpyInstance<() => void> = jest
|
|
744
|
+
.spyOn(Navigation, "reload")
|
|
745
|
+
.mockImplementation(() => {
|
|
746
|
+
return undefined;
|
|
747
|
+
});
|
|
748
|
+
serveHub((): Answer => {
|
|
749
|
+
return new HTTPErrorResponse(404, { message: "Not found" }, {});
|
|
750
|
+
});
|
|
751
|
+
renderPanel();
|
|
752
|
+
await waitForConnections();
|
|
753
|
+
|
|
754
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Related" }));
|
|
755
|
+
|
|
756
|
+
const alert: HTMLElement = await screen.findByRole("alert");
|
|
757
|
+
expect(alert).toHaveTextContent("Topology was updated. Reload the page.");
|
|
758
|
+
expect(
|
|
759
|
+
screen.queryByRole("button", { name: "Show more: Related" }),
|
|
760
|
+
).not.toBeInTheDocument();
|
|
761
|
+
fireEvent.click(within(alert).getByRole("button", { name: "Reload page" }));
|
|
762
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
describe("a failed load", () => {
|
|
767
|
+
test("says what went wrong and asks again on refresh", async () => {
|
|
768
|
+
let fail: boolean = true;
|
|
769
|
+
serve((): Answer => {
|
|
770
|
+
if (fail) {
|
|
771
|
+
return new HTTPErrorResponse(
|
|
772
|
+
500,
|
|
773
|
+
{ message: "Postgres is unavailable." },
|
|
774
|
+
{},
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
return hubResponse();
|
|
778
|
+
});
|
|
779
|
+
renderPanel();
|
|
780
|
+
|
|
781
|
+
expect(
|
|
782
|
+
await screen.findByText(
|
|
783
|
+
"Could not load this item's connections. Postgres is unavailable.",
|
|
784
|
+
),
|
|
785
|
+
).toBeInTheDocument();
|
|
786
|
+
|
|
787
|
+
fail = false;
|
|
788
|
+
fireEvent.click(screen.getByTestId("refresh-button"));
|
|
789
|
+
|
|
790
|
+
await waitFor(() => {
|
|
791
|
+
expect(relatedRows()).toHaveLength(PAGE);
|
|
792
|
+
});
|
|
793
|
+
expect(requests()).toHaveLength(2);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("a busy server says to try again in a moment", async () => {
|
|
797
|
+
serve((): Answer => {
|
|
798
|
+
return new HTTPErrorResponse(429, { message: "Too many requests" }, {});
|
|
799
|
+
});
|
|
800
|
+
renderPanel();
|
|
801
|
+
|
|
802
|
+
expect(
|
|
803
|
+
await screen.findByText(
|
|
804
|
+
"Could not load this item's connections. The topology service is busy. Try again in a moment.",
|
|
805
|
+
),
|
|
806
|
+
).toBeInTheDocument();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("an outdated bundle reloads the page instead of asking again", async () => {
|
|
810
|
+
const reload: SpyInstance<() => void> = jest
|
|
811
|
+
.spyOn(Navigation, "reload")
|
|
812
|
+
.mockImplementation(() => {
|
|
813
|
+
return undefined;
|
|
814
|
+
});
|
|
815
|
+
serve((): Answer => {
|
|
816
|
+
return new HTTPErrorResponse(404, { message: "Not found" }, {});
|
|
817
|
+
});
|
|
818
|
+
renderPanel();
|
|
819
|
+
|
|
820
|
+
await screen.findByText(
|
|
821
|
+
"Could not load this item's connections. Topology was updated. Reload the page.",
|
|
822
|
+
);
|
|
823
|
+
fireEvent.click(screen.getByTestId("refresh-button"));
|
|
824
|
+
|
|
825
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
826
|
+
expect(requests()).toHaveLength(1);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
describe("moving to another item", () => {
|
|
831
|
+
test("drops the previous item's request and never shows its rows", async () => {
|
|
832
|
+
const first: Deferred = deferred();
|
|
833
|
+
serve((request: PostRequest): Answer => {
|
|
834
|
+
if (request.data["entityKey"] === "ns-payments") {
|
|
835
|
+
return first.promise;
|
|
836
|
+
}
|
|
837
|
+
return entityResponse(entityRow({ key: "ns-orders", name: "orders" }), {
|
|
838
|
+
related: sectionOf([pod(900)]),
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const rendered: RenderResult = renderPanel();
|
|
843
|
+
rendered.rerender(
|
|
844
|
+
<InventoryRelationships
|
|
845
|
+
entityKey="ns-orders"
|
|
846
|
+
entityType={EntityType.KubernetesNamespace}
|
|
847
|
+
/>,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
await waitFor(() => {
|
|
851
|
+
expect(relatedRows()).toHaveLength(1);
|
|
852
|
+
});
|
|
853
|
+
expect(requests()[0]!.options.signal!.aborted).toBe(true);
|
|
854
|
+
|
|
855
|
+
// The first item's answer arrives late: it is ignored.
|
|
856
|
+
await act(async () => {
|
|
857
|
+
first.resolve(hubResponse());
|
|
858
|
+
});
|
|
859
|
+
expect(relatedRows()).toHaveLength(1);
|
|
860
|
+
expect(
|
|
861
|
+
screen.getByRole("link", { name: "api-pod-900" }),
|
|
862
|
+
).toBeInTheDocument();
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe("the card", () => {
|
|
867
|
+
test("offers the full map, and drops its frame when the page has its own", async () => {
|
|
868
|
+
serveHub();
|
|
869
|
+
const rendered: RenderResult = renderPanel({
|
|
870
|
+
fullMapRoute: new Route("/dashboard/project/topology?focus=ns-payments"),
|
|
871
|
+
});
|
|
872
|
+
await waitForConnections();
|
|
873
|
+
|
|
874
|
+
expect(screen.getByText("Connections")).toBeInTheDocument();
|
|
875
|
+
expect(
|
|
876
|
+
screen.getByRole("link", { name: "Open full map" }).getAttribute("href"),
|
|
877
|
+
).toBe("/dashboard/project/topology?focus=ns-payments");
|
|
878
|
+
|
|
879
|
+
rendered.unmount();
|
|
880
|
+
renderPanel({ showCard: false });
|
|
881
|
+
await waitForConnections();
|
|
882
|
+
expect(screen.queryByText("Connections")).not.toBeInTheDocument();
|
|
883
|
+
expect(relatedRows()).toHaveLength(PAGE);
|
|
884
|
+
});
|
|
885
|
+
});
|