@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,869 @@
|
|
|
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 { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { FunctionComponent, ReactElement } from "react";
|
|
13
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The alerts table's "Link to Incident" and "Declare Incident" bulk actions
|
|
17
|
+
* (useBulkIncidentLinkActions). They sit on every alerts list in the product,
|
|
18
|
+
* so what is pinned here is what a user would notice going wrong:
|
|
19
|
+
*
|
|
20
|
+
* - gating: a viewer sees both actions locked with the missing permission,
|
|
21
|
+
* a member can use them, declaring also needs incident create, and
|
|
22
|
+
* linking to an existing incident also needs to read incidents;
|
|
23
|
+
* - the cap: above MAX_ALERTS_PER_INCIDENT_LINK_ACTION selected alerts
|
|
24
|
+
* both actions stay on the menu, locked, with a reason;
|
|
25
|
+
* - linking: one IncidentAlert per alert, progress after each, an alert
|
|
26
|
+
* that is already linked counts as done, other failures are isolated;
|
|
27
|
+
* - declaring: navigates to the create page with the selected ids.
|
|
28
|
+
*
|
|
29
|
+
* The dialog is the shared LinkIncidentAlertModal, whose BasicFormModal is
|
|
30
|
+
* stubbed here to capture its props - the contract is the field it declares,
|
|
31
|
+
* the numbered options it is handed, and onSubmit.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
35
|
+
const createMock: MockFunction = getJestMockFunction();
|
|
36
|
+
|
|
37
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
38
|
+
return {
|
|
39
|
+
__esModule: true,
|
|
40
|
+
default: {
|
|
41
|
+
getList: (...args: Array<any>) => {
|
|
42
|
+
return getListMock(...args);
|
|
43
|
+
},
|
|
44
|
+
create: (...args: Array<any>) => {
|
|
45
|
+
return createMock(...args);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
interface CapturedModalProps {
|
|
52
|
+
title: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
isLoading?: boolean;
|
|
55
|
+
submitButtonText?: string;
|
|
56
|
+
onClose: () => void;
|
|
57
|
+
onSubmit: (data: Record<string, unknown>) => Promise<void>;
|
|
58
|
+
formProps: {
|
|
59
|
+
fields: Array<Record<string, any>>;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let capturedModal: CapturedModalProps | null = null;
|
|
64
|
+
|
|
65
|
+
jest.mock("../../../UI/Components/FormModal/BasicFormModal", () => {
|
|
66
|
+
return {
|
|
67
|
+
__esModule: true,
|
|
68
|
+
default: (props: CapturedModalProps): React.ReactElement => {
|
|
69
|
+
capturedModal = props;
|
|
70
|
+
return <div data-testid="basic-form-modal">{props.title}</div>;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
import useBulkIncidentLinkActions, {
|
|
76
|
+
BulkIncidentLinkActionsResult,
|
|
77
|
+
DECLARE_CAP_TOOLTIP,
|
|
78
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
79
|
+
LINK_CAP_TOOLTIP,
|
|
80
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
81
|
+
getDeclareIncidentFromAlertsRoute,
|
|
82
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Alert/BulkIncidentLinkActions";
|
|
83
|
+
import { LINK_OPTIONS_LIMIT } from "../../../../App/FeatureSet/Dashboard/src/Components/IncidentAlert/IncidentAlertLink";
|
|
84
|
+
import {
|
|
85
|
+
BulkActionButtonSchema,
|
|
86
|
+
BulkActionFailed,
|
|
87
|
+
BulkActionOnClickProps,
|
|
88
|
+
ProgressInfo,
|
|
89
|
+
} from "../../../UI/Components/BulkUpdate/BulkUpdateForm";
|
|
90
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
91
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
92
|
+
import IncidentAlert from "../../../Models/DatabaseModels/IncidentAlert";
|
|
93
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
94
|
+
import Route from "../../../Types/API/Route";
|
|
95
|
+
import {
|
|
96
|
+
INCIDENT_ALERT_ALREADY_LINKED_MESSAGE,
|
|
97
|
+
MAX_ALERTS_PER_INCIDENT_LINK_ACTION,
|
|
98
|
+
} from "../../../Types/Incident/IncidentAlertLink";
|
|
99
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
100
|
+
import Permission from "../../../Types/Permission";
|
|
101
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
102
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
103
|
+
import PermissionGate from "../../../UI/Utils/PermissionGate";
|
|
104
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
105
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
106
|
+
|
|
107
|
+
const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
|
|
108
|
+
const INCIDENT_ID: string = "44444444-4444-4444-8444-444444444444";
|
|
109
|
+
|
|
110
|
+
type MakeAlertFunction = (index: number) => Alert;
|
|
111
|
+
|
|
112
|
+
const makeAlert: MakeAlertFunction = (index: number): Alert => {
|
|
113
|
+
const alert: Alert = new Alert();
|
|
114
|
+
alert._id = `22222222-2222-4222-8222-${index.toString().padStart(12, "0")}`;
|
|
115
|
+
alert.title = `Alert ${index}`;
|
|
116
|
+
return alert;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type MakeAlertsFunction = (count: number) => Array<Alert>;
|
|
120
|
+
|
|
121
|
+
const makeAlerts: MakeAlertsFunction = (count: number): Array<Alert> => {
|
|
122
|
+
return Array.from({ length: count }, (_value: unknown, index: number) => {
|
|
123
|
+
return makeAlert(index + 1);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type MakeIncidentFunction = (data: {
|
|
128
|
+
id: string;
|
|
129
|
+
title?: string;
|
|
130
|
+
incidentNumber?: number;
|
|
131
|
+
incidentNumberWithPrefix?: string;
|
|
132
|
+
}) => Incident;
|
|
133
|
+
|
|
134
|
+
const makeIncident: MakeIncidentFunction = (data: {
|
|
135
|
+
id: string;
|
|
136
|
+
title?: string;
|
|
137
|
+
incidentNumber?: number;
|
|
138
|
+
incidentNumberWithPrefix?: string;
|
|
139
|
+
}): Incident => {
|
|
140
|
+
const incident: Incident = new Incident();
|
|
141
|
+
incident._id = data.id;
|
|
142
|
+
if (data.title !== undefined) {
|
|
143
|
+
incident.title = data.title;
|
|
144
|
+
}
|
|
145
|
+
if (data.incidentNumber !== undefined) {
|
|
146
|
+
incident.incidentNumber = data.incidentNumber;
|
|
147
|
+
}
|
|
148
|
+
if (data.incidentNumberWithPrefix !== undefined) {
|
|
149
|
+
incident.incidentNumberWithPrefix = data.incidentNumberWithPrefix;
|
|
150
|
+
}
|
|
151
|
+
return incident;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
type ListResultFunction = (data: Array<unknown>) => {
|
|
155
|
+
data: Array<unknown>;
|
|
156
|
+
count: number;
|
|
157
|
+
skip: number;
|
|
158
|
+
limit: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const listResult: ListResultFunction = (data: Array<unknown>) => {
|
|
162
|
+
return { data: data, count: data.length, skip: 0, limit: data.length };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let permissionsForTest: Array<Permission> = [];
|
|
166
|
+
let currentResult: BulkIncidentLinkActionsResult | null = null;
|
|
167
|
+
|
|
168
|
+
const Harness: FunctionComponent = (): ReactElement => {
|
|
169
|
+
const result: BulkIncidentLinkActionsResult = useBulkIncidentLinkActions();
|
|
170
|
+
currentResult = result;
|
|
171
|
+
return <div>{result.modals}</div>;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type RenderHarnessFunction = () => void;
|
|
175
|
+
|
|
176
|
+
const renderHarness: RenderHarnessFunction = (): void => {
|
|
177
|
+
render(<Harness />);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type ActionsFunction = () => Array<BulkActionButtonSchema<Alert>>;
|
|
181
|
+
|
|
182
|
+
const actions: ActionsFunction = (): Array<BulkActionButtonSchema<Alert>> => {
|
|
183
|
+
if (!currentResult) {
|
|
184
|
+
throw new Error("The hook has not rendered.");
|
|
185
|
+
}
|
|
186
|
+
return currentResult.bulkActions;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/*
|
|
190
|
+
* What the action bar shows for a selection: BulkUpdateForm keeps only the
|
|
191
|
+
* actions whose isVisible accepts the selected items.
|
|
192
|
+
*/
|
|
193
|
+
type VisibleActionFunction = (
|
|
194
|
+
title: string,
|
|
195
|
+
items: Array<Alert>,
|
|
196
|
+
) => BulkActionButtonSchema<Alert>;
|
|
197
|
+
|
|
198
|
+
const visibleAction: VisibleActionFunction = (
|
|
199
|
+
title: string,
|
|
200
|
+
items: Array<Alert>,
|
|
201
|
+
): BulkActionButtonSchema<Alert> => {
|
|
202
|
+
const visible: Array<BulkActionButtonSchema<Alert>> = actions().filter(
|
|
203
|
+
(action: BulkActionButtonSchema<Alert>): boolean => {
|
|
204
|
+
return (
|
|
205
|
+
action.title === title &&
|
|
206
|
+
(!action.isVisible || action.isVisible(items) !== false)
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(visible).toHaveLength(1);
|
|
212
|
+
|
|
213
|
+
return visible[0]!;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
interface ProgressSnapshot {
|
|
217
|
+
inProgress: number;
|
|
218
|
+
success: Array<string>;
|
|
219
|
+
failed: Array<string>;
|
|
220
|
+
failedMessages: Array<string>;
|
|
221
|
+
total: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let progressSnapshots: Array<ProgressSnapshot> = [];
|
|
225
|
+
let callOrder: Array<string> = [];
|
|
226
|
+
|
|
227
|
+
type MakeActionPropsFunction = (
|
|
228
|
+
items: Array<Alert>,
|
|
229
|
+
) => BulkActionOnClickProps<Alert>;
|
|
230
|
+
|
|
231
|
+
const makeActionProps: MakeActionPropsFunction = (
|
|
232
|
+
items: Array<Alert>,
|
|
233
|
+
): BulkActionOnClickProps<Alert> => {
|
|
234
|
+
return {
|
|
235
|
+
items: items,
|
|
236
|
+
onProgressInfo: (info: ProgressInfo<Alert>): void => {
|
|
237
|
+
progressSnapshots.push({
|
|
238
|
+
inProgress: info.inProgressItems.length,
|
|
239
|
+
success: info.successItems.map((item: Alert): string => {
|
|
240
|
+
return item.title || "";
|
|
241
|
+
}),
|
|
242
|
+
failed: info.failed.map((failure: BulkActionFailed<Alert>): string => {
|
|
243
|
+
return failure.item.title || "";
|
|
244
|
+
}),
|
|
245
|
+
failedMessages: info.failed.map(
|
|
246
|
+
(failure: BulkActionFailed<Alert>): string => {
|
|
247
|
+
return String(failure.failedMessage);
|
|
248
|
+
},
|
|
249
|
+
),
|
|
250
|
+
total: info.totalItems.length,
|
|
251
|
+
});
|
|
252
|
+
callOrder.push("progress");
|
|
253
|
+
},
|
|
254
|
+
onBulkActionStart: (): void => {
|
|
255
|
+
callOrder.push("start");
|
|
256
|
+
},
|
|
257
|
+
onBulkActionEnd: (): void => {
|
|
258
|
+
callOrder.push("end");
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
type ActAsyncFunction = (body: () => Promise<void>) => Promise<void>;
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* zone.js reschedules the microtask React's act() uses to check that its
|
|
267
|
+
* thenable was awaited, so a plain `await act(...)` is reported as
|
|
268
|
+
* un-awaited. Hooking then() up synchronously answers that check in time (as
|
|
269
|
+
* BulkLabelActions.test.tsx does).
|
|
270
|
+
*/
|
|
271
|
+
const actAsync: ActAsyncFunction = (
|
|
272
|
+
body: () => Promise<void>,
|
|
273
|
+
): Promise<void> => {
|
|
274
|
+
const scope: PromiseLike<void> = act(body) as unknown as PromiseLike<void>;
|
|
275
|
+
|
|
276
|
+
return new Promise<void>(
|
|
277
|
+
(resolve: () => void, reject: (reason: unknown) => void): void => {
|
|
278
|
+
scope.then(resolve, reject);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* A call that throws from inside rather than a promise that is already
|
|
285
|
+
* rejected: zone.js sees an eagerly rejected promise before the hook's catch
|
|
286
|
+
* is attached and prints its stack on every run.
|
|
287
|
+
*/
|
|
288
|
+
type FailWithFunction = (message: string) => () => Promise<never>;
|
|
289
|
+
|
|
290
|
+
const failWith: FailWithFunction = (message: string) => {
|
|
291
|
+
return async (): Promise<never> => {
|
|
292
|
+
throw new Error(message);
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type ClickFunction = (title: string, items: Array<Alert>) => Promise<void>;
|
|
297
|
+
|
|
298
|
+
const click: ClickFunction = async (
|
|
299
|
+
title: string,
|
|
300
|
+
items: Array<Alert>,
|
|
301
|
+
): Promise<void> => {
|
|
302
|
+
await actAsync(async () => {
|
|
303
|
+
await visibleAction(title, items).onClick(makeActionProps(items));
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
type SubmitLinkFunction = (incidentId: unknown) => Promise<void>;
|
|
308
|
+
|
|
309
|
+
const submitLink: SubmitLinkFunction = async (
|
|
310
|
+
incidentId: unknown,
|
|
311
|
+
): Promise<void> => {
|
|
312
|
+
if (!capturedModal) {
|
|
313
|
+
throw new Error("The Link to Incident dialog is not open.");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const onSubmit: CapturedModalProps["onSubmit"] = capturedModal.onSubmit;
|
|
317
|
+
|
|
318
|
+
await actAsync(async () => {
|
|
319
|
+
await onSubmit({ linkedRecordId: incidentId });
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
type CreatedLinkFunction = (callIndex: number) => IncidentAlert;
|
|
324
|
+
|
|
325
|
+
const createdLink: CreatedLinkFunction = (callIndex: number): IncidentAlert => {
|
|
326
|
+
const call: Array<any> = createMock.mock.calls[callIndex] as Array<any>;
|
|
327
|
+
expect(call[0].modelType).toBe(IncidentAlert);
|
|
328
|
+
return call[0].model as IncidentAlert;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
describe("useBulkIncidentLinkActions", () => {
|
|
332
|
+
let navigateSpy: ReturnType<typeof jest.spyOn>;
|
|
333
|
+
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
capturedModal = null;
|
|
336
|
+
currentResult = null;
|
|
337
|
+
progressSnapshots = [];
|
|
338
|
+
callOrder = [];
|
|
339
|
+
permissionsForTest = [Permission.ProjectMember];
|
|
340
|
+
getListMock.mockReset();
|
|
341
|
+
createMock.mockReset();
|
|
342
|
+
getListMock.mockResolvedValue(listResult([]));
|
|
343
|
+
createMock.mockResolvedValue({});
|
|
344
|
+
PermissionGate.clearPermissionPropsCache();
|
|
345
|
+
|
|
346
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockImplementation(() => {
|
|
347
|
+
return permissionsForTest;
|
|
348
|
+
});
|
|
349
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
350
|
+
jest
|
|
351
|
+
.spyOn(ProjectUtil, "getCurrentProjectId")
|
|
352
|
+
.mockReturnValue(new ObjectID(PROJECT_ID));
|
|
353
|
+
navigateSpy = jest
|
|
354
|
+
.spyOn(Navigation, "navigate")
|
|
355
|
+
.mockImplementation((): void => {});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
afterEach(() => {
|
|
359
|
+
cleanup();
|
|
360
|
+
jest.restoreAllMocks();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("permission gating", () => {
|
|
364
|
+
test("leaves both actions working for a project member", () => {
|
|
365
|
+
renderHarness();
|
|
366
|
+
|
|
367
|
+
const items: Array<Alert> = makeAlerts(2);
|
|
368
|
+
|
|
369
|
+
expect(
|
|
370
|
+
visibleAction(LINK_TO_INCIDENT_ACTION_TITLE, items).disabled,
|
|
371
|
+
).toBeFalsy();
|
|
372
|
+
expect(
|
|
373
|
+
visibleAction(DECLARE_INCIDENT_ACTION_TITLE, items).disabled,
|
|
374
|
+
).toBeFalsy();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("locks both actions for a viewer, saying which permission is missing", () => {
|
|
378
|
+
permissionsForTest = [Permission.Viewer];
|
|
379
|
+
renderHarness();
|
|
380
|
+
|
|
381
|
+
const items: Array<Alert> = makeAlerts(2);
|
|
382
|
+
const link: BulkActionButtonSchema<Alert> = visibleAction(
|
|
383
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
384
|
+
items,
|
|
385
|
+
);
|
|
386
|
+
const declare: BulkActionButtonSchema<Alert> = visibleAction(
|
|
387
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
388
|
+
items,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
expect(link.disabled).toBe(true);
|
|
392
|
+
expect(link.tooltip).toContain(
|
|
393
|
+
"You do not have permission to create this Incident Alert.",
|
|
394
|
+
);
|
|
395
|
+
expect(declare.disabled).toBe(true);
|
|
396
|
+
expect(declare.tooltip).toContain(
|
|
397
|
+
"You do not have permission to create this Incident.",
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("keeps locked actions on the menu rather than dropping them", () => {
|
|
402
|
+
permissionsForTest = [Permission.Viewer];
|
|
403
|
+
renderHarness();
|
|
404
|
+
|
|
405
|
+
const titles: Array<string> = actions().map(
|
|
406
|
+
(action: BulkActionButtonSchema<Alert>): string => {
|
|
407
|
+
return action.title;
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
expect(titles).toEqual([
|
|
412
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
413
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
414
|
+
]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
/*
|
|
418
|
+
* An alert role alone may create the link row, but the server only links
|
|
419
|
+
* an incident the caller can read - and the incident picker could not
|
|
420
|
+
* list any - so the action is locked with the read permission it needs.
|
|
421
|
+
*/
|
|
422
|
+
test("locks linking for an alert member who cannot read incidents", () => {
|
|
423
|
+
permissionsForTest = [Permission.AlertMember];
|
|
424
|
+
renderHarness();
|
|
425
|
+
|
|
426
|
+
const items: Array<Alert> = makeAlerts(1);
|
|
427
|
+
const link: BulkActionButtonSchema<Alert> = visibleAction(
|
|
428
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
429
|
+
items,
|
|
430
|
+
);
|
|
431
|
+
const declare: BulkActionButtonSchema<Alert> = visibleAction(
|
|
432
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
433
|
+
items,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
expect(link.disabled).toBe(true);
|
|
437
|
+
expect(link.tooltip).toContain(
|
|
438
|
+
"You do not have permission to read this Incident.",
|
|
439
|
+
);
|
|
440
|
+
expect(declare.disabled).toBe(true);
|
|
441
|
+
expect(declare.tooltip).toContain(
|
|
442
|
+
"You do not have permission to create this Incident.",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("lets an alert member who can also read incidents link, but not declare", () => {
|
|
447
|
+
permissionsForTest = [Permission.AlertMember, Permission.IncidentViewer];
|
|
448
|
+
renderHarness();
|
|
449
|
+
|
|
450
|
+
const items: Array<Alert> = makeAlerts(1);
|
|
451
|
+
const declare: BulkActionButtonSchema<Alert> = visibleAction(
|
|
452
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
453
|
+
items,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
expect(
|
|
457
|
+
visibleAction(LINK_TO_INCIDENT_ACTION_TITLE, items).disabled,
|
|
458
|
+
).toBeFalsy();
|
|
459
|
+
expect(declare.disabled).toBe(true);
|
|
460
|
+
expect(declare.tooltip).toContain(
|
|
461
|
+
"You do not have permission to create this Incident.",
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("names the link permission first when both are missing", () => {
|
|
466
|
+
permissionsForTest = [Permission.AlertViewer];
|
|
467
|
+
renderHarness();
|
|
468
|
+
|
|
469
|
+
const link: BulkActionButtonSchema<Alert> = visibleAction(
|
|
470
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
471
|
+
makeAlerts(1),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(link.disabled).toBe(true);
|
|
475
|
+
expect(link.tooltip).toContain(
|
|
476
|
+
"You do not have permission to create this Incident Alert.",
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("locks declaring for someone who may create incidents but not links", () => {
|
|
481
|
+
permissionsForTest = [Permission.CreateProjectIncident];
|
|
482
|
+
renderHarness();
|
|
483
|
+
|
|
484
|
+
const declare: BulkActionButtonSchema<Alert> = visibleAction(
|
|
485
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
486
|
+
makeAlerts(1),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
expect(declare.disabled).toBe(true);
|
|
490
|
+
expect(declare.tooltip).toContain(
|
|
491
|
+
"You do not have permission to create this Incident Alert.",
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("does not accuse anyone before the permission snapshot has loaded", () => {
|
|
496
|
+
permissionsForTest = [];
|
|
497
|
+
renderHarness();
|
|
498
|
+
|
|
499
|
+
const items: Array<Alert> = makeAlerts(1);
|
|
500
|
+
|
|
501
|
+
expect(
|
|
502
|
+
visibleAction(LINK_TO_INCIDENT_ACTION_TITLE, items).disabled,
|
|
503
|
+
).toBeFalsy();
|
|
504
|
+
expect(
|
|
505
|
+
visibleAction(DECLARE_INCIDENT_ACTION_TITLE, items).disabled,
|
|
506
|
+
).toBeFalsy();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("leaves both working for a master admin", () => {
|
|
510
|
+
permissionsForTest = [];
|
|
511
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(true);
|
|
512
|
+
renderHarness();
|
|
513
|
+
|
|
514
|
+
const items: Array<Alert> = makeAlerts(1);
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
visibleAction(LINK_TO_INCIDENT_ACTION_TITLE, items).disabled,
|
|
518
|
+
).toBeFalsy();
|
|
519
|
+
expect(
|
|
520
|
+
visibleAction(DECLARE_INCIDENT_ACTION_TITLE, items).disabled,
|
|
521
|
+
).toBeFalsy();
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe(`the ${MAX_ALERTS_PER_INCIDENT_LINK_ACTION} alert cap`, () => {
|
|
526
|
+
test("offers both actions up to the cap", () => {
|
|
527
|
+
renderHarness();
|
|
528
|
+
|
|
529
|
+
const items: Array<Alert> = makeAlerts(
|
|
530
|
+
MAX_ALERTS_PER_INCIDENT_LINK_ACTION,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
expect(
|
|
534
|
+
visibleAction(LINK_TO_INCIDENT_ACTION_TITLE, items).disabled,
|
|
535
|
+
).toBeFalsy();
|
|
536
|
+
expect(
|
|
537
|
+
visibleAction(DECLARE_INCIDENT_ACTION_TITLE, items).disabled,
|
|
538
|
+
).toBeFalsy();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("locks both actions above the cap, with a reason", () => {
|
|
542
|
+
renderHarness();
|
|
543
|
+
|
|
544
|
+
const items: Array<Alert> = makeAlerts(
|
|
545
|
+
MAX_ALERTS_PER_INCIDENT_LINK_ACTION + 1,
|
|
546
|
+
);
|
|
547
|
+
const link: BulkActionButtonSchema<Alert> = visibleAction(
|
|
548
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
549
|
+
items,
|
|
550
|
+
);
|
|
551
|
+
const declare: BulkActionButtonSchema<Alert> = visibleAction(
|
|
552
|
+
DECLARE_INCIDENT_ACTION_TITLE,
|
|
553
|
+
items,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
expect(link.disabled).toBe(true);
|
|
557
|
+
expect(link.tooltip).toBe(LINK_CAP_TOOLTIP);
|
|
558
|
+
expect(LINK_CAP_TOOLTIP).toContain(
|
|
559
|
+
`${MAX_ALERTS_PER_INCIDENT_LINK_ACTION}`,
|
|
560
|
+
);
|
|
561
|
+
expect(declare.disabled).toBe(true);
|
|
562
|
+
expect(declare.tooltip).toBe(DECLARE_CAP_TOOLTIP);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("keeps the permission reason for a viewer above the cap", () => {
|
|
566
|
+
permissionsForTest = [Permission.Viewer];
|
|
567
|
+
renderHarness();
|
|
568
|
+
|
|
569
|
+
const link: BulkActionButtonSchema<Alert> = visibleAction(
|
|
570
|
+
LINK_TO_INCIDENT_ACTION_TITLE,
|
|
571
|
+
makeAlerts(MAX_ALERTS_PER_INCIDENT_LINK_ACTION + 1),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
expect(link.disabled).toBe(true);
|
|
575
|
+
expect(link.tooltip).toContain("You do not have permission");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("does nothing if the working action is invoked with too many alerts anyway", async () => {
|
|
579
|
+
renderHarness();
|
|
580
|
+
|
|
581
|
+
const items: Array<Alert> = makeAlerts(
|
|
582
|
+
MAX_ALERTS_PER_INCIDENT_LINK_ACTION + 1,
|
|
583
|
+
);
|
|
584
|
+
const working: Array<BulkActionButtonSchema<Alert>> = actions().filter(
|
|
585
|
+
(action: BulkActionButtonSchema<Alert>): boolean => {
|
|
586
|
+
return !action.disabled;
|
|
587
|
+
},
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(working).toHaveLength(2);
|
|
591
|
+
|
|
592
|
+
for (const action of working) {
|
|
593
|
+
await actAsync(async () => {
|
|
594
|
+
await action.onClick(makeActionProps(items));
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
expect(capturedModal).toBeNull();
|
|
599
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
600
|
+
expect(navigateSpy).not.toHaveBeenCalled();
|
|
601
|
+
expect(callOrder).toEqual([]);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe("Link to Incident", () => {
|
|
606
|
+
test("opens the dialog and lists recent incidents by number and title", async () => {
|
|
607
|
+
getListMock.mockResolvedValue(
|
|
608
|
+
listResult([
|
|
609
|
+
makeIncident({
|
|
610
|
+
id: "44444444-4444-4444-8444-000000000001",
|
|
611
|
+
title: "Database is down",
|
|
612
|
+
incidentNumber: 42,
|
|
613
|
+
incidentNumberWithPrefix: "INC-42",
|
|
614
|
+
}),
|
|
615
|
+
makeIncident({
|
|
616
|
+
id: "44444444-4444-4444-8444-000000000002",
|
|
617
|
+
title: "Checkout errors",
|
|
618
|
+
incidentNumber: 41,
|
|
619
|
+
}),
|
|
620
|
+
]),
|
|
621
|
+
);
|
|
622
|
+
renderHarness();
|
|
623
|
+
|
|
624
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(2));
|
|
625
|
+
|
|
626
|
+
await waitFor(() => {
|
|
627
|
+
expect(capturedModal?.isLoading).toBe(false);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
expect(capturedModal?.title).toBe("Link to Incident");
|
|
631
|
+
expect(capturedModal?.submitButtonText).toBe("Link Alerts");
|
|
632
|
+
expect(getListMock).toHaveBeenCalledTimes(1);
|
|
633
|
+
|
|
634
|
+
const request: any = getListMock.mock.calls[0]![0];
|
|
635
|
+
|
|
636
|
+
expect(request.modelType).toBe(Incident);
|
|
637
|
+
expect(request.limit).toBe(LINK_OPTIONS_LIMIT);
|
|
638
|
+
expect(request.sort).toEqual({ createdAt: SortOrder.Descending });
|
|
639
|
+
expect(request.select).toEqual({
|
|
640
|
+
_id: true,
|
|
641
|
+
title: true,
|
|
642
|
+
incidentNumber: true,
|
|
643
|
+
incidentNumberWithPrefix: true,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const field: Record<string, any> = capturedModal!.formProps.fields[0]!;
|
|
647
|
+
|
|
648
|
+
expect(capturedModal!.formProps.fields).toHaveLength(1);
|
|
649
|
+
expect(field["field"]).toEqual({ linkedRecordId: true });
|
|
650
|
+
expect(field["title"]).toBe("Incident");
|
|
651
|
+
expect(field["placeholder"]).toBe("Select an incident");
|
|
652
|
+
expect(field["required"]).toBe(true);
|
|
653
|
+
expect(field["dropdownModal"]).toEqual({
|
|
654
|
+
type: Incident,
|
|
655
|
+
labelField: "title",
|
|
656
|
+
valueField: "_id",
|
|
657
|
+
});
|
|
658
|
+
expect(field["dropdownOptions"]).toEqual([
|
|
659
|
+
{
|
|
660
|
+
label: "INC-42: Database is down",
|
|
661
|
+
value: "44444444-4444-4444-8444-000000000001",
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
label: "#41: Checkout errors",
|
|
665
|
+
value: "44444444-4444-4444-8444-000000000002",
|
|
666
|
+
},
|
|
667
|
+
]);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("still opens the dialog when the incident list cannot be read", async () => {
|
|
671
|
+
getListMock.mockImplementation(failWith("Forbidden"));
|
|
672
|
+
renderHarness();
|
|
673
|
+
|
|
674
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(1));
|
|
675
|
+
|
|
676
|
+
await waitFor(() => {
|
|
677
|
+
expect(capturedModal?.isLoading).toBe(false);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
expect(capturedModal!.formProps.fields[0]!["dropdownOptions"]).toEqual(
|
|
681
|
+
[],
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("creates one link per alert, reporting progress after each", async () => {
|
|
686
|
+
renderHarness();
|
|
687
|
+
const items: Array<Alert> = makeAlerts(3);
|
|
688
|
+
|
|
689
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, items);
|
|
690
|
+
await submitLink(INCIDENT_ID);
|
|
691
|
+
|
|
692
|
+
expect(createMock).toHaveBeenCalledTimes(3);
|
|
693
|
+
|
|
694
|
+
items.forEach((alert: Alert, index: number) => {
|
|
695
|
+
const link: IncidentAlert = createdLink(index);
|
|
696
|
+
|
|
697
|
+
expect(link.incidentId?.toString()).toBe(INCIDENT_ID);
|
|
698
|
+
expect(link.alertId?.toString()).toBe(alert._id);
|
|
699
|
+
expect(link.projectId?.toString()).toBe(PROJECT_ID);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
expect(callOrder).toEqual([
|
|
703
|
+
"start",
|
|
704
|
+
"progress",
|
|
705
|
+
"progress",
|
|
706
|
+
"progress",
|
|
707
|
+
"end",
|
|
708
|
+
]);
|
|
709
|
+
expect(progressSnapshots).toEqual([
|
|
710
|
+
{
|
|
711
|
+
inProgress: 2,
|
|
712
|
+
success: ["Alert 1"],
|
|
713
|
+
failed: [],
|
|
714
|
+
failedMessages: [],
|
|
715
|
+
total: 3,
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
inProgress: 1,
|
|
719
|
+
success: ["Alert 1", "Alert 2"],
|
|
720
|
+
failed: [],
|
|
721
|
+
failedMessages: [],
|
|
722
|
+
total: 3,
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
inProgress: 0,
|
|
726
|
+
success: ["Alert 1", "Alert 2", "Alert 3"],
|
|
727
|
+
failed: [],
|
|
728
|
+
failedMessages: [],
|
|
729
|
+
total: 3,
|
|
730
|
+
},
|
|
731
|
+
]);
|
|
732
|
+
|
|
733
|
+
// The dialog closes so the progress modal is visible.
|
|
734
|
+
expect(
|
|
735
|
+
document.querySelector('[data-testid="basic-form-modal"]'),
|
|
736
|
+
).toBeNull();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("does not read incidents until the dialog opens", () => {
|
|
740
|
+
renderHarness();
|
|
741
|
+
|
|
742
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
743
|
+
expect(capturedModal).toBeNull();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("counts an alert that is already linked as done and isolates other failures", async () => {
|
|
747
|
+
createMock
|
|
748
|
+
.mockImplementationOnce(failWith(INCIDENT_ALERT_ALREADY_LINKED_MESSAGE))
|
|
749
|
+
.mockImplementationOnce(failWith("You cannot read this alert."))
|
|
750
|
+
.mockResolvedValueOnce({});
|
|
751
|
+
renderHarness();
|
|
752
|
+
|
|
753
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(3));
|
|
754
|
+
await submitLink(INCIDENT_ID);
|
|
755
|
+
|
|
756
|
+
expect(createMock).toHaveBeenCalledTimes(3);
|
|
757
|
+
expect(progressSnapshots[progressSnapshots.length - 1]).toEqual({
|
|
758
|
+
inProgress: 0,
|
|
759
|
+
success: ["Alert 1", "Alert 3"],
|
|
760
|
+
failed: ["Alert 2"],
|
|
761
|
+
failedMessages: ["You cannot read this alert."],
|
|
762
|
+
total: 3,
|
|
763
|
+
});
|
|
764
|
+
expect(callOrder[callOrder.length - 1]).toBe("end");
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
/*
|
|
768
|
+
* Two links for the same pair that race past the model's check are
|
|
769
|
+
* refused by the unique index instead, in its own words. The alert is
|
|
770
|
+
* linked either way, so it still counts as done.
|
|
771
|
+
*/
|
|
772
|
+
test("counts a duplicate caught by the unique index as done too", async () => {
|
|
773
|
+
createMock
|
|
774
|
+
.mockImplementationOnce(
|
|
775
|
+
failWith(
|
|
776
|
+
"A Incident Alert with the same Incident Id, Alert Id, Project Id already exists. Please use different values and try again.",
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
.mockResolvedValueOnce({});
|
|
780
|
+
renderHarness();
|
|
781
|
+
|
|
782
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(2));
|
|
783
|
+
await submitLink(INCIDENT_ID);
|
|
784
|
+
|
|
785
|
+
expect(progressSnapshots[progressSnapshots.length - 1]).toEqual({
|
|
786
|
+
inProgress: 0,
|
|
787
|
+
success: ["Alert 1", "Alert 2"],
|
|
788
|
+
failed: [],
|
|
789
|
+
failedMessages: [],
|
|
790
|
+
total: 2,
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("accepts the selected incident as a dropdown option object", async () => {
|
|
795
|
+
renderHarness();
|
|
796
|
+
|
|
797
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(1));
|
|
798
|
+
await submitLink({ label: "INC-1: x", value: INCIDENT_ID });
|
|
799
|
+
|
|
800
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
801
|
+
expect(createdLink(0).incidentId?.toString()).toBe(INCIDENT_ID);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("starts nothing without an incident", async () => {
|
|
805
|
+
renderHarness();
|
|
806
|
+
|
|
807
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(2));
|
|
808
|
+
await submitLink("");
|
|
809
|
+
|
|
810
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
811
|
+
expect(callOrder).toEqual([]);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("closing the dialog links nothing", async () => {
|
|
815
|
+
renderHarness();
|
|
816
|
+
|
|
817
|
+
await click(LINK_TO_INCIDENT_ACTION_TITLE, makeAlerts(2));
|
|
818
|
+
|
|
819
|
+
const onClose: () => void = capturedModal!.onClose;
|
|
820
|
+
|
|
821
|
+
act(() => {
|
|
822
|
+
onClose();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
expect(
|
|
826
|
+
document.querySelector('[data-testid="basic-form-modal"]'),
|
|
827
|
+
).toBeNull();
|
|
828
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
829
|
+
expect(callOrder).toEqual([]);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe("Declare Incident", () => {
|
|
834
|
+
test("navigates to the create page with the selected alert ids", async () => {
|
|
835
|
+
renderHarness();
|
|
836
|
+
const items: Array<Alert> = makeAlerts(2);
|
|
837
|
+
|
|
838
|
+
await click(DECLARE_INCIDENT_ACTION_TITLE, items);
|
|
839
|
+
|
|
840
|
+
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
|
841
|
+
expect(navigateSpy.mock.calls[0]![0]!.toString()).toBe(
|
|
842
|
+
`/dashboard/${PROJECT_ID}/incidents/create?alertIds=${items[0]!._id},${items[1]!._id}`,
|
|
843
|
+
);
|
|
844
|
+
// No progress modal: it is a navigation, not a bulk write.
|
|
845
|
+
expect(callOrder).toEqual([]);
|
|
846
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe("the bulk incident link helpers", () => {
|
|
852
|
+
beforeEach(() => {
|
|
853
|
+
jest
|
|
854
|
+
.spyOn(ProjectUtil, "getCurrentProjectId")
|
|
855
|
+
.mockReturnValue(new ObjectID(PROJECT_ID));
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
afterEach(() => {
|
|
859
|
+
jest.restoreAllMocks();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test("builds the declare route from the shared query parameter", () => {
|
|
863
|
+
const route: Route = getDeclareIncidentFromAlertsRoute(["a", "b", "c"]);
|
|
864
|
+
|
|
865
|
+
expect(route.toString()).toBe(
|
|
866
|
+
`/dashboard/${PROJECT_ID}/incidents/create?alertIds=a,b,c`,
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
});
|