@oneuptime/common 12.0.19 → 12.0.21
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/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,465 @@
|
|
|
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, screen, waitFor } from "@testing-library/react";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { Location } from "react-router-dom";
|
|
13
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Issue #3374's twin. The Alert Episode detail page carried a byte for byte
|
|
17
|
+
* copy of the crashing Incident Episode severity renderer, including the
|
|
18
|
+
* throw - and alertSeverityId is nullable in exactly the same way, so an
|
|
19
|
+
* Alert Episode created without a severity could not be opened either. The severity field's
|
|
20
|
+
* getElement threw BadDataException("Episode Severity not found") from inside
|
|
21
|
+
* Detail's own render, so the whole route was replaced by the app level
|
|
22
|
+
* "Something went wrong" boundary. The only recovery was deleting the episode,
|
|
23
|
+
* which detached every member incident.
|
|
24
|
+
*
|
|
25
|
+
* These tests drive the real page against a fake server and assert what a
|
|
26
|
+
* person actually sees: the Episode Severity row, holding a placeholder.
|
|
27
|
+
*
|
|
28
|
+
* The current state field had the identical shape. Its FK is NOT NULL, but the
|
|
29
|
+
* relation is nullable with orphanedRowAction "nullify" and nothing stops an
|
|
30
|
+
* in-use AlertState from being soft deleted - so it is covered here too.
|
|
31
|
+
*
|
|
32
|
+
* Detail now also swallows a throwing field renderer as a last resort. That
|
|
33
|
+
* guard must NOT be what makes these pass: every case asserts the page never
|
|
34
|
+
* logged "threw and was ignored", which is how a regression at the page level
|
|
35
|
+
* stays visible instead of being quietly absorbed by the framework.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const EPISODE_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
39
|
+
const PROJECT_ID: string = "22222222-2222-4222-8222-222222222222";
|
|
40
|
+
|
|
41
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
42
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
43
|
+
const getCommonHeadersMock: MockFunction = getJestMockFunction();
|
|
44
|
+
const loggerErrorMock: MockFunction = getJestMockFunction();
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
|
|
48
|
+
* requires, so the mock consts above are still unassigned when the factory
|
|
49
|
+
* runs. Dereferencing them lazily, at call time, is what makes this work.
|
|
50
|
+
*/
|
|
51
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
52
|
+
return {
|
|
53
|
+
__esModule: true,
|
|
54
|
+
default: {
|
|
55
|
+
getItem: (...args: Array<any>) => {
|
|
56
|
+
return getItemMock(...args);
|
|
57
|
+
},
|
|
58
|
+
getList: (...args: Array<any>) => {
|
|
59
|
+
return getListMock(...args);
|
|
60
|
+
},
|
|
61
|
+
getCommonHeaders: (...args: Array<any>) => {
|
|
62
|
+
return getCommonHeadersMock(...args);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
jest.mock("../../../UI/Utils/Logger", () => {
|
|
69
|
+
return {
|
|
70
|
+
__esModule: true,
|
|
71
|
+
Logger: {
|
|
72
|
+
error: (...args: Array<any>) => {
|
|
73
|
+
return loggerErrorMock(...args);
|
|
74
|
+
},
|
|
75
|
+
warn: () => {
|
|
76
|
+
return undefined;
|
|
77
|
+
},
|
|
78
|
+
log: () => {
|
|
79
|
+
return undefined;
|
|
80
|
+
},
|
|
81
|
+
info: () => {
|
|
82
|
+
return undefined;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The page's two heavy children each drive their own API traffic and neither
|
|
90
|
+
* renders a detail field. Stubbing them keeps this suite pointed at the
|
|
91
|
+
* Episode Details card, which is where #3374 lives.
|
|
92
|
+
*/
|
|
93
|
+
jest.mock(
|
|
94
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AlertEpisode/ChangeState",
|
|
95
|
+
() => {
|
|
96
|
+
return {
|
|
97
|
+
__esModule: true,
|
|
98
|
+
default: () => {
|
|
99
|
+
return null;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
jest.mock(
|
|
106
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AlertEpisode/AlertEpisodeFeed",
|
|
107
|
+
() => {
|
|
108
|
+
return {
|
|
109
|
+
__esModule: true,
|
|
110
|
+
default: () => {
|
|
111
|
+
return null;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
import AlertEpisodeView from "../../../../App/FeatureSet/Dashboard/src/Pages/Alerts/EpisodeView/Index";
|
|
118
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
119
|
+
import AlertEpisode from "../../../Models/DatabaseModels/AlertEpisode";
|
|
120
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
121
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
122
|
+
import Route from "../../../Types/API/Route";
|
|
123
|
+
import Color from "../../../Types/Color";
|
|
124
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
125
|
+
import Permission from "../../../Types/Permission";
|
|
126
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
127
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
128
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
129
|
+
|
|
130
|
+
const pageProps: PageComponentProps = {
|
|
131
|
+
pageRoute: new Route("/alerts"),
|
|
132
|
+
currentProject: null,
|
|
133
|
+
hasPaymentMethod: false,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface EpisodeSpec {
|
|
137
|
+
severity?: AlertSeverity | null | undefined;
|
|
138
|
+
state?: AlertState | null | undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type BuildSeverityFunction = (
|
|
142
|
+
name: string | null,
|
|
143
|
+
color: Color | null,
|
|
144
|
+
) => AlertSeverity;
|
|
145
|
+
|
|
146
|
+
const buildSeverity: BuildSeverityFunction = (
|
|
147
|
+
name: string | null,
|
|
148
|
+
color: Color | null,
|
|
149
|
+
): AlertSeverity => {
|
|
150
|
+
const severity: AlertSeverity = new AlertSeverity();
|
|
151
|
+
severity.id = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
152
|
+
|
|
153
|
+
if (name) {
|
|
154
|
+
severity.name = name;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (color) {
|
|
158
|
+
severity.color = color;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return severity;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type BuildStateFunction = (
|
|
165
|
+
name: string | null,
|
|
166
|
+
color: Color | null,
|
|
167
|
+
) => AlertState;
|
|
168
|
+
|
|
169
|
+
const buildState: BuildStateFunction = (
|
|
170
|
+
name: string | null,
|
|
171
|
+
color: Color | null,
|
|
172
|
+
): AlertState => {
|
|
173
|
+
const state: AlertState = new AlertState();
|
|
174
|
+
state.id = new ObjectID("44444444-4444-4444-8444-444444444444");
|
|
175
|
+
|
|
176
|
+
if (name) {
|
|
177
|
+
state.name = name;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (color) {
|
|
181
|
+
state.color = color;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return state;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* A row shaped like what the API actually returns for an episode created with
|
|
189
|
+
* `{"title": "..."}` and nothing else: a title, a number, a state assigned by
|
|
190
|
+
* AlertEpisodeService.onBeforeCreate, and no severity at all.
|
|
191
|
+
*/
|
|
192
|
+
type BuildEpisodeFunction = (spec: EpisodeSpec) => AlertEpisode;
|
|
193
|
+
|
|
194
|
+
const buildEpisode: BuildEpisodeFunction = (
|
|
195
|
+
spec: EpisodeSpec,
|
|
196
|
+
): AlertEpisode => {
|
|
197
|
+
const episode: AlertEpisode = new AlertEpisode();
|
|
198
|
+
episode.id = new ObjectID(EPISODE_ID);
|
|
199
|
+
episode.projectId = new ObjectID(PROJECT_ID);
|
|
200
|
+
episode.title = "episode without severity";
|
|
201
|
+
episode.episodeNumber = 7;
|
|
202
|
+
episode.episodeNumberWithPrefix = "#7";
|
|
203
|
+
episode.alertCount = 0;
|
|
204
|
+
episode.createdAt = new Date("2026-01-01T00:00:00.000Z");
|
|
205
|
+
|
|
206
|
+
if (spec.severity) {
|
|
207
|
+
episode.alertSeverity = spec.severity;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (spec.state) {
|
|
211
|
+
episode.currentAlertState = spec.state;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return episode;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
type RenderPageFunction = (spec: EpisodeSpec) => Promise<void>;
|
|
218
|
+
|
|
219
|
+
const renderPage: RenderPageFunction = async (
|
|
220
|
+
spec: EpisodeSpec,
|
|
221
|
+
): Promise<void> => {
|
|
222
|
+
getItemMock.mockResolvedValue(buildEpisode(spec) as never);
|
|
223
|
+
|
|
224
|
+
render(<AlertEpisodeView {...pageProps} />);
|
|
225
|
+
|
|
226
|
+
await waitFor((): void => {
|
|
227
|
+
expect(screen.getByText("Episode Severity")).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/*
|
|
232
|
+
* Detail renders each field as <container><FieldLabel/><value/></container>,
|
|
233
|
+
* and FieldLabel's root is the only div.space-y-1 in that subtree. Walking up
|
|
234
|
+
* from the label and taking the sibling is what a person reading the row does.
|
|
235
|
+
*/
|
|
236
|
+
type FieldValueFunction = (title: string) => string;
|
|
237
|
+
|
|
238
|
+
const fieldValue: FieldValueFunction = (title: string): string => {
|
|
239
|
+
const label: HTMLElement = screen.getByText(title);
|
|
240
|
+
const labelRoot: Element | null = label.closest("div.space-y-1");
|
|
241
|
+
const container: HTMLElement | null | undefined = labelRoot?.parentElement;
|
|
242
|
+
|
|
243
|
+
if (!container) {
|
|
244
|
+
throw new Error(`Could not find the field container for "${title}"`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const value: Element | undefined = container.children[1];
|
|
248
|
+
|
|
249
|
+
if (!value) {
|
|
250
|
+
throw new Error(`The field "${title}" rendered no value element`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (value.textContent || "").trim();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type PillForFunction = (title: string) => HTMLElement;
|
|
257
|
+
|
|
258
|
+
const pillFor: PillForFunction = (title: string): HTMLElement => {
|
|
259
|
+
const label: HTMLElement = screen.getByText(title);
|
|
260
|
+
const labelRoot: Element | null = label.closest("div.space-y-1");
|
|
261
|
+
const pill: HTMLElement | null | undefined =
|
|
262
|
+
labelRoot?.parentElement?.querySelector<HTMLElement>(
|
|
263
|
+
'[data-testid="pill"]',
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!pill) {
|
|
267
|
+
throw new Error(`The field "${title}" rendered no pill`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return pill;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
type ExpectNoCrashFunction = () => void;
|
|
274
|
+
|
|
275
|
+
/*
|
|
276
|
+
* Two separate claims, and both matter:
|
|
277
|
+
* - the app level ErrorBoundary copy is nowhere on the page, and
|
|
278
|
+
* - Detail never had to swallow a throw, so the page itself is null safe
|
|
279
|
+
* rather than being rescued by the framework guard.
|
|
280
|
+
*/
|
|
281
|
+
const expectNoCrash: ExpectNoCrashFunction = (): void => {
|
|
282
|
+
expect(screen.queryByText("Something went wrong")).toBeNull();
|
|
283
|
+
expect(screen.queryByText(/An unexpected error has occurred/)).toBeNull();
|
|
284
|
+
|
|
285
|
+
const swallowed: Array<unknown> = loggerErrorMock.mock.calls.filter(
|
|
286
|
+
(call: Array<unknown>): boolean => {
|
|
287
|
+
return String(call[0]).includes("threw and was ignored");
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(swallowed).toEqual([]);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
beforeEach((): void => {
|
|
295
|
+
getItemMock.mockReset();
|
|
296
|
+
getListMock.mockReset();
|
|
297
|
+
getCommonHeadersMock.mockReset();
|
|
298
|
+
loggerErrorMock.mockReset();
|
|
299
|
+
|
|
300
|
+
getListMock.mockResolvedValue({
|
|
301
|
+
data: [],
|
|
302
|
+
count: 0,
|
|
303
|
+
skip: 0,
|
|
304
|
+
limit: 0,
|
|
305
|
+
} as never);
|
|
306
|
+
getCommonHeadersMock.mockReturnValue({} as never);
|
|
307
|
+
|
|
308
|
+
const path: string = `/dashboard/${PROJECT_ID}/alerts/episodes/${EPISODE_ID}`;
|
|
309
|
+
|
|
310
|
+
window.history.pushState({}, "", path);
|
|
311
|
+
Navigation.setLocation({
|
|
312
|
+
pathname: path,
|
|
313
|
+
search: "",
|
|
314
|
+
hash: "",
|
|
315
|
+
state: null,
|
|
316
|
+
key: "test",
|
|
317
|
+
} as Location);
|
|
318
|
+
|
|
319
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
320
|
+
jest
|
|
321
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
322
|
+
.mockReturnValue([Permission.ProjectAdmin]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterEach((): void => {
|
|
326
|
+
cleanup();
|
|
327
|
+
jest.restoreAllMocks();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("Alert Episode detail page: severity (issue #3374)", () => {
|
|
331
|
+
test("renders the page when the episode has no severity at all", async () => {
|
|
332
|
+
await renderPage({ state: buildState("Created", new Color("#4b5563")) });
|
|
333
|
+
|
|
334
|
+
expect(screen.getByText("Episode Severity")).toBeInTheDocument();
|
|
335
|
+
expect(fieldValue("Episode Severity")).toEqual("-");
|
|
336
|
+
expectNoCrash();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("renders the page when the severity relation is explicitly null", async () => {
|
|
340
|
+
getItemMock.mockResolvedValue(
|
|
341
|
+
Object.assign(
|
|
342
|
+
buildEpisode({ state: buildState("Created", new Color("#4b5563")) }),
|
|
343
|
+
{ alertSeverity: null },
|
|
344
|
+
) as never,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
render(<AlertEpisodeView {...pageProps} />);
|
|
348
|
+
|
|
349
|
+
await waitFor((): void => {
|
|
350
|
+
expect(screen.getByText("Episode Severity")).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(fieldValue("Episode Severity")).toEqual("-");
|
|
354
|
+
expectNoCrash();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("still renders the rest of the episode when severity is missing", async () => {
|
|
358
|
+
await renderPage({ state: buildState("Created", new Color("#4b5563")) });
|
|
359
|
+
|
|
360
|
+
expect(screen.getByText("Episode Title")).toBeInTheDocument();
|
|
361
|
+
expect(fieldValue("Episode Title")).toEqual("episode without severity");
|
|
362
|
+
expect(fieldValue("Episode Number")).toContain("#7");
|
|
363
|
+
expect(fieldValue("Alert Count")).toEqual("0");
|
|
364
|
+
// An episode with no grouping rule is a manual one, not a crash.
|
|
365
|
+
expect(fieldValue("Grouping Rule")).toEqual("Manual Episode");
|
|
366
|
+
// No createdByUser on an API-created episode.
|
|
367
|
+
expect(fieldValue("Created By")).toEqual("System");
|
|
368
|
+
expectNoCrash();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("renders the severity pill when the episode has one", async () => {
|
|
372
|
+
await renderPage({
|
|
373
|
+
severity: buildSeverity("Critical", new Color("#ef4444")),
|
|
374
|
+
state: buildState("Created", new Color("#4b5563")),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const pill: HTMLElement = pillFor("Episode Severity");
|
|
378
|
+
|
|
379
|
+
expect(pill.textContent).toContain("Critical");
|
|
380
|
+
expect(pill.style.backgroundColor).toEqual("rgb(239, 68, 68)");
|
|
381
|
+
expectNoCrash();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("falls back to Unknown when the severity has no name", async () => {
|
|
385
|
+
await renderPage({
|
|
386
|
+
severity: buildSeverity(null, new Color("#ef4444")),
|
|
387
|
+
state: buildState("Created", new Color("#4b5563")),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(pillFor("Episode Severity").textContent).toContain("Unknown");
|
|
391
|
+
expectNoCrash();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("renders a severity with no color instead of crashing", async () => {
|
|
395
|
+
await renderPage({
|
|
396
|
+
severity: buildSeverity("Critical", null),
|
|
397
|
+
state: buildState("Created", new Color("#4b5563")),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const pill: HTMLElement = pillFor("Episode Severity");
|
|
401
|
+
|
|
402
|
+
expect(pill.textContent).toContain("Critical");
|
|
403
|
+
// Black is the declared fallback in the page.
|
|
404
|
+
expect(pill.style.backgroundColor).toEqual("rgb(0, 0, 0)");
|
|
405
|
+
expectNoCrash();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("Alert Episode detail page: current state", () => {
|
|
410
|
+
test("renders a placeholder when the state relation is absent", async () => {
|
|
411
|
+
await renderPage({
|
|
412
|
+
severity: buildSeverity("Critical", new Color("#ef4444")),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(screen.getByText("Current State")).toBeInTheDocument();
|
|
416
|
+
expect(fieldValue("Current State")).toEqual("-");
|
|
417
|
+
expectNoCrash();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("renders the state pill when the episode has one", async () => {
|
|
421
|
+
await renderPage({
|
|
422
|
+
severity: buildSeverity("Critical", new Color("#ef4444")),
|
|
423
|
+
state: buildState("Acknowledged", new Color("#f59e0b")),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const pill: HTMLElement = pillFor("Current State");
|
|
427
|
+
|
|
428
|
+
expect(pill.textContent).toContain("Acknowledged");
|
|
429
|
+
expect(pill.style.backgroundColor).toEqual("rgb(245, 158, 11)");
|
|
430
|
+
expectNoCrash();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("falls back to Unknown when the state has no name", async () => {
|
|
434
|
+
await renderPage({
|
|
435
|
+
severity: buildSeverity("Critical", new Color("#ef4444")),
|
|
436
|
+
state: buildState(null, new Color("#f59e0b")),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(pillFor("Current State").textContent).toContain("Unknown");
|
|
440
|
+
expectNoCrash();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("renders a state with no color instead of crashing", async () => {
|
|
444
|
+
await renderPage({
|
|
445
|
+
severity: buildSeverity("Critical", new Color("#ef4444")),
|
|
446
|
+
state: buildState("Created", null),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(pillFor("Current State").style.backgroundColor).toEqual(
|
|
450
|
+
"rgb(0, 0, 0)",
|
|
451
|
+
);
|
|
452
|
+
expectNoCrash();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe("Alert Episode detail page: both relations missing", () => {
|
|
457
|
+
test("renders both placeholders when state and severity are absent", async () => {
|
|
458
|
+
await renderPage({});
|
|
459
|
+
|
|
460
|
+
expect(fieldValue("Episode Severity")).toEqual("-");
|
|
461
|
+
expect(fieldValue("Current State")).toEqual("-");
|
|
462
|
+
expect(fieldValue("Episode Title")).toEqual("episode without severity");
|
|
463
|
+
expectNoCrash();
|
|
464
|
+
});
|
|
465
|
+
});
|