@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,828 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, render, waitFor } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { MemoryRouter } from "react-router-dom";
|
|
13
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The "Linked Alerts" page of an incident and the "Linked Incidents" page of
|
|
17
|
+
* an alert. Both are a ModelTable over IncidentAlert, stubbed here to capture
|
|
18
|
+
* its props: what is pinned is that each lists only the viewed record's links,
|
|
19
|
+
* links from a card button of its own (not the table's create form, whose
|
|
20
|
+
* picker could only show bare titles) gated on creating the link AND reading
|
|
21
|
+
* the other side, reads "Link" / "Unlink" rather than create / delete - the
|
|
22
|
+
* bulk action included - resolves the other side's current state from a
|
|
23
|
+
* separate state list (relations are only joined one level deep), exports
|
|
24
|
+
* real values to CSV, and - on the alert page - that "Declare Incident" is
|
|
25
|
+
* gated on both permissions it needs. The link dialog itself is covered in
|
|
26
|
+
* IncidentAlertLinkDialog.test.tsx.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
jest.mock("react-i18next", () => {
|
|
30
|
+
return {
|
|
31
|
+
useTranslation: () => {
|
|
32
|
+
return {
|
|
33
|
+
t: (key: string, opts?: { defaultValue?: string }): string => {
|
|
34
|
+
return opts?.defaultValue ?? key;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
type CapturedTableProps = Record<string, any>;
|
|
42
|
+
|
|
43
|
+
let capturedTables: Array<CapturedTableProps> = [];
|
|
44
|
+
|
|
45
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: (props: CapturedTableProps): React.ReactElement => {
|
|
49
|
+
capturedTables.push(props);
|
|
50
|
+
return <div data-testid="model-table" />;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
56
|
+
|
|
57
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
58
|
+
return {
|
|
59
|
+
__esModule: true,
|
|
60
|
+
default: {
|
|
61
|
+
getList: (...args: Array<any>) => {
|
|
62
|
+
return getListMock(...args);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
import IncidentViewAlerts from "../../../../App/FeatureSet/Dashboard/src/Pages/Incidents/View/Alerts";
|
|
69
|
+
import AlertViewIncidents from "../../../../App/FeatureSet/Dashboard/src/Pages/Alerts/View/Incidents";
|
|
70
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
71
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
72
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
73
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
74
|
+
import IncidentAlert from "../../../Models/DatabaseModels/IncidentAlert";
|
|
75
|
+
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
|
76
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
77
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
78
|
+
import Route from "../../../Types/API/Route";
|
|
79
|
+
import Color from "../../../Types/Color";
|
|
80
|
+
import Email from "../../../Types/Email";
|
|
81
|
+
import Name from "../../../Types/Name";
|
|
82
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
83
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
84
|
+
import Permission from "../../../Types/Permission";
|
|
85
|
+
import { ModalTableBulkDefaultActions } from "../../../UI/Components/ModelTable/BaseModelTable";
|
|
86
|
+
import { CardButtonSchema } from "../../../UI/Components/Card/Card";
|
|
87
|
+
import FieldType from "../../../UI/Components/Types/FieldType";
|
|
88
|
+
import TableColumnsToCsv from "../../../UI/Utils/TableColumnsToCsv";
|
|
89
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
90
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
91
|
+
import PermissionGate from "../../../UI/Utils/PermissionGate";
|
|
92
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
93
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
94
|
+
|
|
95
|
+
const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
|
|
96
|
+
const MODEL_ID: string = "0193c0de-3333-4aaa-8bbb-000000000003";
|
|
97
|
+
const OTHER_ID: string = "0193c0de-3333-4aaa-8bbb-000000000004";
|
|
98
|
+
const STATE_ID: string = "0193c0de-3333-4aaa-8bbb-000000000005";
|
|
99
|
+
|
|
100
|
+
let permissionsForTest: Array<Permission> = [];
|
|
101
|
+
|
|
102
|
+
type ListResultFunction = (data: Array<unknown>) => {
|
|
103
|
+
data: Array<unknown>;
|
|
104
|
+
count: number;
|
|
105
|
+
skip: number;
|
|
106
|
+
limit: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const listResult: ListResultFunction = (data: Array<unknown>) => {
|
|
110
|
+
return { data: data, count: data.length, skip: 0, limit: data.length };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type StateFunction = <T extends AlertState | IncidentState>(modelType: {
|
|
114
|
+
new (): T;
|
|
115
|
+
}) => T;
|
|
116
|
+
|
|
117
|
+
const makeState: StateFunction = <
|
|
118
|
+
T extends AlertState | IncidentState,
|
|
119
|
+
>(modelType: {
|
|
120
|
+
new (): T;
|
|
121
|
+
}): T => {
|
|
122
|
+
const state: T = new modelType();
|
|
123
|
+
state._id = STATE_ID;
|
|
124
|
+
state.name = "Acknowledged";
|
|
125
|
+
state.color = new Color("#ff0000");
|
|
126
|
+
return state;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type OpenPageFunction = (
|
|
130
|
+
page: React.FunctionComponent<PageComponentProps>,
|
|
131
|
+
) => Promise<CapturedTableProps>;
|
|
132
|
+
|
|
133
|
+
const openPage: OpenPageFunction = async (
|
|
134
|
+
page: React.FunctionComponent<PageComponentProps>,
|
|
135
|
+
): Promise<CapturedTableProps> => {
|
|
136
|
+
const Page: React.FunctionComponent<PageComponentProps> = page;
|
|
137
|
+
|
|
138
|
+
render(
|
|
139
|
+
<MemoryRouter>
|
|
140
|
+
<Page
|
|
141
|
+
pageRoute={new Route("/dashboard/page")}
|
|
142
|
+
currentProject={new Project()}
|
|
143
|
+
hasPaymentMethod={true}
|
|
144
|
+
/>
|
|
145
|
+
</MemoryRouter>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// The state list lands after the first render.
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(getListMock).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(capturedTables.length).toBeGreaterThan(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return capturedTables[capturedTables.length - 1]!;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type RenderCellFunction = (
|
|
160
|
+
table: CapturedTableProps,
|
|
161
|
+
title: string,
|
|
162
|
+
item: IncidentAlert,
|
|
163
|
+
) => string;
|
|
164
|
+
|
|
165
|
+
// The text a column renders for a row.
|
|
166
|
+
const renderCell: RenderCellFunction = (
|
|
167
|
+
table: CapturedTableProps,
|
|
168
|
+
title: string,
|
|
169
|
+
item: IncidentAlert,
|
|
170
|
+
): string => {
|
|
171
|
+
const column: Record<string, any> | undefined = table["columns"].find(
|
|
172
|
+
(candidate: Record<string, any>): boolean => {
|
|
173
|
+
return candidate["title"] === title;
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!column) {
|
|
178
|
+
throw new Error(`No "${title}" column.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { container } = render(
|
|
182
|
+
<MemoryRouter>{column["getElement"](item)}</MemoryRouter>,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return container.textContent || "";
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
type ExportCellFunction = (
|
|
189
|
+
table: CapturedTableProps,
|
|
190
|
+
title: string,
|
|
191
|
+
item: IncidentAlert,
|
|
192
|
+
) => string;
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* What Export CSV writes for a column: BaseModelTable keys a column by its
|
|
196
|
+
* first field, and TableColumnsToCsv prefers the column's getExportValue.
|
|
197
|
+
*/
|
|
198
|
+
const exportCell: ExportCellFunction = (
|
|
199
|
+
table: CapturedTableProps,
|
|
200
|
+
title: string,
|
|
201
|
+
item: IncidentAlert,
|
|
202
|
+
): string => {
|
|
203
|
+
const column: Record<string, any> | undefined = table["columns"].find(
|
|
204
|
+
(candidate: Record<string, any>): boolean => {
|
|
205
|
+
return candidate["title"] === title;
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (!column) {
|
|
210
|
+
throw new Error(`No "${title}" column.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return TableColumnsToCsv.getCellValue(
|
|
214
|
+
item as any,
|
|
215
|
+
{
|
|
216
|
+
title: column["title"],
|
|
217
|
+
type: column["type"],
|
|
218
|
+
key: Object.keys(column["field"])[0] as any,
|
|
219
|
+
getExportValue: column["getExportValue"],
|
|
220
|
+
disableCsvExport: column["disableCsvExport"],
|
|
221
|
+
} as any,
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
type CardButtonFunction = (
|
|
226
|
+
table: CapturedTableProps,
|
|
227
|
+
title: string,
|
|
228
|
+
) => CardButtonSchema | undefined;
|
|
229
|
+
|
|
230
|
+
const cardButton: CardButtonFunction = (
|
|
231
|
+
table: CapturedTableProps,
|
|
232
|
+
title: string,
|
|
233
|
+
): CardButtonSchema | undefined => {
|
|
234
|
+
return (
|
|
235
|
+
(table["cardProps"]["buttons"] || []) as Array<CardButtonSchema>
|
|
236
|
+
).find((button: CardButtonSchema): boolean => {
|
|
237
|
+
return button.title === title;
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
type ColumnTitlesFunction = (table: CapturedTableProps) => Array<string>;
|
|
242
|
+
|
|
243
|
+
const columnTitles: ColumnTitlesFunction = (
|
|
244
|
+
table: CapturedTableProps,
|
|
245
|
+
): Array<string> => {
|
|
246
|
+
return table["columns"].map((column: Record<string, any>): string => {
|
|
247
|
+
return column["title"];
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
type LinkedByUserFunction = () => User;
|
|
252
|
+
|
|
253
|
+
const linkedByUser: LinkedByUserFunction = (): User => {
|
|
254
|
+
const user: User = new User();
|
|
255
|
+
user.name = new Name("Riley Responder");
|
|
256
|
+
user.email = new Email("riley@example.com");
|
|
257
|
+
return user;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
describe("the linked alerts and incidents pages", () => {
|
|
261
|
+
let navigateSpy: ReturnType<typeof jest.spyOn>;
|
|
262
|
+
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
capturedTables = [];
|
|
265
|
+
permissionsForTest = [Permission.ProjectMember];
|
|
266
|
+
getListMock.mockReset();
|
|
267
|
+
PermissionGate.clearPermissionPropsCache();
|
|
268
|
+
|
|
269
|
+
jest
|
|
270
|
+
.spyOn(ProjectUtil, "getCurrentProjectId")
|
|
271
|
+
.mockReturnValue(new ObjectID(PROJECT_ID));
|
|
272
|
+
jest
|
|
273
|
+
.spyOn(Navigation, "getLastParamAsObjectID")
|
|
274
|
+
.mockReturnValue(new ObjectID(MODEL_ID));
|
|
275
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockImplementation(() => {
|
|
276
|
+
return permissionsForTest;
|
|
277
|
+
});
|
|
278
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
279
|
+
navigateSpy = jest
|
|
280
|
+
.spyOn(Navigation, "navigate")
|
|
281
|
+
.mockImplementation((): void => {});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
cleanup();
|
|
286
|
+
jest.restoreAllMocks();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("an incident's Linked Alerts", () => {
|
|
290
|
+
beforeEach(() => {
|
|
291
|
+
getListMock.mockResolvedValue(listResult([makeState(AlertState)]));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("lists only this incident's links", async () => {
|
|
295
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
296
|
+
|
|
297
|
+
expect(Navigation.getLastParamAsObjectID).toHaveBeenCalledWith(1);
|
|
298
|
+
expect(table["modelType"]).toBe(IncidentAlert);
|
|
299
|
+
expect(table["query"]["incidentId"].toString()).toBe(MODEL_ID);
|
|
300
|
+
expect(table["query"]["projectId"].toString()).toBe(PROJECT_ID);
|
|
301
|
+
expect(Object.keys(table["query"]).sort()).toEqual([
|
|
302
|
+
"incidentId",
|
|
303
|
+
"projectId",
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("reads the alert states once, for this project", async () => {
|
|
308
|
+
await openPage(IncidentViewAlerts);
|
|
309
|
+
|
|
310
|
+
const request: any = getListMock.mock.calls[0]![0];
|
|
311
|
+
|
|
312
|
+
expect(request.modelType).toBe(AlertState);
|
|
313
|
+
expect(request.query.projectId.toString()).toBe(PROJECT_ID);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/*
|
|
317
|
+
* The table's own create form could only list alerts by title, and alerts
|
|
318
|
+
* from one monitor share a title. Linking is a card button that opens a
|
|
319
|
+
* dialog listing them by number instead.
|
|
320
|
+
*/
|
|
321
|
+
test("links an alert from a card button, not the table's create form", async () => {
|
|
322
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
323
|
+
const button: CardButtonSchema | undefined = cardButton(
|
|
324
|
+
table,
|
|
325
|
+
"Link Alert",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(table["isCreateable"]).toBe(false);
|
|
329
|
+
expect(table["isEditable"]).toBe(false);
|
|
330
|
+
expect(table["formFields"]).toBeUndefined();
|
|
331
|
+
expect(table["onBeforeCreate"]).toBeUndefined();
|
|
332
|
+
expect(table["singularName"]).toBe("Alert");
|
|
333
|
+
expect(table["cardProps"]["buttons"]).toHaveLength(1);
|
|
334
|
+
expect(button).toBeDefined();
|
|
335
|
+
expect(button!.icon).toBe(IconProp.Link);
|
|
336
|
+
expect(button!.disabled).toBeFalsy();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("the Link Alert button", () => {
|
|
340
|
+
type LinkButtonFunction = () => Promise<CardButtonSchema | undefined>;
|
|
341
|
+
|
|
342
|
+
const linkButton: LinkButtonFunction = async (): Promise<
|
|
343
|
+
CardButtonSchema | undefined
|
|
344
|
+
> => {
|
|
345
|
+
return cardButton(await openPage(IncidentViewAlerts), "Link Alert");
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
test("is locked for a viewer, naming the link permission", async () => {
|
|
349
|
+
permissionsForTest = [Permission.Viewer];
|
|
350
|
+
|
|
351
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
352
|
+
|
|
353
|
+
expect(button!.disabled).toBe(true);
|
|
354
|
+
expect(button!.tooltip).toContain(
|
|
355
|
+
"You do not have permission to create this Incident Alert.",
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/*
|
|
360
|
+
* An incident role may create the link row, but the server only links
|
|
361
|
+
* an alert the caller can read - and the picker could not list any.
|
|
362
|
+
*/
|
|
363
|
+
test("is locked for an incident member who cannot read alerts", async () => {
|
|
364
|
+
permissionsForTest = [Permission.IncidentMember];
|
|
365
|
+
|
|
366
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
367
|
+
|
|
368
|
+
expect(button!.disabled).toBe(true);
|
|
369
|
+
expect(button!.tooltip).toContain(
|
|
370
|
+
"You do not have permission to read this Alert.",
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("works for an incident member who can also read alerts", async () => {
|
|
375
|
+
permissionsForTest = [
|
|
376
|
+
Permission.IncidentMember,
|
|
377
|
+
Permission.AlertViewer,
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
381
|
+
|
|
382
|
+
expect(button!.disabled).toBeFalsy();
|
|
383
|
+
expect(button!.tooltip).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("works for a master admin", async () => {
|
|
387
|
+
permissionsForTest = [];
|
|
388
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(true);
|
|
389
|
+
|
|
390
|
+
expect((await linkButton())!.disabled).toBeFalsy();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("is hidden until the permission snapshot has loaded", async () => {
|
|
394
|
+
permissionsForTest = [];
|
|
395
|
+
|
|
396
|
+
expect(await linkButton()).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("unlinks rather than deletes", async () => {
|
|
401
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
402
|
+
|
|
403
|
+
expect(table["isDeleteable"]).toBe(true);
|
|
404
|
+
expect(table["deleteButtonText"]).toBe("Unlink");
|
|
405
|
+
|
|
406
|
+
const confirmation: Record<string, string> = await table[
|
|
407
|
+
"getDeleteConfirmation"
|
|
408
|
+
](new IncidentAlert());
|
|
409
|
+
|
|
410
|
+
expect(confirmation["title"]).toBe("Unlink Alert");
|
|
411
|
+
expect(confirmation["submitButtonText"]).toBe("Unlink");
|
|
412
|
+
expect(confirmation["description"]).toContain("not deleted");
|
|
413
|
+
expect(table["bulkActions"]["buttons"]).toEqual([
|
|
414
|
+
ModalTableBulkDefaultActions.Delete,
|
|
415
|
+
]);
|
|
416
|
+
// The bulk action says Unlink too, and does not claim it is permanent.
|
|
417
|
+
expect(table["bulkActions"]["deleteVerb"]).toBe("Unlink");
|
|
418
|
+
expect(table["bulkActions"]["deleteIcon"]).toBe(IconProp.LinkSlash);
|
|
419
|
+
expect(table["bulkActions"]["deleteConfirmationWarning"]).toContain(
|
|
420
|
+
"Only the links are removed",
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("shows the alert, its current state and who linked it", async () => {
|
|
425
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
426
|
+
|
|
427
|
+
expect(columnTitles(table)).toEqual([
|
|
428
|
+
"Alert #",
|
|
429
|
+
"Title",
|
|
430
|
+
"Current State",
|
|
431
|
+
"Linked At",
|
|
432
|
+
"Linked By",
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
const alert: Alert = new Alert();
|
|
436
|
+
alert._id = OTHER_ID;
|
|
437
|
+
alert.title = "Database is down";
|
|
438
|
+
alert.alertNumber = 12;
|
|
439
|
+
alert.alertNumberWithPrefix = "ALT-12";
|
|
440
|
+
alert.currentAlertStateId = new ObjectID(STATE_ID);
|
|
441
|
+
|
|
442
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
443
|
+
link.alert = alert;
|
|
444
|
+
link.createdByUser = linkedByUser();
|
|
445
|
+
|
|
446
|
+
expect(renderCell(table, "Alert #", link)).toBe("ALT-12");
|
|
447
|
+
expect(renderCell(table, "Title", link)).toBe("Database is down");
|
|
448
|
+
expect(renderCell(table, "Current State", link)).toContain(
|
|
449
|
+
"Acknowledged",
|
|
450
|
+
);
|
|
451
|
+
expect(renderCell(table, "Linked By", link)).toContain("Riley Responder");
|
|
452
|
+
|
|
453
|
+
const bare: IncidentAlert = new IncidentAlert();
|
|
454
|
+
|
|
455
|
+
expect(renderCell(table, "Alert #", bare)).toBe("-");
|
|
456
|
+
expect(renderCell(table, "Title", bare)).toBe("-");
|
|
457
|
+
expect(renderCell(table, "Current State", bare)).toBe("-");
|
|
458
|
+
expect(renderCell(table, "Linked By", bare)).toBe("-");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* Every column after the first is keyed by the same `alert` relation, so
|
|
463
|
+
* without an export value of its own each one exported the alert's title.
|
|
464
|
+
*/
|
|
465
|
+
test("exports the number, title, state and linker to CSV", async () => {
|
|
466
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
467
|
+
|
|
468
|
+
const alert: Alert = new Alert();
|
|
469
|
+
alert._id = OTHER_ID;
|
|
470
|
+
alert.title = "Database is down";
|
|
471
|
+
alert.alertNumber = 12;
|
|
472
|
+
alert.alertNumberWithPrefix = "ALT-12";
|
|
473
|
+
alert.currentAlertStateId = new ObjectID(STATE_ID);
|
|
474
|
+
|
|
475
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
476
|
+
link.alert = alert;
|
|
477
|
+
link.createdByUser = linkedByUser();
|
|
478
|
+
|
|
479
|
+
expect(exportCell(table, "Alert #", link)).toBe("ALT-12");
|
|
480
|
+
expect(exportCell(table, "Title", link)).toBe("Database is down");
|
|
481
|
+
expect(exportCell(table, "Current State", link)).toBe("Acknowledged");
|
|
482
|
+
expect(exportCell(table, "Linked By", link)).toBe("Riley Responder");
|
|
483
|
+
|
|
484
|
+
const unprefixed: Alert = new Alert();
|
|
485
|
+
unprefixed.alertNumber = 12;
|
|
486
|
+
|
|
487
|
+
const unprefixedLink: IncidentAlert = new IncidentAlert();
|
|
488
|
+
unprefixedLink.alert = unprefixed;
|
|
489
|
+
|
|
490
|
+
expect(exportCell(table, "Alert #", unprefixedLink)).toBe("#12");
|
|
491
|
+
|
|
492
|
+
const bare: IncidentAlert = new IncidentAlert();
|
|
493
|
+
|
|
494
|
+
for (const title of ["Alert #", "Title", "Current State", "Linked By"]) {
|
|
495
|
+
expect(exportCell(table, title, bare)).toBe("");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const unnamed: IncidentAlert = new IncidentAlert();
|
|
499
|
+
unnamed.createdByUser = new User();
|
|
500
|
+
unnamed.createdByUser.email = new Email("riley@example.com");
|
|
501
|
+
|
|
502
|
+
expect(exportCell(table, "Linked By", unnamed)).toBe("riley@example.com");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("gives every relation column an export value of its own", async () => {
|
|
506
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
507
|
+
|
|
508
|
+
for (const column of table["columns"] as Array<Record<string, any>>) {
|
|
509
|
+
if (column["type"] === FieldType.DateTime) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
expect(typeof column["getExportValue"]).toBe("function");
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("opens the alert from a row", async () => {
|
|
518
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
519
|
+
const action: Record<string, any> = table["actionButtons"][0];
|
|
520
|
+
const onComplete: MockFunction = getJestMockFunction();
|
|
521
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
522
|
+
link.alert = new Alert();
|
|
523
|
+
link.alert._id = OTHER_ID;
|
|
524
|
+
|
|
525
|
+
expect(action["title"]).toBe("View Alert");
|
|
526
|
+
|
|
527
|
+
action["onClick"](link, onComplete);
|
|
528
|
+
|
|
529
|
+
expect(navigateSpy.mock.calls[0]![0]!.toString()).toBe(
|
|
530
|
+
`/dashboard/${PROJECT_ID}/alerts/${OTHER_ID}`,
|
|
531
|
+
);
|
|
532
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("has only the Link Alert card button", async () => {
|
|
536
|
+
const table: CapturedTableProps = await openPage(IncidentViewAlerts);
|
|
537
|
+
|
|
538
|
+
expect(table["cardProps"]["title"]).toBe("Linked Alerts");
|
|
539
|
+
expect(
|
|
540
|
+
(table["cardProps"]["buttons"] as Array<CardButtonSchema>).map(
|
|
541
|
+
(button: CardButtonSchema): string => {
|
|
542
|
+
return button.title;
|
|
543
|
+
},
|
|
544
|
+
),
|
|
545
|
+
).toEqual(["Link Alert"]);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe("an alert's Linked Incidents", () => {
|
|
550
|
+
beforeEach(() => {
|
|
551
|
+
getListMock.mockResolvedValue(listResult([makeState(IncidentState)]));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("lists only this alert's links", async () => {
|
|
555
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
556
|
+
|
|
557
|
+
expect(table["modelType"]).toBe(IncidentAlert);
|
|
558
|
+
expect(table["query"]["alertId"].toString()).toBe(MODEL_ID);
|
|
559
|
+
expect(table["query"]["projectId"].toString()).toBe(PROJECT_ID);
|
|
560
|
+
expect(Object.keys(table["query"]).sort()).toEqual([
|
|
561
|
+
"alertId",
|
|
562
|
+
"projectId",
|
|
563
|
+
]);
|
|
564
|
+
expect(getListMock.mock.calls[0]![0].modelType).toBe(IncidentState);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("links an incident from a card button, not the table's create form", async () => {
|
|
568
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
569
|
+
const button: CardButtonSchema | undefined = cardButton(
|
|
570
|
+
table,
|
|
571
|
+
"Link Incident",
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
expect(table["isCreateable"]).toBe(false);
|
|
575
|
+
expect(table["formFields"]).toBeUndefined();
|
|
576
|
+
expect(table["onBeforeCreate"]).toBeUndefined();
|
|
577
|
+
expect(table["singularName"]).toBe("Incident");
|
|
578
|
+
expect(button).toBeDefined();
|
|
579
|
+
expect(button!.icon).toBe(IconProp.Link);
|
|
580
|
+
expect(button!.disabled).toBeFalsy();
|
|
581
|
+
|
|
582
|
+
// Link first, then Declare.
|
|
583
|
+
expect(
|
|
584
|
+
(table["cardProps"]["buttons"] as Array<CardButtonSchema>).map(
|
|
585
|
+
(candidate: CardButtonSchema): string => {
|
|
586
|
+
return candidate.title;
|
|
587
|
+
},
|
|
588
|
+
),
|
|
589
|
+
).toEqual(["Link Incident", "Declare Incident"]);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe("the Link Incident button", () => {
|
|
593
|
+
type LinkButtonFunction = () => Promise<CardButtonSchema | undefined>;
|
|
594
|
+
|
|
595
|
+
const linkButton: LinkButtonFunction = async (): Promise<
|
|
596
|
+
CardButtonSchema | undefined
|
|
597
|
+
> => {
|
|
598
|
+
return cardButton(await openPage(AlertViewIncidents), "Link Incident");
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
test("is locked for a viewer, naming the link permission", async () => {
|
|
602
|
+
permissionsForTest = [Permission.Viewer];
|
|
603
|
+
|
|
604
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
605
|
+
|
|
606
|
+
expect(button!.disabled).toBe(true);
|
|
607
|
+
expect(button!.tooltip).toContain(
|
|
608
|
+
"You do not have permission to create this Incident Alert.",
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
/*
|
|
613
|
+
* The case the docs used to get wrong: an alert role may create the
|
|
614
|
+
* link row, but the server only links an incident the caller can read.
|
|
615
|
+
*/
|
|
616
|
+
test("is locked for an alert member who cannot read incidents", async () => {
|
|
617
|
+
permissionsForTest = [Permission.AlertMember];
|
|
618
|
+
|
|
619
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
620
|
+
|
|
621
|
+
expect(button!.disabled).toBe(true);
|
|
622
|
+
expect(button!.tooltip).toContain(
|
|
623
|
+
"You do not have permission to read this Incident.",
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("works for an alert member who can also read incidents", async () => {
|
|
628
|
+
permissionsForTest = [
|
|
629
|
+
Permission.AlertMember,
|
|
630
|
+
Permission.IncidentViewer,
|
|
631
|
+
];
|
|
632
|
+
|
|
633
|
+
const button: CardButtonSchema | undefined = await linkButton();
|
|
634
|
+
|
|
635
|
+
expect(button!.disabled).toBeFalsy();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("is hidden until the permission snapshot has loaded", async () => {
|
|
639
|
+
permissionsForTest = [];
|
|
640
|
+
|
|
641
|
+
expect(await linkButton()).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("unlinks rather than deletes", async () => {
|
|
646
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
647
|
+
|
|
648
|
+
expect(table["deleteButtonText"]).toBe("Unlink");
|
|
649
|
+
|
|
650
|
+
const confirmation: Record<string, string> = await table[
|
|
651
|
+
"getDeleteConfirmation"
|
|
652
|
+
](new IncidentAlert());
|
|
653
|
+
|
|
654
|
+
expect(confirmation["title"]).toBe("Unlink Incident");
|
|
655
|
+
expect(confirmation["submitButtonText"]).toBe("Unlink");
|
|
656
|
+
expect(table["bulkActions"]["buttons"]).toEqual([
|
|
657
|
+
ModalTableBulkDefaultActions.Delete,
|
|
658
|
+
]);
|
|
659
|
+
expect(table["bulkActions"]["deleteVerb"]).toBe("Unlink");
|
|
660
|
+
expect(table["bulkActions"]["deleteIcon"]).toBe(IconProp.LinkSlash);
|
|
661
|
+
expect(table["bulkActions"]["deleteConfirmationWarning"]).toContain(
|
|
662
|
+
"Only the links are removed",
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("shows the incident and its current state", async () => {
|
|
667
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
668
|
+
|
|
669
|
+
expect(columnTitles(table)).toEqual([
|
|
670
|
+
"Incident #",
|
|
671
|
+
"Title",
|
|
672
|
+
"Current State",
|
|
673
|
+
"Linked At",
|
|
674
|
+
"Linked By",
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
const incident: Incident = new Incident();
|
|
678
|
+
incident._id = OTHER_ID;
|
|
679
|
+
incident.title = "Checkout outage";
|
|
680
|
+
incident.incidentNumber = 7;
|
|
681
|
+
incident.currentIncidentStateId = new ObjectID(STATE_ID);
|
|
682
|
+
|
|
683
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
684
|
+
link.incident = incident;
|
|
685
|
+
|
|
686
|
+
expect(renderCell(table, "Incident #", link)).toBe("#7");
|
|
687
|
+
expect(renderCell(table, "Title", link)).toBe("Checkout outage");
|
|
688
|
+
expect(renderCell(table, "Current State", link)).toContain(
|
|
689
|
+
"Acknowledged",
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("exports the number, title, state and linker to CSV", async () => {
|
|
694
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
695
|
+
|
|
696
|
+
const incident: Incident = new Incident();
|
|
697
|
+
incident._id = OTHER_ID;
|
|
698
|
+
incident.title = "Checkout outage";
|
|
699
|
+
incident.incidentNumber = 7;
|
|
700
|
+
incident.incidentNumberWithPrefix = "INC-7";
|
|
701
|
+
incident.currentIncidentStateId = new ObjectID(STATE_ID);
|
|
702
|
+
|
|
703
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
704
|
+
link.incident = incident;
|
|
705
|
+
link.createdByUser = linkedByUser();
|
|
706
|
+
|
|
707
|
+
expect(exportCell(table, "Incident #", link)).toBe("INC-7");
|
|
708
|
+
expect(exportCell(table, "Title", link)).toBe("Checkout outage");
|
|
709
|
+
expect(exportCell(table, "Current State", link)).toBe("Acknowledged");
|
|
710
|
+
expect(exportCell(table, "Linked By", link)).toBe("Riley Responder");
|
|
711
|
+
|
|
712
|
+
const bare: IncidentAlert = new IncidentAlert();
|
|
713
|
+
|
|
714
|
+
for (const title of [
|
|
715
|
+
"Incident #",
|
|
716
|
+
"Title",
|
|
717
|
+
"Current State",
|
|
718
|
+
"Linked By",
|
|
719
|
+
]) {
|
|
720
|
+
expect(exportCell(table, title, bare)).toBe("");
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("opens the incident from a row", async () => {
|
|
725
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
726
|
+
const action: Record<string, any> = table["actionButtons"][0];
|
|
727
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
728
|
+
link.incident = new Incident();
|
|
729
|
+
link.incident._id = OTHER_ID;
|
|
730
|
+
|
|
731
|
+
expect(action["title"]).toBe("View Incident");
|
|
732
|
+
|
|
733
|
+
action["onClick"](link, (): void => {});
|
|
734
|
+
|
|
735
|
+
expect(navigateSpy.mock.calls[0]![0]!.toString()).toBe(
|
|
736
|
+
`/dashboard/${PROJECT_ID}/incidents/${OTHER_ID}`,
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
describe("Declare Incident", () => {
|
|
741
|
+
type DeclareButtonFunction = () => Promise<CardButtonSchema | undefined>;
|
|
742
|
+
|
|
743
|
+
const declareButton: DeclareButtonFunction = async (): Promise<
|
|
744
|
+
CardButtonSchema | undefined
|
|
745
|
+
> => {
|
|
746
|
+
const table: CapturedTableProps = await openPage(AlertViewIncidents);
|
|
747
|
+
|
|
748
|
+
return (table["cardProps"]["buttons"] as Array<CardButtonSchema>).find(
|
|
749
|
+
(button: CardButtonSchema): boolean => {
|
|
750
|
+
return button.title === "Declare Incident";
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
test("declares an incident from this alert", async () => {
|
|
756
|
+
const button: CardButtonSchema | undefined = await declareButton();
|
|
757
|
+
|
|
758
|
+
expect(button).toBeDefined();
|
|
759
|
+
expect(button!.disabled).toBeFalsy();
|
|
760
|
+
|
|
761
|
+
button!.onClick();
|
|
762
|
+
|
|
763
|
+
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
|
764
|
+
expect(navigateSpy.mock.calls[0]![0]!.toString()).toBe(
|
|
765
|
+
`/dashboard/${PROJECT_ID}/incidents/create?alertIds=${MODEL_ID}`,
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("is locked for a viewer, saying why, and goes nowhere", async () => {
|
|
770
|
+
permissionsForTest = [Permission.Viewer];
|
|
771
|
+
|
|
772
|
+
const button: CardButtonSchema | undefined = await declareButton();
|
|
773
|
+
|
|
774
|
+
expect(button!.disabled).toBe(true);
|
|
775
|
+
expect(button!.tooltip).toContain(
|
|
776
|
+
"You do not have permission to create this Incident.",
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
button!.onClick();
|
|
780
|
+
|
|
781
|
+
expect(navigateSpy).not.toHaveBeenCalled();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("is locked for someone who may link but not create incidents", async () => {
|
|
785
|
+
permissionsForTest = [Permission.AlertMember];
|
|
786
|
+
|
|
787
|
+
const button: CardButtonSchema | undefined = await declareButton();
|
|
788
|
+
|
|
789
|
+
expect(button!.disabled).toBe(true);
|
|
790
|
+
expect(button!.tooltip).toContain(
|
|
791
|
+
"You do not have permission to create this Incident.",
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("is locked for someone who may create incidents but not link them", async () => {
|
|
796
|
+
permissionsForTest = [Permission.CreateProjectIncident];
|
|
797
|
+
|
|
798
|
+
const button: CardButtonSchema | undefined = await declareButton();
|
|
799
|
+
|
|
800
|
+
expect(button!.disabled).toBe(true);
|
|
801
|
+
expect(button!.tooltip).toContain(
|
|
802
|
+
"You do not have permission to create this Incident Alert.",
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("is hidden until the permission snapshot has loaded", async () => {
|
|
807
|
+
permissionsForTest = [];
|
|
808
|
+
|
|
809
|
+
expect(await declareButton()).toBeUndefined();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
/*
|
|
813
|
+
* Declaring needs no incident read beyond creating one: the alert is
|
|
814
|
+
* readable (the user is looking at it), and the new incident is theirs.
|
|
815
|
+
*/
|
|
816
|
+
test("is not locked for lack of incident read", async () => {
|
|
817
|
+
permissionsForTest = [
|
|
818
|
+
Permission.AlertMember,
|
|
819
|
+
Permission.CreateProjectIncident,
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
const button: CardButtonSchema | undefined = await declareButton();
|
|
823
|
+
|
|
824
|
+
expect(button!.disabled).toBeFalsy();
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
});
|