@oneuptime/common 14.0.2 → 14.0.3
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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
jest,
|
|
7
|
+
test,
|
|
8
|
+
} from "@jest/globals";
|
|
9
|
+
import "@testing-library/jest-dom";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
RenderResult,
|
|
16
|
+
screen,
|
|
17
|
+
within,
|
|
18
|
+
} from "@testing-library/react";
|
|
19
|
+
import React from "react";
|
|
20
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* The Docker Swarm cluster pages, rendered for real in jsdom with only the
|
|
24
|
+
* API and the router replaced: every count, field and column header that
|
|
25
|
+
* shows a number has an (i), hovering it shows the text from
|
|
26
|
+
* DockerSwarmMetricDescriptions, and the (i) never sits inside another
|
|
27
|
+
* control. The overview tiles used to be <button>s; they are now cards with
|
|
28
|
+
* an overlay button, so the (i) can be reached and does not navigate.
|
|
29
|
+
*
|
|
30
|
+
* Tippy under jsdom (see InfoTooltip.test.tsx): the popup is portalled to
|
|
31
|
+
* document.body and queried through `screen`; it is lazy, so fake timers
|
|
32
|
+
* are advanced after the hover.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const CLUSTER_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
36
|
+
|
|
37
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
38
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
39
|
+
const navigateMock: MockFunction = getJestMockFunction();
|
|
40
|
+
let lastParam: string = "";
|
|
41
|
+
|
|
42
|
+
jest.mock("react-i18next", () => {
|
|
43
|
+
return {
|
|
44
|
+
useTranslation: () => {
|
|
45
|
+
return {
|
|
46
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
47
|
+
return options?.defaultValue ?? key;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
55
|
+
return {
|
|
56
|
+
__esModule: true,
|
|
57
|
+
default: {
|
|
58
|
+
getItem: (...args: Array<unknown>) => {
|
|
59
|
+
return getItemMock(...args);
|
|
60
|
+
},
|
|
61
|
+
getList: (...args: Array<unknown>) => {
|
|
62
|
+
return getListMock(...args);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
jest.mock("../../../UI/Utils/Navigation", () => {
|
|
69
|
+
const ObjectIDClass: typeof import("../../../Types/ObjectID").default = (
|
|
70
|
+
jest.requireActual("../../../Types/ObjectID") as {
|
|
71
|
+
default: typeof import("../../../Types/ObjectID").default;
|
|
72
|
+
}
|
|
73
|
+
).default;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
__esModule: true,
|
|
77
|
+
default: {
|
|
78
|
+
getLastParamAsObjectID: () => {
|
|
79
|
+
return new ObjectIDClass(CLUSTER_ID);
|
|
80
|
+
},
|
|
81
|
+
getLastParamAsString: () => {
|
|
82
|
+
return lastParam;
|
|
83
|
+
},
|
|
84
|
+
// RouteMap asks for the project from the URL; there is none here.
|
|
85
|
+
getFirstParam: (): undefined => {
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
navigate: (...args: Array<unknown>) => {
|
|
89
|
+
navigateMock(...args);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* The auto-refresh control has its own tests; here it would only add a
|
|
97
|
+
* 30-second timer to every render.
|
|
98
|
+
*/
|
|
99
|
+
jest.mock(
|
|
100
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/AutoRefreshControl",
|
|
101
|
+
() => {
|
|
102
|
+
return {
|
|
103
|
+
__esModule: true,
|
|
104
|
+
default: (): null => {
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
jest.mock(
|
|
112
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/useAutoRefresh",
|
|
113
|
+
() => {
|
|
114
|
+
return {
|
|
115
|
+
__esModule: true,
|
|
116
|
+
default: () => {
|
|
117
|
+
return {
|
|
118
|
+
autoRefreshInterval: "off",
|
|
119
|
+
setAutoRefreshInterval: (): void => {},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
import DockerSwarmClusterOverview, {
|
|
127
|
+
SwarmCountTile,
|
|
128
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Index";
|
|
129
|
+
import DockerSwarmClusterTaskDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/TaskDetail";
|
|
130
|
+
import DockerSwarmClusterServiceDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/ServiceDetail";
|
|
131
|
+
import DockerSwarmClusterNodeDetail from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/NodeDetail";
|
|
132
|
+
import DockerSwarmClusterTasks from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Tasks";
|
|
133
|
+
import DockerSwarmClusterNodes from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Nodes";
|
|
134
|
+
import DockerSwarmClusterServices from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Services";
|
|
135
|
+
import DockerSwarmClusterStacks from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/View/Stacks";
|
|
136
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
137
|
+
import { DOCKER_SWARM_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/DockerSwarmMetricDescriptions";
|
|
138
|
+
import DockerSwarmCluster from "../../../Models/DatabaseModels/DockerSwarmCluster";
|
|
139
|
+
import DockerSwarmResource from "../../../Models/DatabaseModels/DockerSwarmResource";
|
|
140
|
+
import Route from "../../../Types/API/Route";
|
|
141
|
+
|
|
142
|
+
const T: typeof DOCKER_SWARM_METRIC_DESCRIPTIONS =
|
|
143
|
+
DOCKER_SWARM_METRIC_DESCRIPTIONS;
|
|
144
|
+
|
|
145
|
+
const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
|
|
146
|
+
|
|
147
|
+
type RowInit = Partial<{
|
|
148
|
+
kind: string;
|
|
149
|
+
externalId: string;
|
|
150
|
+
name: string;
|
|
151
|
+
state: string;
|
|
152
|
+
role: string;
|
|
153
|
+
isReady: boolean;
|
|
154
|
+
serviceMode: string;
|
|
155
|
+
runningReplicas: number;
|
|
156
|
+
desiredReplicas: number;
|
|
157
|
+
serviceName: string;
|
|
158
|
+
nodeHostname: string;
|
|
159
|
+
image: string;
|
|
160
|
+
latestCpuPercent: number;
|
|
161
|
+
latestMemoryBytes: number;
|
|
162
|
+
metricsUpdatedAt: Date;
|
|
163
|
+
lastSeenAt: Date;
|
|
164
|
+
attributes: Record<string, unknown>;
|
|
165
|
+
}>;
|
|
166
|
+
|
|
167
|
+
function row(init: RowInit): DockerSwarmResource {
|
|
168
|
+
const model: DockerSwarmResource = new DockerSwarmResource();
|
|
169
|
+
Object.assign(model, init);
|
|
170
|
+
return model;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cluster(): DockerSwarmCluster {
|
|
174
|
+
const model: DockerSwarmCluster = new DockerSwarmCluster();
|
|
175
|
+
model.name = "prod-swarm";
|
|
176
|
+
model.otelCollectorStatus = "connected";
|
|
177
|
+
model.dockerVersion = "27.1.1";
|
|
178
|
+
model.agentVersion = "0.161.0";
|
|
179
|
+
return model;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const INVENTORY: Array<DockerSwarmResource> = [
|
|
183
|
+
row({
|
|
184
|
+
kind: "Node",
|
|
185
|
+
externalId: "node/a",
|
|
186
|
+
name: "manager-1",
|
|
187
|
+
state: "ready",
|
|
188
|
+
role: "manager",
|
|
189
|
+
isReady: true,
|
|
190
|
+
}),
|
|
191
|
+
row({
|
|
192
|
+
kind: "Node",
|
|
193
|
+
externalId: "node/b",
|
|
194
|
+
name: "worker-1",
|
|
195
|
+
state: "ready",
|
|
196
|
+
role: "worker",
|
|
197
|
+
isReady: true,
|
|
198
|
+
}),
|
|
199
|
+
row({
|
|
200
|
+
kind: "Service",
|
|
201
|
+
externalId: "service/web",
|
|
202
|
+
name: "shop_web",
|
|
203
|
+
state: "2/3",
|
|
204
|
+
runningReplicas: 2,
|
|
205
|
+
desiredReplicas: 3,
|
|
206
|
+
isReady: false,
|
|
207
|
+
}),
|
|
208
|
+
row({
|
|
209
|
+
kind: "Task",
|
|
210
|
+
externalId: "task/t1",
|
|
211
|
+
name: "shop_web.1",
|
|
212
|
+
state: "running",
|
|
213
|
+
isReady: true,
|
|
214
|
+
}),
|
|
215
|
+
row({
|
|
216
|
+
kind: "Task",
|
|
217
|
+
externalId: "task/t2",
|
|
218
|
+
name: "shop_web.2",
|
|
219
|
+
state: "starting",
|
|
220
|
+
isReady: false,
|
|
221
|
+
}),
|
|
222
|
+
row({ kind: "Stack", externalId: "stack/shop", name: "shop" }),
|
|
223
|
+
row({ kind: "Network", externalId: "network/n1", name: "shop_default" }),
|
|
224
|
+
row({ kind: "Volume", externalId: "volume/data@manager-1", name: "data" }),
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
function listResult(data: Array<DockerSwarmResource>): {
|
|
228
|
+
data: Array<DockerSwarmResource>;
|
|
229
|
+
count: number;
|
|
230
|
+
skip: number;
|
|
231
|
+
limit: number;
|
|
232
|
+
} {
|
|
233
|
+
return { data, count: data.length, skip: 0, limit: data.length };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function flush(): Promise<void> {
|
|
237
|
+
for (let i: number = 0; i < 5; i++) {
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await Promise.resolve();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
245
|
+
fireEvent.mouseEnter(trigger);
|
|
246
|
+
await act(async () => {
|
|
247
|
+
jest.advanceTimersByTime(200);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function expectTooltip(name: string, text: string): Promise<void> {
|
|
252
|
+
const buttons: Array<HTMLElement> = screen.getAllByRole("button", {
|
|
253
|
+
name: `About ${name}`,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
for (const button of buttons) {
|
|
257
|
+
await hover(button);
|
|
258
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(text);
|
|
259
|
+
fireEvent.mouseLeave(button);
|
|
260
|
+
await act(async () => {
|
|
261
|
+
jest.advanceTimersByTime(1000);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function expectNoNestedControls(root: HTMLElement): void {
|
|
267
|
+
expect(root.querySelectorAll("button button")).toHaveLength(0);
|
|
268
|
+
expect(root.querySelectorAll("a button")).toHaveLength(0);
|
|
269
|
+
expect(root.querySelectorAll("button a")).toHaveLength(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
jest.useFakeTimers();
|
|
274
|
+
getItemMock.mockReset();
|
|
275
|
+
getListMock.mockReset();
|
|
276
|
+
navigateMock.mockReset();
|
|
277
|
+
lastParam = "";
|
|
278
|
+
getItemMock.mockImplementation(async () => {
|
|
279
|
+
return cluster();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
afterEach(() => {
|
|
284
|
+
cleanup();
|
|
285
|
+
jest.useRealTimers();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("SwarmCountTile", () => {
|
|
289
|
+
function renderTile(
|
|
290
|
+
props: Partial<React.ComponentProps<typeof SwarmCountTile>> = {},
|
|
291
|
+
): { onOpen: MockFunction; result: RenderResult } {
|
|
292
|
+
const onOpen: MockFunction = getJestMockFunction();
|
|
293
|
+
const result: RenderResult = render(
|
|
294
|
+
<SwarmCountTile
|
|
295
|
+
label="Tasks"
|
|
296
|
+
value={12}
|
|
297
|
+
subline="10/12 running"
|
|
298
|
+
description={T.tasks}
|
|
299
|
+
onOpen={() => {
|
|
300
|
+
onOpen();
|
|
301
|
+
}}
|
|
302
|
+
{...props}
|
|
303
|
+
/>,
|
|
304
|
+
);
|
|
305
|
+
return { onOpen, result };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
test("shows the label, the count, the subline and an (i) named after the label", () => {
|
|
309
|
+
renderTile();
|
|
310
|
+
|
|
311
|
+
expect(screen.getByText("Tasks")).toBeInTheDocument();
|
|
312
|
+
expect(screen.getByText("12")).toBeInTheDocument();
|
|
313
|
+
expect(screen.getByText("10/12 running")).toBeInTheDocument();
|
|
314
|
+
expect(
|
|
315
|
+
screen.getByRole("button", { name: "About Tasks" }),
|
|
316
|
+
).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("hovering the (i) explains the count", async () => {
|
|
320
|
+
renderTile();
|
|
321
|
+
|
|
322
|
+
await expectTooltip("Tasks", T.tasks);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("the whole card still opens the list through an overlay button", () => {
|
|
326
|
+
const { onOpen } = renderTile();
|
|
327
|
+
|
|
328
|
+
fireEvent.click(screen.getByRole("button", { name: "View Tasks" }));
|
|
329
|
+
|
|
330
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("asking what the count means does not open the list - by click, Enter or Space", () => {
|
|
334
|
+
const { onOpen } = renderTile();
|
|
335
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
336
|
+
name: "About Tasks",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
fireEvent.click(info);
|
|
340
|
+
fireEvent.keyDown(info, { key: "Enter" });
|
|
341
|
+
fireEvent.keyDown(info, { key: " " });
|
|
342
|
+
|
|
343
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("the (i) is a sibling of the overlay, lifted above it, never inside it", () => {
|
|
347
|
+
const { result } = renderTile();
|
|
348
|
+
const overlay: HTMLElement = screen.getByRole("button", {
|
|
349
|
+
name: "View Tasks",
|
|
350
|
+
});
|
|
351
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
352
|
+
name: "About Tasks",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expectNoNestedControls(result.container);
|
|
356
|
+
expect(overlay).not.toContainElement(info);
|
|
357
|
+
expect(overlay.className).toContain("absolute inset-0");
|
|
358
|
+
expect(info.className).toContain("relative z-10");
|
|
359
|
+
expect(overlay.parentElement!.className).toContain("relative");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("no description, no (i) - and the tile still works", () => {
|
|
363
|
+
const { onOpen } = renderTile({ description: undefined, subline: null });
|
|
364
|
+
|
|
365
|
+
expect(
|
|
366
|
+
screen.queryByRole("button", { name: "About Tasks" }),
|
|
367
|
+
).not.toBeInTheDocument();
|
|
368
|
+
expect(screen.queryByText("10/12 running")).not.toBeInTheDocument();
|
|
369
|
+
|
|
370
|
+
fireEvent.click(screen.getByRole("button", { name: "View Tasks" }));
|
|
371
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Docker Swarm overview page", () => {
|
|
376
|
+
async function renderOverview(): Promise<RenderResult> {
|
|
377
|
+
getListMock.mockImplementation(async () => {
|
|
378
|
+
return listResult(INVENTORY);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result: RenderResult = render(
|
|
382
|
+
<DockerSwarmClusterOverview {...PAGE_PROPS} />,
|
|
383
|
+
);
|
|
384
|
+
await flush();
|
|
385
|
+
|
|
386
|
+
expect(screen.getByText("prod-swarm")).toBeInTheDocument();
|
|
387
|
+
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const TILES: Array<[string, string]> = [
|
|
392
|
+
["Nodes", T.nodes],
|
|
393
|
+
["Tasks", T.tasks],
|
|
394
|
+
["Stacks", T.stacks],
|
|
395
|
+
["Networks", T.networks],
|
|
396
|
+
["Volumes", T.volumes],
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
test.each(TILES)(
|
|
400
|
+
"the %s tile explains itself on hover",
|
|
401
|
+
async (title: string, text: string) => {
|
|
402
|
+
await renderOverview();
|
|
403
|
+
|
|
404
|
+
await expectTooltip(title, text);
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
test("Services has one (i) on its tile and one on its hero chip, both with the same text", async () => {
|
|
409
|
+
await renderOverview();
|
|
410
|
+
|
|
411
|
+
expect(
|
|
412
|
+
screen.getAllByRole("button", { name: "About Services" }),
|
|
413
|
+
).toHaveLength(2);
|
|
414
|
+
await expectTooltip("Services", T.services);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("the hero chips that are counts are explained under a static name", async () => {
|
|
418
|
+
await renderOverview();
|
|
419
|
+
|
|
420
|
+
expect(screen.getByText("2/2 nodes ready")).toBeInTheDocument();
|
|
421
|
+
expect(screen.getByText("1 manager")).toBeInTheDocument();
|
|
422
|
+
expect(screen.getByText("1/2 tasks running")).toBeInTheDocument();
|
|
423
|
+
|
|
424
|
+
await expectTooltip("Nodes ready", T.nodes);
|
|
425
|
+
await expectTooltip("Managers", T.managers);
|
|
426
|
+
await expectTooltip("Tasks running", T.tasks);
|
|
427
|
+
|
|
428
|
+
// The (i) is never named after the numbers in the chip.
|
|
429
|
+
expect(
|
|
430
|
+
screen.queryByRole("button", { name: /About \d/ }),
|
|
431
|
+
).not.toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("version chips are metadata and get no (i)", async () => {
|
|
435
|
+
await renderOverview();
|
|
436
|
+
|
|
437
|
+
expect(screen.getByText("Docker 27.1.1")).toBeInTheDocument();
|
|
438
|
+
expect(screen.getByText("Agent 0.161.0")).toBeInTheDocument();
|
|
439
|
+
expect(
|
|
440
|
+
screen.queryByRole("button", { name: /About (Docker|Agent)/ }),
|
|
441
|
+
).not.toBeInTheDocument();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("tiles show the inventory counts they explain", async () => {
|
|
445
|
+
await renderOverview();
|
|
446
|
+
|
|
447
|
+
const tasksTile: HTMLElement = screen.getByRole("button", {
|
|
448
|
+
name: "View Tasks",
|
|
449
|
+
}).parentElement!;
|
|
450
|
+
|
|
451
|
+
expect(within(tasksTile).getByText("2")).toBeInTheDocument();
|
|
452
|
+
expect(within(tasksTile).getByText("1/2 running")).toBeInTheDocument();
|
|
453
|
+
|
|
454
|
+
const servicesTile: HTMLElement = screen.getByRole("button", {
|
|
455
|
+
name: "View Services",
|
|
456
|
+
}).parentElement!;
|
|
457
|
+
|
|
458
|
+
expect(within(servicesTile).getByText("0/1 converged")).toBeInTheDocument();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("every tile has exactly one (i), and there are six tiles", async () => {
|
|
462
|
+
await renderOverview();
|
|
463
|
+
|
|
464
|
+
const overlays: Array<HTMLElement> = screen.getAllByRole("button", {
|
|
465
|
+
name: /^View /,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(
|
|
469
|
+
overlays.map((overlay: HTMLElement) => {
|
|
470
|
+
return overlay.textContent;
|
|
471
|
+
}),
|
|
472
|
+
).toEqual([
|
|
473
|
+
"View Nodes",
|
|
474
|
+
"View Services",
|
|
475
|
+
"View Tasks",
|
|
476
|
+
"View Stacks",
|
|
477
|
+
"View Networks",
|
|
478
|
+
"View Volumes",
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
for (const overlay of overlays) {
|
|
482
|
+
expect(
|
|
483
|
+
within(overlay.parentElement!).getAllByRole("button", {
|
|
484
|
+
name: /^About /,
|
|
485
|
+
}),
|
|
486
|
+
).toHaveLength(1);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("no control is nested in another anywhere on the page", async () => {
|
|
491
|
+
const result: RenderResult = await renderOverview();
|
|
492
|
+
|
|
493
|
+
expectNoNestedControls(result.container);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("a tile still opens its list; its (i) does not", async () => {
|
|
497
|
+
await renderOverview();
|
|
498
|
+
|
|
499
|
+
fireEvent.click(screen.getByRole("button", { name: "About Tasks" }));
|
|
500
|
+
expect(navigateMock).not.toHaveBeenCalled();
|
|
501
|
+
|
|
502
|
+
fireEvent.click(screen.getByRole("button", { name: "View Tasks" }));
|
|
503
|
+
expect(navigateMock).toHaveBeenCalledTimes(1);
|
|
504
|
+
|
|
505
|
+
const route: Route = navigateMock.mock.calls[0]![0] as Route;
|
|
506
|
+
expect(route.toString()).toContain(CLUSTER_ID);
|
|
507
|
+
expect(route.toString()).toContain("tasks");
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe("Docker Swarm detail pages", () => {
|
|
512
|
+
test("the task page explains CPU and Memory, and leaves names and state plain", async () => {
|
|
513
|
+
lastParam = encodeURIComponent("task/t1");
|
|
514
|
+
getListMock.mockImplementation(async () => {
|
|
515
|
+
return listResult([
|
|
516
|
+
row({
|
|
517
|
+
kind: "Task",
|
|
518
|
+
externalId: "task/t1",
|
|
519
|
+
name: "shop_web.1",
|
|
520
|
+
state: "running",
|
|
521
|
+
isReady: true,
|
|
522
|
+
serviceName: "shop_web",
|
|
523
|
+
latestCpuPercent: 12.5,
|
|
524
|
+
latestMemoryBytes: 64 * 1024 * 1024,
|
|
525
|
+
}),
|
|
526
|
+
]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
render(<DockerSwarmClusterTaskDetail {...PAGE_PROPS} />);
|
|
530
|
+
await flush();
|
|
531
|
+
|
|
532
|
+
expect(screen.getByText("12.5%")).toBeInTheDocument();
|
|
533
|
+
expect(screen.getByText("64.0 MiB")).toBeInTheDocument();
|
|
534
|
+
|
|
535
|
+
await expectTooltip("CPU", T.taskCpu);
|
|
536
|
+
await expectTooltip("Memory", T.taskMemory);
|
|
537
|
+
|
|
538
|
+
for (const plain of [
|
|
539
|
+
"Task",
|
|
540
|
+
"Cluster",
|
|
541
|
+
"State",
|
|
542
|
+
"Service",
|
|
543
|
+
"External ID",
|
|
544
|
+
]) {
|
|
545
|
+
expect(
|
|
546
|
+
screen.queryByRole("button", { name: `About ${plain}` }),
|
|
547
|
+
).not.toBeInTheDocument();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("the service page explains Status and Replicas", async () => {
|
|
552
|
+
lastParam = encodeURIComponent("service/web");
|
|
553
|
+
getListMock.mockImplementation(async () => {
|
|
554
|
+
return listResult([
|
|
555
|
+
row({
|
|
556
|
+
kind: "Service",
|
|
557
|
+
externalId: "service/web",
|
|
558
|
+
name: "shop_web",
|
|
559
|
+
state: "2/3",
|
|
560
|
+
serviceMode: "replicated",
|
|
561
|
+
runningReplicas: 2,
|
|
562
|
+
desiredReplicas: 3,
|
|
563
|
+
isReady: false,
|
|
564
|
+
}),
|
|
565
|
+
]);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
render(<DockerSwarmClusterServiceDetail {...PAGE_PROPS} />);
|
|
569
|
+
await flush();
|
|
570
|
+
|
|
571
|
+
expect(screen.getAllByText("2/3").length).toBeGreaterThanOrEqual(2);
|
|
572
|
+
|
|
573
|
+
await expectTooltip("Status", T.serviceStatus);
|
|
574
|
+
await expectTooltip("Replicas", T.replicas);
|
|
575
|
+
|
|
576
|
+
expect(
|
|
577
|
+
screen.queryByRole("button", { name: "About Mode" }),
|
|
578
|
+
).not.toBeInTheDocument();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("the node page shows no (i): nothing on it is a metric the swarm reports per node", async () => {
|
|
582
|
+
lastParam = encodeURIComponent("node/a");
|
|
583
|
+
getListMock.mockImplementation(async () => {
|
|
584
|
+
return listResult([
|
|
585
|
+
row({
|
|
586
|
+
kind: "Node",
|
|
587
|
+
externalId: "node/a",
|
|
588
|
+
name: "manager-1",
|
|
589
|
+
state: "ready",
|
|
590
|
+
role: "manager",
|
|
591
|
+
isReady: true,
|
|
592
|
+
}),
|
|
593
|
+
]);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
render(<DockerSwarmClusterNodeDetail {...PAGE_PROPS} />);
|
|
597
|
+
await flush();
|
|
598
|
+
|
|
599
|
+
expect(screen.getByText("manager-1")).toBeInTheDocument();
|
|
600
|
+
expect(
|
|
601
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
602
|
+
).not.toBeInTheDocument();
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
describe("Docker Swarm list pages", () => {
|
|
607
|
+
const FRESH: Date = new Date();
|
|
608
|
+
|
|
609
|
+
test("Tasks: CPU and Memory headers explain the latest reading", async () => {
|
|
610
|
+
getListMock.mockImplementation(async () => {
|
|
611
|
+
return listResult([
|
|
612
|
+
row({
|
|
613
|
+
kind: "Task",
|
|
614
|
+
externalId: "task/t1",
|
|
615
|
+
name: "shop_web.1",
|
|
616
|
+
state: "running",
|
|
617
|
+
isReady: true,
|
|
618
|
+
serviceName: "shop_web",
|
|
619
|
+
nodeHostname: "manager-1",
|
|
620
|
+
latestCpuPercent: 12.5,
|
|
621
|
+
latestMemoryBytes: 64 * 1024 * 1024,
|
|
622
|
+
metricsUpdatedAt: FRESH,
|
|
623
|
+
}),
|
|
624
|
+
]);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const result: RenderResult = render(
|
|
628
|
+
<DockerSwarmClusterTasks {...PAGE_PROPS} />,
|
|
629
|
+
);
|
|
630
|
+
await flush();
|
|
631
|
+
|
|
632
|
+
expect(screen.getByText("shop_web.1")).toBeInTheDocument();
|
|
633
|
+
|
|
634
|
+
await expectTooltip("CPU", T.taskCpuColumn);
|
|
635
|
+
await expectTooltip("Memory", T.taskMemoryColumn);
|
|
636
|
+
|
|
637
|
+
// Status and metadata columns stay plain.
|
|
638
|
+
for (const plain of ["Status", "Service", "Image", "Node", "Age"]) {
|
|
639
|
+
expect(
|
|
640
|
+
screen.queryByRole("button", { name: `About ${plain}` }),
|
|
641
|
+
).not.toBeInTheDocument();
|
|
642
|
+
}
|
|
643
|
+
expectNoNestedControls(result.container);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("Nodes: CPU and Memory headers say why every row reads N/A", async () => {
|
|
647
|
+
getListMock.mockImplementation(async () => {
|
|
648
|
+
return listResult([
|
|
649
|
+
row({
|
|
650
|
+
kind: "Node",
|
|
651
|
+
externalId: "node/a",
|
|
652
|
+
name: "manager-1",
|
|
653
|
+
state: "ready",
|
|
654
|
+
role: "manager",
|
|
655
|
+
isReady: true,
|
|
656
|
+
}),
|
|
657
|
+
]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const result: RenderResult = render(
|
|
661
|
+
<DockerSwarmClusterNodes {...PAGE_PROPS} />,
|
|
662
|
+
);
|
|
663
|
+
await flush();
|
|
664
|
+
|
|
665
|
+
expect(screen.getAllByText("N/A")).toHaveLength(2);
|
|
666
|
+
|
|
667
|
+
await expectTooltip("CPU", T.nodeUsageColumns);
|
|
668
|
+
await expectTooltip("Memory", T.nodeUsageColumns);
|
|
669
|
+
expect(
|
|
670
|
+
screen.queryByRole("button", { name: "About Role" }),
|
|
671
|
+
).not.toBeInTheDocument();
|
|
672
|
+
expectNoNestedControls(result.container);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("Services: Status, Replicas, CPU and Memory headers are explained", async () => {
|
|
676
|
+
getListMock.mockImplementation(async () => {
|
|
677
|
+
return listResult([
|
|
678
|
+
row({
|
|
679
|
+
kind: "Service",
|
|
680
|
+
externalId: "service/web",
|
|
681
|
+
name: "shop_web",
|
|
682
|
+
state: "2/3",
|
|
683
|
+
serviceMode: "replicated",
|
|
684
|
+
runningReplicas: 2,
|
|
685
|
+
desiredReplicas: 3,
|
|
686
|
+
isReady: false,
|
|
687
|
+
}),
|
|
688
|
+
]);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const result: RenderResult = render(
|
|
692
|
+
<DockerSwarmClusterServices {...PAGE_PROPS} />,
|
|
693
|
+
);
|
|
694
|
+
await flush();
|
|
695
|
+
|
|
696
|
+
expect(screen.getByText("shop_web")).toBeInTheDocument();
|
|
697
|
+
|
|
698
|
+
await expectTooltip("Status", T.serviceStatusColumn);
|
|
699
|
+
await expectTooltip("Replicas", T.replicas);
|
|
700
|
+
await expectTooltip("CPU", T.serviceUsageColumns);
|
|
701
|
+
await expectTooltip("Memory", T.serviceUsageColumns);
|
|
702
|
+
expect(
|
|
703
|
+
screen.queryByRole("button", { name: "About Mode" }),
|
|
704
|
+
).not.toBeInTheDocument();
|
|
705
|
+
expectNoNestedControls(result.container);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("Stacks: the Services count and the Status that repeats it are explained", async () => {
|
|
709
|
+
getListMock.mockImplementation(async () => {
|
|
710
|
+
return listResult([
|
|
711
|
+
row({
|
|
712
|
+
kind: "Stack",
|
|
713
|
+
externalId: "stack/shop",
|
|
714
|
+
name: "shop",
|
|
715
|
+
state: "3 services",
|
|
716
|
+
attributes: { serviceCount: 3 },
|
|
717
|
+
}),
|
|
718
|
+
]);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const result: RenderResult = render(
|
|
722
|
+
<DockerSwarmClusterStacks {...PAGE_PROPS} />,
|
|
723
|
+
);
|
|
724
|
+
await flush();
|
|
725
|
+
|
|
726
|
+
expect(screen.getByText("shop")).toBeInTheDocument();
|
|
727
|
+
// The Status badge and the Services cell show the same count.
|
|
728
|
+
expect(screen.getByText("3 services")).toBeInTheDocument();
|
|
729
|
+
expect(screen.getByText("3")).toBeInTheDocument();
|
|
730
|
+
|
|
731
|
+
await expectTooltip("Services", T.stackServices);
|
|
732
|
+
await expectTooltip("Status", T.stackStatusColumn);
|
|
733
|
+
|
|
734
|
+
expect(
|
|
735
|
+
screen
|
|
736
|
+
.getAllByRole("button", { name: /^About / })
|
|
737
|
+
.map((button: HTMLElement): string => {
|
|
738
|
+
return button.getAttribute("aria-label") || "";
|
|
739
|
+
}),
|
|
740
|
+
).toEqual(["About Status", "About Services"]);
|
|
741
|
+
// A stack has no CPU or memory of its own, and no columns for them.
|
|
742
|
+
expect(
|
|
743
|
+
screen.queryByRole("columnheader", { name: /^CPU/ }),
|
|
744
|
+
).not.toBeInTheDocument();
|
|
745
|
+
expectNoNestedControls(result.container);
|
|
746
|
+
});
|
|
747
|
+
});
|