@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,242 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
} from "@testing-library/react";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import {
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeEach,
|
|
13
|
+
describe,
|
|
14
|
+
expect,
|
|
15
|
+
jest,
|
|
16
|
+
test,
|
|
17
|
+
} from "@jest/globals";
|
|
18
|
+
import InfoTooltip from "../../../UI/Components/Tooltip/InfoTooltip";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* The (i) that sits beside a metric's title and says what the metric means.
|
|
23
|
+
*
|
|
24
|
+
* Asserting tippy under jsdom (see DisabledButtonTooltip.test.tsx): the
|
|
25
|
+
* popup is portalled to document.body, so it is queried through `screen`;
|
|
26
|
+
* its box never finishes animating, so presence is asserted rather than
|
|
27
|
+
* data-state.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
31
|
+
fireEvent.mouseEnter(trigger);
|
|
32
|
+
await act(async () => {
|
|
33
|
+
jest.advanceTimersByTime(200);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function focus(trigger: HTMLElement): Promise<void> {
|
|
38
|
+
act(() => {
|
|
39
|
+
trigger.focus();
|
|
40
|
+
});
|
|
41
|
+
await act(async () => {
|
|
42
|
+
jest.advanceTimersByTime(200);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.useFakeTimers();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
cleanup();
|
|
52
|
+
jest.useRealTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const P95: string =
|
|
56
|
+
"95% of page loads finished faster than this; the slowest 5% took longer.";
|
|
57
|
+
|
|
58
|
+
describe("InfoTooltip", () => {
|
|
59
|
+
test("renders a button named after what it explains", () => {
|
|
60
|
+
render(<InfoTooltip label="p95 duration" text={P95} />);
|
|
61
|
+
|
|
62
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
63
|
+
name: "About p95 duration",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(button).toBeInTheDocument();
|
|
67
|
+
expect(button).toHaveAttribute("type", "button");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("does not create the tooltip until someone reaches for it", () => {
|
|
71
|
+
render(<InfoTooltip label="p95 duration" text={P95} />);
|
|
72
|
+
|
|
73
|
+
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
|
74
|
+
expect(screen.queryByText(P95)).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("shows the explanation on hover", async () => {
|
|
78
|
+
render(<InfoTooltip label="p95 duration" text={P95} />);
|
|
79
|
+
|
|
80
|
+
await hover(screen.getByRole("button", { name: "About p95 duration" }));
|
|
81
|
+
|
|
82
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(P95);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("shows the explanation on keyboard focus", async () => {
|
|
86
|
+
render(
|
|
87
|
+
<InfoTooltip label="Error rate" text="Share of events that failed." />,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await focus(screen.getByRole("button", { name: "About Error rate" }));
|
|
91
|
+
|
|
92
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
|
93
|
+
"Share of events that failed.",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("links the explanation to the button for assistive technology once shown", async () => {
|
|
98
|
+
render(<InfoTooltip label="p95 duration" text={P95} />);
|
|
99
|
+
|
|
100
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
101
|
+
name: "About p95 duration",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await focus(button);
|
|
105
|
+
await act(async () => {
|
|
106
|
+
jest.advanceTimersByTime(50);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const describedBy: string | null = button.getAttribute("aria-describedby");
|
|
110
|
+
|
|
111
|
+
expect(describedBy).toBeTruthy();
|
|
112
|
+
expect(document.getElementById(describedBy as string)).toHaveTextContent(
|
|
113
|
+
P95,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("does not mark the trigger as expandable (it is not a disclosure)", async () => {
|
|
118
|
+
render(<InfoTooltip label="p95 duration" text={P95} />);
|
|
119
|
+
|
|
120
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
121
|
+
name: "About p95 duration",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await hover(button);
|
|
125
|
+
|
|
126
|
+
expect(button).not.toHaveAttribute("aria-expanded");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test.each([
|
|
130
|
+
["undefined", undefined],
|
|
131
|
+
["empty", ""],
|
|
132
|
+
["whitespace", " \n "],
|
|
133
|
+
])(
|
|
134
|
+
"renders nothing when the explanation is %s",
|
|
135
|
+
(_: string, text?: string) => {
|
|
136
|
+
const { container } = render(
|
|
137
|
+
<InfoTooltip label="p95 duration" text={text} />,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(container).toBeEmptyDOMElement();
|
|
141
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
test("trims the explanation it shows", async () => {
|
|
146
|
+
render(<InfoTooltip label="Clients" text={" Platforms seen. "} />);
|
|
147
|
+
|
|
148
|
+
await hover(screen.getByRole("button", { name: "About Clients" }));
|
|
149
|
+
|
|
150
|
+
expect(screen.getByRole("tooltip").textContent).toBe("Platforms seen.");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a click does not bubble, so an (i) inside a link does not navigate", () => {
|
|
154
|
+
const onParentClick: MockFunction = getJestMockFunction();
|
|
155
|
+
|
|
156
|
+
render(
|
|
157
|
+
<div onClick={onParentClick}>
|
|
158
|
+
<InfoTooltip label="Clients" text="Platforms seen." />
|
|
159
|
+
</div>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const notCancelled: boolean = fireEvent.click(
|
|
163
|
+
screen.getByRole("button", { name: "About Clients" }),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(onParentClick).not.toHaveBeenCalled();
|
|
167
|
+
/* fireEvent returns false when preventDefault() was called. */
|
|
168
|
+
expect(notCancelled).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a click inside an anchor does not follow the link", () => {
|
|
172
|
+
render(
|
|
173
|
+
<a href="#/somewhere-else" data-testid="card-link">
|
|
174
|
+
<span>Clients</span>
|
|
175
|
+
<InfoTooltip label="Clients" text="Platforms seen." />
|
|
176
|
+
</a>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const notCancelled: boolean = fireEvent.click(
|
|
180
|
+
screen.getByRole("button", { name: "About Clients" }),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(notCancelled).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("the icon is decorative - the button's name comes from the label alone", () => {
|
|
187
|
+
render(<InfoTooltip label="CPU" text="Processor time in use." />);
|
|
188
|
+
|
|
189
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
190
|
+
name: "About CPU",
|
|
191
|
+
});
|
|
192
|
+
const svg: SVGElement | null = button.querySelector("svg");
|
|
193
|
+
|
|
194
|
+
expect(svg).not.toBeNull();
|
|
195
|
+
expect(svg).toHaveAttribute("aria-hidden", "true");
|
|
196
|
+
expect(button.querySelector("[role='img']")).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("passes the test id, extra classes and icon size through", () => {
|
|
200
|
+
render(
|
|
201
|
+
<InfoTooltip
|
|
202
|
+
label="CPU"
|
|
203
|
+
text="Processor time in use."
|
|
204
|
+
dataTestId="cpu-help"
|
|
205
|
+
className="ml-1"
|
|
206
|
+
iconClassName="h-4 w-4"
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const button: HTMLElement = screen.getByTestId("cpu-help");
|
|
211
|
+
|
|
212
|
+
expect(button).toHaveClass("ml-1");
|
|
213
|
+
expect(button.querySelector("svg")).toHaveClass("h-4", "w-4");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("uses a small icon by default and shows a visible focus ring", () => {
|
|
217
|
+
render(<InfoTooltip label="CPU" text="Processor time in use." />);
|
|
218
|
+
|
|
219
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
220
|
+
name: "About CPU",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(button.querySelector("svg")).toHaveClass("h-3.5", "w-3.5");
|
|
224
|
+
expect(button.className).toContain("focus-visible:ring-2");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("two tooltips on one page each show their own text", async () => {
|
|
228
|
+
render(
|
|
229
|
+
<div>
|
|
230
|
+
<InfoTooltip label="p95 duration" text={P95} />
|
|
231
|
+
<InfoTooltip label="Error rate" text="Share of events that failed." />
|
|
232
|
+
</div>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await hover(screen.getByRole("button", { name: "About Error rate" }));
|
|
236
|
+
|
|
237
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
|
238
|
+
"Share of events that failed.",
|
|
239
|
+
);
|
|
240
|
+
expect(screen.queryByText(P95)).not.toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -101,6 +101,7 @@ jest.mock("../../../../UI/Utils/Translation", () => {
|
|
|
101
101
|
import BaseModelTable, {
|
|
102
102
|
BaseTableCallbacks,
|
|
103
103
|
ComponentProps as BaseModelTableProps,
|
|
104
|
+
ModalTableBulkDefaultActions,
|
|
104
105
|
} from "../../../../UI/Components/ModelTable/BaseModelTable";
|
|
105
106
|
import TableFilterUrlState from "../../../../UI/Utils/TableFilterUrlState";
|
|
106
107
|
import PermissionGate from "../../../../UI/Utils/PermissionGate";
|
|
@@ -108,6 +109,7 @@ import FieldType from "../../../../UI/Components/Types/FieldType";
|
|
|
108
109
|
import { ButtonStyleType } from "../../../../UI/Components/Button/Button";
|
|
109
110
|
import NetworkDevice from "../../../../Models/DatabaseModels/NetworkDevice";
|
|
110
111
|
import Permission from "../../../../Types/Permission";
|
|
112
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
111
113
|
import ListResult from "../../../../Types/BaseDatabase/ListResult";
|
|
112
114
|
import { JSONObject } from "../../../../Types/JSON";
|
|
113
115
|
|
|
@@ -136,6 +138,8 @@ const DEVICE_DELETE_WARNING: string =
|
|
|
136
138
|
type TableOptions = {
|
|
137
139
|
bulkActionButtons?: Array<unknown> | undefined;
|
|
138
140
|
deleteConfirmationWarning?: string | undefined;
|
|
141
|
+
deleteVerb?: string | undefined;
|
|
142
|
+
deleteIcon?: IconProp | undefined;
|
|
139
143
|
deleteItem?: ((item: NetworkDevice) => Promise<void>) | undefined;
|
|
140
144
|
};
|
|
141
145
|
|
|
@@ -218,6 +222,8 @@ describe("BaseModelTable bulk Delete", () => {
|
|
|
218
222
|
...(options.deleteConfirmationWarning
|
|
219
223
|
? { deleteConfirmationWarning: options.deleteConfirmationWarning }
|
|
220
224
|
: {}),
|
|
225
|
+
...(options.deleteVerb ? { deleteVerb: options.deleteVerb } : {}),
|
|
226
|
+
...(options.deleteIcon ? { deleteIcon: options.deleteIcon } : {}),
|
|
221
227
|
},
|
|
222
228
|
} as unknown as BaseModelTableProps<NetworkDevice>;
|
|
223
229
|
};
|
|
@@ -307,10 +313,12 @@ describe("BaseModelTable bulk Delete", () => {
|
|
|
307
313
|
});
|
|
308
314
|
};
|
|
309
315
|
|
|
310
|
-
type ConfirmFunction = () => Promise<void>;
|
|
316
|
+
type ConfirmFunction = (buttonText?: string | undefined) => Promise<void>;
|
|
311
317
|
|
|
312
318
|
/* Presses the danger button in the confirmation dialog's footer. */
|
|
313
|
-
const confirmDelete: ConfirmFunction = async (
|
|
319
|
+
const confirmDelete: ConfirmFunction = async (
|
|
320
|
+
buttonText?: string | undefined,
|
|
321
|
+
): Promise<void> => {
|
|
314
322
|
await waitFor(() => {
|
|
315
323
|
expect(
|
|
316
324
|
document.querySelector('[data-testid="modal-footer"]'),
|
|
@@ -324,7 +332,7 @@ describe("BaseModelTable bulk Delete", () => {
|
|
|
324
332
|
const submitButton: HTMLButtonElement | undefined = Array.from(
|
|
325
333
|
footer.querySelectorAll<HTMLButtonElement>("button"),
|
|
326
334
|
).find((button: HTMLButtonElement) => {
|
|
327
|
-
return (button.textContent || "").trim() === "Delete";
|
|
335
|
+
return (button.textContent || "").trim() === (buttonText || "Delete");
|
|
328
336
|
});
|
|
329
337
|
|
|
330
338
|
expect(submitButton).toBeDefined();
|
|
@@ -748,4 +756,200 @@ describe("BaseModelTable bulk Delete", () => {
|
|
|
748
756
|
expect(deletedIds).toEqual([]);
|
|
749
757
|
});
|
|
750
758
|
});
|
|
759
|
+
|
|
760
|
+
/*
|
|
761
|
+
* A table of links (an incident's linked alerts, say) deletes rows that are
|
|
762
|
+
* relationships, not things. "Delete 3 Alerts ... This action cannot be
|
|
763
|
+
* undone" read as if the alerts themselves were going, and contradicted the
|
|
764
|
+
* warning appended after it. Such a table names its own verb; every other
|
|
765
|
+
* table keeps the default wording (pinned above).
|
|
766
|
+
*/
|
|
767
|
+
describe("a table that names its own verb", () => {
|
|
768
|
+
const LINK_WARNING: string =
|
|
769
|
+
"Only the links are removed: the devices themselves are not deleted.";
|
|
770
|
+
|
|
771
|
+
test("offers the verb in place of Delete, once", async () => {
|
|
772
|
+
const { container } = renderTable({
|
|
773
|
+
bulkActionButtons: [
|
|
774
|
+
ARCHIVE_ACTION,
|
|
775
|
+
ModalTableBulkDefaultActions.Delete,
|
|
776
|
+
],
|
|
777
|
+
deleteVerb: "Unlink",
|
|
778
|
+
deleteIcon: IconProp.LinkSlash,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
await selectRows(container, 1);
|
|
782
|
+
await openBulkMenu();
|
|
783
|
+
|
|
784
|
+
expect(menuItemLabels()).toEqual(
|
|
785
|
+
expect.arrayContaining(["Archive", "Export CSV", "Unlink"]),
|
|
786
|
+
);
|
|
787
|
+
expect(menuItemLabels()).not.toContain("Delete");
|
|
788
|
+
expect(
|
|
789
|
+
menuItemLabels().filter((label: string) => {
|
|
790
|
+
return label === "Unlink";
|
|
791
|
+
}).length,
|
|
792
|
+
).toBe(1);
|
|
793
|
+
expect(findMenuItem("Unlink")).not.toBeDisabled();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
/*
|
|
797
|
+
* The verb must still be recognised as the table's delete action when it
|
|
798
|
+
* is added automatically, or a second, default Delete would sit next to
|
|
799
|
+
* it.
|
|
800
|
+
*/
|
|
801
|
+
test("uses the verb for the automatically added action too", async () => {
|
|
802
|
+
const { container } = renderTable({
|
|
803
|
+
deleteVerb: "Unlink",
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await selectRows(container, 1);
|
|
807
|
+
await openBulkMenu();
|
|
808
|
+
|
|
809
|
+
expect(findMenuItem("Unlink")).toBeDefined();
|
|
810
|
+
expect(findMenuItem("Delete")).toBeUndefined();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("does not add the default action beside a page's own button with that verb", async () => {
|
|
814
|
+
const { container } = renderTable({
|
|
815
|
+
bulkActionButtons: [
|
|
816
|
+
ARCHIVE_ACTION,
|
|
817
|
+
{
|
|
818
|
+
title: "Unlink",
|
|
819
|
+
buttonStyleType: ButtonStyleType.DANGER,
|
|
820
|
+
onClick: async (): Promise<void> => {
|
|
821
|
+
return Promise.resolve();
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
deleteVerb: "Unlink",
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await selectRows(container, 1);
|
|
829
|
+
await openBulkMenu();
|
|
830
|
+
|
|
831
|
+
expect(
|
|
832
|
+
menuItemLabels().filter((label: string) => {
|
|
833
|
+
return label === "Unlink" || label === "Delete";
|
|
834
|
+
}),
|
|
835
|
+
).toEqual(["Unlink"]);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("asks with the verb, and does not claim it cannot be undone", async () => {
|
|
839
|
+
const { container } = renderTable({
|
|
840
|
+
bulkActionButtons: [
|
|
841
|
+
ARCHIVE_ACTION,
|
|
842
|
+
ModalTableBulkDefaultActions.Delete,
|
|
843
|
+
],
|
|
844
|
+
deleteVerb: "Unlink",
|
|
845
|
+
deleteConfirmationWarning: LINK_WARNING,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
await selectRows(container, 2);
|
|
849
|
+
await openBulkMenu();
|
|
850
|
+
|
|
851
|
+
fireEvent.click(findMenuItem("Unlink")!);
|
|
852
|
+
|
|
853
|
+
await waitFor(() => {
|
|
854
|
+
expect(confirmationText()).not.toBe("");
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
expect(confirmationText().trim()).toBe(
|
|
858
|
+
`Are you sure you want to unlink 2 network devices? ${LINK_WARNING}`,
|
|
859
|
+
);
|
|
860
|
+
expect(confirmationText()).not.toContain("cannot be undone");
|
|
861
|
+
expect(screen.getByText("Unlink 2 Network Devices")).toBeInTheDocument();
|
|
862
|
+
expect(screen.queryByText("Delete 2 Network Devices")).toBeNull();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test("uses the singular for one row", async () => {
|
|
866
|
+
const { container } = renderTable({
|
|
867
|
+
deleteVerb: "Unlink",
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
await selectRows(container, 1);
|
|
871
|
+
await openBulkMenu();
|
|
872
|
+
|
|
873
|
+
fireEvent.click(findMenuItem("Unlink")!);
|
|
874
|
+
|
|
875
|
+
await waitFor(() => {
|
|
876
|
+
expect(confirmationText()).not.toBe("");
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
expect(confirmationText().trim()).toBe(
|
|
880
|
+
"Are you sure you want to unlink 1 network device?",
|
|
881
|
+
);
|
|
882
|
+
expect(screen.getByText("Unlink 1 Network Device")).toBeInTheDocument();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("still deletes exactly the selected rows once confirmed", async () => {
|
|
886
|
+
const { container } = renderTable({
|
|
887
|
+
deleteVerb: "Unlink",
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
await selectRows(container, 2);
|
|
891
|
+
await openBulkMenu();
|
|
892
|
+
|
|
893
|
+
fireEvent.click(findMenuItem("Unlink")!);
|
|
894
|
+
|
|
895
|
+
expect(deletedIds).toEqual([]);
|
|
896
|
+
|
|
897
|
+
await confirmDelete("Unlink");
|
|
898
|
+
|
|
899
|
+
await waitFor(() => {
|
|
900
|
+
expect(deletedIds.length).toBe(2);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
expect(deletedIds.sort()).toEqual(["device-1", "device-2"]);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test("is still gated on the model's delete permission", async () => {
|
|
907
|
+
permissionsForTest = [Permission.Viewer];
|
|
908
|
+
|
|
909
|
+
const { container } = renderTable({
|
|
910
|
+
deleteVerb: "Unlink",
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
await selectRows(container, 1);
|
|
914
|
+
await openBulkMenu();
|
|
915
|
+
|
|
916
|
+
const unlinkItem: HTMLElement | undefined = findMenuItem("Unlink");
|
|
917
|
+
|
|
918
|
+
expect(unlinkItem).toBeDefined();
|
|
919
|
+
expect(unlinkItem).toBeDisabled();
|
|
920
|
+
|
|
921
|
+
fireEvent.mouseEnter(unlinkItem!.parentElement as HTMLElement);
|
|
922
|
+
|
|
923
|
+
/*
|
|
924
|
+
* The reason follows the verb, but the permission it names is still the
|
|
925
|
+
* delete one the action is gated on.
|
|
926
|
+
*/
|
|
927
|
+
const tooltip: HTMLElement = screen.getByRole("tooltip");
|
|
928
|
+
|
|
929
|
+
expect(tooltip).toHaveTextContent(
|
|
930
|
+
"You do not have permission to unlink this Network Device.",
|
|
931
|
+
);
|
|
932
|
+
expect(tooltip).toHaveTextContent("Delete Network Device");
|
|
933
|
+
expect(tooltip).not.toHaveTextContent("delete this Network Device");
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test("a blank verb keeps the default wording", async () => {
|
|
937
|
+
const { container } = renderTable({
|
|
938
|
+
deleteVerb: " ",
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
await selectRows(container, 2);
|
|
942
|
+
await openBulkMenu();
|
|
943
|
+
|
|
944
|
+
fireEvent.click(findMenuItem("Delete")!);
|
|
945
|
+
|
|
946
|
+
await waitFor(() => {
|
|
947
|
+
expect(confirmationText()).not.toBe("");
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
expect(confirmationText().trim()).toBe(
|
|
951
|
+
"Are you sure you want to delete 2 Network Devices? This action cannot be undone.",
|
|
952
|
+
);
|
|
953
|
+
});
|
|
954
|
+
});
|
|
751
955
|
});
|