@pagerduty/backstage-plugin 0.7.0
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/CHANGELOG.md +1390 -0
- package/LICENSE +201 -0
- package/README.md +258 -0
- package/assets/emptystate.svg +1 -0
- package/config.d.ts +28 -0
- package/dist/assets/emptystate.svg +1 -0
- package/dist/esm/index-308a2487.esm.js +29 -0
- package/dist/esm/index-308a2487.esm.js.map +1 -0
- package/dist/esm/index-7a206e9e.esm.js +777 -0
- package/dist/esm/index-7a206e9e.esm.js.map +1 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.esm.js +23 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { createApiRef, createRouteRef, createPlugin, createApiFactory, discoveryApiRef, configApiRef, fetchApiRef, createComponentExtension, useApi, alertApiRef, identityApiRef } from '@backstage/core-plugin-api';
|
|
2
|
+
import { NotFoundError } from '@backstage/errors';
|
|
3
|
+
import { createCardExtension } from '@backstage/plugin-home-react';
|
|
4
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
5
|
+
import { useEntity } from '@backstage/plugin-catalog-react';
|
|
6
|
+
import { makeStyles, ListItem, ListItemText, Chip, Typography, ListItemSecondaryAction, Tooltip, IconButton, Grid, List, ListItemIcon, ListSubheader, Button, Dialog, DialogTitle, DialogContent, TextField, DialogActions, CircularProgress, Card, CardHeader, Divider, CardContent } from '@material-ui/core';
|
|
7
|
+
import Done from '@material-ui/icons/Done';
|
|
8
|
+
import Warning from '@material-ui/icons/Warning';
|
|
9
|
+
import { DateTime, Duration } from 'luxon';
|
|
10
|
+
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
|
11
|
+
import { Link, Progress, StatusWarning, EmptyState, HeaderIconLinkRow, TabbedCard, CardTab, InfoCard } from '@backstage/core-components';
|
|
12
|
+
import EmptyStateImage from '../assets/emptystate.svg';
|
|
13
|
+
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
|
14
|
+
import { Alert } from '@material-ui/lab';
|
|
15
|
+
import Avatar from '@material-ui/core/Avatar';
|
|
16
|
+
import EmailIcon from '@material-ui/icons/Email';
|
|
17
|
+
import useAsync from 'react-use/lib/useAsync';
|
|
18
|
+
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
|
19
|
+
import WebIcon from '@material-ui/icons/Web';
|
|
20
|
+
import DateRangeIcon from '@material-ui/icons/DateRange';
|
|
21
|
+
import { parseEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
|
22
|
+
|
|
23
|
+
const PAGERDUTY_INTEGRATION_KEY = "pagerduty.com/integration-key";
|
|
24
|
+
const PAGERDUTY_SERVICE_ID = "pagerduty.com/service-id";
|
|
25
|
+
|
|
26
|
+
function getPagerDutyEntity(entity) {
|
|
27
|
+
const {
|
|
28
|
+
[PAGERDUTY_INTEGRATION_KEY]: integrationKey,
|
|
29
|
+
[PAGERDUTY_SERVICE_ID]: serviceId
|
|
30
|
+
} = entity.metadata.annotations || {};
|
|
31
|
+
const name = entity.metadata.name;
|
|
32
|
+
return { integrationKey, serviceId, name };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class UnauthorizedError extends Error {
|
|
36
|
+
}
|
|
37
|
+
const pagerDutyApiRef = createApiRef({
|
|
38
|
+
id: "plugin.pagerduty.api"
|
|
39
|
+
});
|
|
40
|
+
const commonGetServiceParams = "time_zone=UTC&include[]=integrations&include[]=escalation_policies";
|
|
41
|
+
class PagerDutyClient {
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
}
|
|
45
|
+
static fromConfig(configApi, dependencies) {
|
|
46
|
+
var _a;
|
|
47
|
+
const { discoveryApi, fetchApi } = dependencies;
|
|
48
|
+
const eventsBaseUrl = (_a = configApi.getOptionalString("pagerDuty.eventsBaseUrl")) != null ? _a : "https://events.pagerduty.com/v2";
|
|
49
|
+
return new PagerDutyClient({
|
|
50
|
+
eventsBaseUrl,
|
|
51
|
+
discoveryApi,
|
|
52
|
+
fetchApi
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async getServiceByPagerDutyEntity(pagerDutyEntity) {
|
|
56
|
+
const { integrationKey, serviceId } = pagerDutyEntity;
|
|
57
|
+
let response;
|
|
58
|
+
let url;
|
|
59
|
+
if (integrationKey) {
|
|
60
|
+
url = `${await this.config.discoveryApi.getBaseUrl(
|
|
61
|
+
"proxy"
|
|
62
|
+
)}/pagerduty/services?${commonGetServiceParams}&query=${integrationKey}`;
|
|
63
|
+
const { services } = await this.findByUrl(url);
|
|
64
|
+
const service = services[0];
|
|
65
|
+
if (!service)
|
|
66
|
+
throw new NotFoundError();
|
|
67
|
+
response = { service };
|
|
68
|
+
} else if (serviceId) {
|
|
69
|
+
url = `${await this.config.discoveryApi.getBaseUrl(
|
|
70
|
+
"proxy"
|
|
71
|
+
)}/pagerduty/services/${serviceId}?${commonGetServiceParams}`;
|
|
72
|
+
response = await this.findByUrl(url);
|
|
73
|
+
} else {
|
|
74
|
+
throw new NotFoundError();
|
|
75
|
+
}
|
|
76
|
+
return response;
|
|
77
|
+
}
|
|
78
|
+
async getServiceByEntity(entity) {
|
|
79
|
+
return await this.getServiceByPagerDutyEntity(getPagerDutyEntity(entity));
|
|
80
|
+
}
|
|
81
|
+
async getIncidentsByServiceId(serviceId) {
|
|
82
|
+
const params = `time_zone=UTC&sort_by=created_at&statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`;
|
|
83
|
+
const url = `${await this.config.discoveryApi.getBaseUrl(
|
|
84
|
+
"proxy"
|
|
85
|
+
)}/pagerduty/incidents?${params}`;
|
|
86
|
+
return await this.findByUrl(url);
|
|
87
|
+
}
|
|
88
|
+
async getChangeEventsByServiceId(serviceId) {
|
|
89
|
+
const params = `limit=5&time_zone=UTC&sort_by=timestamp`;
|
|
90
|
+
const url = `${await this.config.discoveryApi.getBaseUrl(
|
|
91
|
+
"proxy"
|
|
92
|
+
)}/pagerduty/services/${serviceId}/change_events?${params}`;
|
|
93
|
+
return await this.findByUrl(url);
|
|
94
|
+
}
|
|
95
|
+
async getOnCallByPolicyId(policyId) {
|
|
96
|
+
const params = `time_zone=UTC&include[]=users&escalation_policy_ids[]=${policyId}`;
|
|
97
|
+
const url = `${await this.config.discoveryApi.getBaseUrl(
|
|
98
|
+
"proxy"
|
|
99
|
+
)}/pagerduty/oncalls?${params}`;
|
|
100
|
+
return await this.findByUrl(url);
|
|
101
|
+
}
|
|
102
|
+
triggerAlarm(request) {
|
|
103
|
+
var _a;
|
|
104
|
+
const { integrationKey, source, description, userName } = request;
|
|
105
|
+
const body = JSON.stringify({
|
|
106
|
+
event_action: "trigger",
|
|
107
|
+
routing_key: integrationKey,
|
|
108
|
+
client: "Backstage",
|
|
109
|
+
client_url: source,
|
|
110
|
+
payload: {
|
|
111
|
+
summary: description,
|
|
112
|
+
source,
|
|
113
|
+
severity: "error",
|
|
114
|
+
class: "manual trigger",
|
|
115
|
+
custom_details: {
|
|
116
|
+
user: userName
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const options = {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
124
|
+
Accept: "application/json, text/plain, */*"
|
|
125
|
+
},
|
|
126
|
+
body
|
|
127
|
+
};
|
|
128
|
+
const url = (_a = this.config.eventsBaseUrl) != null ? _a : "https://events.pagerduty.com/v2";
|
|
129
|
+
return this.request(`${url}/enqueue`, options);
|
|
130
|
+
}
|
|
131
|
+
async findByUrl(url) {
|
|
132
|
+
const options = {
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers: {
|
|
135
|
+
Accept: "application/vnd.pagerduty+json;version=2",
|
|
136
|
+
"Content-Type": "application/json"
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const response = await this.request(url, options);
|
|
140
|
+
return response.json();
|
|
141
|
+
}
|
|
142
|
+
async request(url, options) {
|
|
143
|
+
const response = await this.config.fetchApi.fetch(url, options);
|
|
144
|
+
if (response.status === 401) {
|
|
145
|
+
throw new UnauthorizedError();
|
|
146
|
+
}
|
|
147
|
+
if (response.status === 404) {
|
|
148
|
+
throw new NotFoundError();
|
|
149
|
+
}
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const payload = await response.json();
|
|
152
|
+
const errors = payload.errors.map((error) => error).join(" ");
|
|
153
|
+
const message = `Request failed with ${response.status}, ${errors}`;
|
|
154
|
+
throw new Error(message);
|
|
155
|
+
}
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
createRouteRef({
|
|
161
|
+
id: "pagerduty"
|
|
162
|
+
});
|
|
163
|
+
const pagerDutyPlugin = createPlugin({
|
|
164
|
+
id: "pagerduty",
|
|
165
|
+
apis: [
|
|
166
|
+
createApiFactory({
|
|
167
|
+
api: pagerDutyApiRef,
|
|
168
|
+
deps: {
|
|
169
|
+
discoveryApi: discoveryApiRef,
|
|
170
|
+
configApi: configApiRef,
|
|
171
|
+
fetchApi: fetchApiRef
|
|
172
|
+
},
|
|
173
|
+
factory: ({ configApi, discoveryApi, fetchApi }) => PagerDutyClient.fromConfig(configApi, { discoveryApi, fetchApi })
|
|
174
|
+
})
|
|
175
|
+
]
|
|
176
|
+
});
|
|
177
|
+
const EntityPagerDutyCard$1 = pagerDutyPlugin.provide(
|
|
178
|
+
createComponentExtension({
|
|
179
|
+
name: "EntityPagerDutyCard",
|
|
180
|
+
component: {
|
|
181
|
+
lazy: () => Promise.resolve().then(function () { return index; }).then(
|
|
182
|
+
(m) => m.EntityPagerDutyCard
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
const HomePagePagerDutyCard = pagerDutyPlugin.provide(
|
|
188
|
+
createCardExtension({
|
|
189
|
+
name: "HomePagePagerDutyCard",
|
|
190
|
+
title: "PagerDuty Homepage Card",
|
|
191
|
+
components: () => import('./index-308a2487.esm.js'),
|
|
192
|
+
settings: {
|
|
193
|
+
schema: {
|
|
194
|
+
title: "PagerDuty",
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
integrationKey: {
|
|
198
|
+
title: "PagerDuty integration key",
|
|
199
|
+
type: "string"
|
|
200
|
+
},
|
|
201
|
+
serviceId: {
|
|
202
|
+
title: "PagerDuty service id",
|
|
203
|
+
type: "string"
|
|
204
|
+
},
|
|
205
|
+
name: {
|
|
206
|
+
title: "PagerDuty service name",
|
|
207
|
+
type: "string"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const useStyles$4 = makeStyles((theme) => ({
|
|
216
|
+
denseListIcon: {
|
|
217
|
+
marginRight: 0,
|
|
218
|
+
display: "flex",
|
|
219
|
+
flexDirection: "column",
|
|
220
|
+
alignItems: "center",
|
|
221
|
+
justifyContent: "center"
|
|
222
|
+
},
|
|
223
|
+
listItemPrimary: {
|
|
224
|
+
fontWeight: "bold"
|
|
225
|
+
},
|
|
226
|
+
warning: {
|
|
227
|
+
borderColor: theme.palette.status.warning,
|
|
228
|
+
color: theme.palette.status.warning,
|
|
229
|
+
"& *": {
|
|
230
|
+
color: theme.palette.status.warning
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
error: {
|
|
234
|
+
borderColor: theme.palette.status.error,
|
|
235
|
+
color: theme.palette.status.error,
|
|
236
|
+
"& *": {
|
|
237
|
+
color: theme.palette.status.error
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
const IncidentListItem = ({ incident }) => {
|
|
242
|
+
var _a, _b, _c;
|
|
243
|
+
const classes = useStyles$4();
|
|
244
|
+
const duration = (/* @__PURE__ */ new Date()).getTime() - new Date(incident.created_at).getTime();
|
|
245
|
+
const createdAt = DateTime.local().minus(Duration.fromMillis(duration)).toRelative({ locale: "en" });
|
|
246
|
+
const user = (_a = incident.assignments[0]) == null ? void 0 : _a.assignee;
|
|
247
|
+
return /* @__PURE__ */ React.createElement(ListItem, { dense: true, key: incident.id }, /* @__PURE__ */ React.createElement(
|
|
248
|
+
ListItemText,
|
|
249
|
+
{
|
|
250
|
+
primary: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
251
|
+
Chip,
|
|
252
|
+
{
|
|
253
|
+
"data-testid": `chip-${incident.status}`,
|
|
254
|
+
label: incident.status,
|
|
255
|
+
size: "small",
|
|
256
|
+
variant: "outlined",
|
|
257
|
+
icon: incident.status === "acknowledged" ? /* @__PURE__ */ React.createElement(Done, null) : /* @__PURE__ */ React.createElement(Warning, null),
|
|
258
|
+
className: incident.status === "triggered" ? classes.error : classes.warning
|
|
259
|
+
}
|
|
260
|
+
), incident.title),
|
|
261
|
+
primaryTypographyProps: {
|
|
262
|
+
variant: "body1",
|
|
263
|
+
className: classes.listItemPrimary
|
|
264
|
+
},
|
|
265
|
+
secondary: /* @__PURE__ */ React.createElement(Typography, { noWrap: true, variant: "body2", color: "textSecondary" }, "Created ", createdAt, " and assigned to", " ", /* @__PURE__ */ React.createElement(Link, { to: (_b = user == null ? void 0 : user.html_url) != null ? _b : "#" }, (_c = user == null ? void 0 : user.summary) != null ? _c : "nobody"))
|
|
266
|
+
}
|
|
267
|
+
), /* @__PURE__ */ React.createElement(ListItemSecondaryAction, null, /* @__PURE__ */ React.createElement(Tooltip, { title: "View in PagerDuty", placement: "top" }, /* @__PURE__ */ React.createElement(
|
|
268
|
+
IconButton,
|
|
269
|
+
{
|
|
270
|
+
href: incident.html_url,
|
|
271
|
+
target: "_blank",
|
|
272
|
+
rel: "noopener noreferrer",
|
|
273
|
+
color: "primary"
|
|
274
|
+
},
|
|
275
|
+
/* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
|
|
276
|
+
))));
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const IncidentsEmptyState = () => {
|
|
280
|
+
return /* @__PURE__ */ React.createElement(
|
|
281
|
+
Grid,
|
|
282
|
+
{
|
|
283
|
+
container: true,
|
|
284
|
+
justifyContent: "center",
|
|
285
|
+
direction: "column",
|
|
286
|
+
alignItems: "center"
|
|
287
|
+
},
|
|
288
|
+
/* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h5" }, "Nice! No incidents found!")),
|
|
289
|
+
/* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(
|
|
290
|
+
"img",
|
|
291
|
+
{
|
|
292
|
+
src: EmptyStateImage,
|
|
293
|
+
alt: "EmptyState",
|
|
294
|
+
"data-testid": "emptyStateImg"
|
|
295
|
+
}
|
|
296
|
+
))
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const Incidents = ({ serviceId, refreshIncidents }) => {
|
|
301
|
+
const api = useApi(pagerDutyApiRef);
|
|
302
|
+
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
|
|
303
|
+
async () => {
|
|
304
|
+
const { incidents: foundIncidents } = await api.getIncidentsByServiceId(
|
|
305
|
+
serviceId
|
|
306
|
+
);
|
|
307
|
+
return foundIncidents;
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
getIncidents();
|
|
312
|
+
}, [refreshIncidents, getIncidents]);
|
|
313
|
+
if (error) {
|
|
314
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Error encountered while fetching information. ", error.message);
|
|
315
|
+
}
|
|
316
|
+
if (loading) {
|
|
317
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
318
|
+
}
|
|
319
|
+
if (!(incidents == null ? void 0 : incidents.length)) {
|
|
320
|
+
return /* @__PURE__ */ React.createElement(IncidentsEmptyState, null);
|
|
321
|
+
}
|
|
322
|
+
return /* @__PURE__ */ React.createElement(List, { dense: true }, incidents.map((incident, index) => /* @__PURE__ */ React.createElement(IncidentListItem, { key: incident.id + index, incident })));
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const useStyles$3 = makeStyles({
|
|
326
|
+
denseListIcon: {
|
|
327
|
+
marginRight: 0,
|
|
328
|
+
display: "flex",
|
|
329
|
+
flexDirection: "column",
|
|
330
|
+
alignItems: "center",
|
|
331
|
+
justifyContent: "center"
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const EscalationUsersEmptyState = () => {
|
|
335
|
+
const classes = useStyles$3();
|
|
336
|
+
return /* @__PURE__ */ React.createElement(ListItem, null, /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement("div", { className: classes.denseListIcon }, /* @__PURE__ */ React.createElement(StatusWarning, null))), /* @__PURE__ */ React.createElement(ListItemText, { primary: "Empty escalation policy" }));
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const useStyles$2 = makeStyles({
|
|
340
|
+
listItemPrimary: {
|
|
341
|
+
fontWeight: "bold"
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
const EscalationUser = ({ user }) => {
|
|
345
|
+
const classes = useStyles$2();
|
|
346
|
+
return /* @__PURE__ */ React.createElement(ListItem, null, /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement(Avatar, { alt: "User" })), /* @__PURE__ */ React.createElement(
|
|
347
|
+
ListItemText,
|
|
348
|
+
{
|
|
349
|
+
primary: /* @__PURE__ */ React.createElement(Typography, { className: classes.listItemPrimary }, user.name),
|
|
350
|
+
secondary: user.email
|
|
351
|
+
}
|
|
352
|
+
), /* @__PURE__ */ React.createElement(ListItemSecondaryAction, null, /* @__PURE__ */ React.createElement(Tooltip, { title: "Send e-mail to user", placement: "top" }, /* @__PURE__ */ React.createElement(IconButton, { href: `mailto:${user.email}` }, /* @__PURE__ */ React.createElement(EmailIcon, { color: "primary" }))), /* @__PURE__ */ React.createElement(Tooltip, { title: "View in PagerDuty", placement: "top" }, /* @__PURE__ */ React.createElement(
|
|
353
|
+
IconButton,
|
|
354
|
+
{
|
|
355
|
+
href: user.html_url,
|
|
356
|
+
target: "_blank",
|
|
357
|
+
rel: "noopener noreferrer",
|
|
358
|
+
color: "primary"
|
|
359
|
+
},
|
|
360
|
+
/* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
|
|
361
|
+
))));
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const EscalationPolicy = ({ policyId }) => {
|
|
365
|
+
const api = useApi(pagerDutyApiRef);
|
|
366
|
+
const {
|
|
367
|
+
value: users,
|
|
368
|
+
loading,
|
|
369
|
+
error
|
|
370
|
+
} = useAsync(async () => {
|
|
371
|
+
const { oncalls } = await api.getOnCallByPolicyId(policyId);
|
|
372
|
+
const usersItem = oncalls.sort((a, b) => a.escalation_level - b.escalation_level).map((oncall) => oncall.user);
|
|
373
|
+
return usersItem;
|
|
374
|
+
});
|
|
375
|
+
if (error) {
|
|
376
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Error encountered while fetching information. ", error.message);
|
|
377
|
+
}
|
|
378
|
+
if (loading) {
|
|
379
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
380
|
+
}
|
|
381
|
+
if (!(users == null ? void 0 : users.length)) {
|
|
382
|
+
return /* @__PURE__ */ React.createElement(EscalationUsersEmptyState, null);
|
|
383
|
+
}
|
|
384
|
+
return /* @__PURE__ */ React.createElement(List, { dense: true, subheader: /* @__PURE__ */ React.createElement(ListSubheader, null, "ON CALL") }, users.map((user, index) => /* @__PURE__ */ React.createElement(EscalationUser, { key: index, user })));
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const MissingTokenError = () => /* @__PURE__ */ React.createElement(
|
|
388
|
+
EmptyState,
|
|
389
|
+
{
|
|
390
|
+
missing: "info",
|
|
391
|
+
title: "Missing or invalid PagerDuty Token",
|
|
392
|
+
description: "The request to fetch data needs a valid token. See README for more details.",
|
|
393
|
+
action: /* @__PURE__ */ React.createElement(
|
|
394
|
+
Button,
|
|
395
|
+
{
|
|
396
|
+
color: "primary",
|
|
397
|
+
variant: "contained",
|
|
398
|
+
href: "https://github.com/backstage/backstage/blob/master/plugins/pagerduty/README.md"
|
|
399
|
+
},
|
|
400
|
+
"Read More"
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const ServiceNotFoundError = () => /* @__PURE__ */ React.createElement(
|
|
406
|
+
EmptyState,
|
|
407
|
+
{
|
|
408
|
+
missing: "data",
|
|
409
|
+
title: "PagerDuty Service Not Found",
|
|
410
|
+
description: "A service could not be found within PagerDuty based on the provided service id. Please verify your configuration.",
|
|
411
|
+
action: /* @__PURE__ */ React.createElement(
|
|
412
|
+
Button,
|
|
413
|
+
{
|
|
414
|
+
color: "primary",
|
|
415
|
+
variant: "contained",
|
|
416
|
+
href: "https://github.com/backstage/backstage/blob/master/plugins/pagerduty/README.md"
|
|
417
|
+
},
|
|
418
|
+
"Read More"
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const TriggerDialog = ({
|
|
424
|
+
showDialog,
|
|
425
|
+
handleDialog,
|
|
426
|
+
onIncidentCreated,
|
|
427
|
+
name,
|
|
428
|
+
integrationKey
|
|
429
|
+
}) => {
|
|
430
|
+
const alertApi = useApi(alertApiRef);
|
|
431
|
+
const identityApi = useApi(identityApiRef);
|
|
432
|
+
const api = useApi(pagerDutyApiRef);
|
|
433
|
+
const [description, setDescription] = useState("");
|
|
434
|
+
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
|
|
435
|
+
async (descriptions) => {
|
|
436
|
+
const { userEntityRef } = await identityApi.getBackstageIdentity();
|
|
437
|
+
const { name: userName } = parseEntityRef(userEntityRef, {
|
|
438
|
+
defaultKind: "User",
|
|
439
|
+
defaultNamespace: DEFAULT_NAMESPACE
|
|
440
|
+
});
|
|
441
|
+
return await api.triggerAlarm({
|
|
442
|
+
integrationKey,
|
|
443
|
+
source: window.location.toString(),
|
|
444
|
+
description: descriptions,
|
|
445
|
+
userName
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
const descriptionChanged = (event) => {
|
|
450
|
+
setDescription(event.target.value);
|
|
451
|
+
};
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (value) {
|
|
454
|
+
(async () => {
|
|
455
|
+
alertApi.post({
|
|
456
|
+
message: `Alarm successfully triggered`
|
|
457
|
+
});
|
|
458
|
+
handleDialog();
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
460
|
+
onIncidentCreated == null ? void 0 : onIncidentCreated();
|
|
461
|
+
})();
|
|
462
|
+
}
|
|
463
|
+
}, [value, alertApi, handleDialog, onIncidentCreated]);
|
|
464
|
+
if (error) {
|
|
465
|
+
alertApi.post({
|
|
466
|
+
message: `Failed to trigger alarm. ${error.message}`,
|
|
467
|
+
severity: "error"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return /* @__PURE__ */ React.createElement(Dialog, { maxWidth: "md", open: showDialog, onClose: handleDialog, fullWidth: true }, /* @__PURE__ */ React.createElement(DialogTitle, null, "This action will trigger an incident for ", /* @__PURE__ */ React.createElement("strong", null, '"', name, '"'), "."), /* @__PURE__ */ React.createElement(DialogContent, null, /* @__PURE__ */ React.createElement(Alert, { severity: "info" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", align: "justify" }, `If the issue you are seeing does not need urgent attention, please get in touch with the responsible team using their preferred communications channel. You can find information about the owner of this entity in the "About" card. If the issue is urgent, please don't hesitate to trigger the alert.`)), /* @__PURE__ */ React.createElement(
|
|
471
|
+
Typography,
|
|
472
|
+
{
|
|
473
|
+
variant: "body1",
|
|
474
|
+
style: { marginTop: "1em" },
|
|
475
|
+
gutterBottom: true,
|
|
476
|
+
align: "justify"
|
|
477
|
+
},
|
|
478
|
+
"Please describe the problem you want to report. Be as descriptive as possible. Your signed in user and a reference to the current page will automatically be amended to the alarm so that the receiver can reach out to you if necessary."
|
|
479
|
+
), /* @__PURE__ */ React.createElement(
|
|
480
|
+
TextField,
|
|
481
|
+
{
|
|
482
|
+
inputProps: { "data-testid": "trigger-input" },
|
|
483
|
+
id: "description",
|
|
484
|
+
multiline: true,
|
|
485
|
+
fullWidth: true,
|
|
486
|
+
rows: "4",
|
|
487
|
+
margin: "normal",
|
|
488
|
+
label: "Problem description",
|
|
489
|
+
variant: "outlined",
|
|
490
|
+
onChange: descriptionChanged
|
|
491
|
+
}
|
|
492
|
+
)), /* @__PURE__ */ React.createElement(DialogActions, null, /* @__PURE__ */ React.createElement(
|
|
493
|
+
Button,
|
|
494
|
+
{
|
|
495
|
+
"data-testid": "trigger-button",
|
|
496
|
+
id: "trigger",
|
|
497
|
+
color: "secondary",
|
|
498
|
+
disabled: !description || loading,
|
|
499
|
+
variant: "contained",
|
|
500
|
+
onClick: () => handleTriggerAlarm(description),
|
|
501
|
+
endIcon: loading && /* @__PURE__ */ React.createElement(CircularProgress, { size: 16 })
|
|
502
|
+
},
|
|
503
|
+
"Trigger Incident"
|
|
504
|
+
), /* @__PURE__ */ React.createElement(Button, { id: "close", color: "primary", onClick: handleDialog }, "Close")));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const useStyles$1 = makeStyles({
|
|
508
|
+
denseListIcon: {
|
|
509
|
+
marginRight: 0,
|
|
510
|
+
display: "flex",
|
|
511
|
+
flexDirection: "column",
|
|
512
|
+
alignItems: "center",
|
|
513
|
+
justifyContent: "center"
|
|
514
|
+
},
|
|
515
|
+
listItemPrimary: {
|
|
516
|
+
fontWeight: "bold"
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
const ChangeEventListItem = ({ changeEvent }) => {
|
|
520
|
+
const classes = useStyles$1();
|
|
521
|
+
const duration = (/* @__PURE__ */ new Date()).getTime() - new Date(changeEvent.timestamp).getTime();
|
|
522
|
+
const changedAt = DateTime.local().minus(Duration.fromMillis(duration)).toRelative({ locale: "en" });
|
|
523
|
+
let externalLinkElem;
|
|
524
|
+
if (changeEvent.links.length > 0) {
|
|
525
|
+
const text = changeEvent.links[0].text;
|
|
526
|
+
externalLinkElem = /* @__PURE__ */ React.createElement(Tooltip, { title: text, placement: "top" }, /* @__PURE__ */ React.createElement(
|
|
527
|
+
IconButton,
|
|
528
|
+
{
|
|
529
|
+
component: Link,
|
|
530
|
+
to: changeEvent.links[0].href,
|
|
531
|
+
color: "primary"
|
|
532
|
+
},
|
|
533
|
+
/* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
|
|
534
|
+
));
|
|
535
|
+
}
|
|
536
|
+
return /* @__PURE__ */ React.createElement(ListItem, { dense: true, key: changeEvent.id }, /* @__PURE__ */ React.createElement(
|
|
537
|
+
ListItemText,
|
|
538
|
+
{
|
|
539
|
+
primary: /* @__PURE__ */ React.createElement(React.Fragment, null, changeEvent.summary),
|
|
540
|
+
primaryTypographyProps: {
|
|
541
|
+
variant: "body1",
|
|
542
|
+
className: classes.listItemPrimary
|
|
543
|
+
},
|
|
544
|
+
secondary: /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Triggered from ", changeEvent.source, " ", changedAt, ".")
|
|
545
|
+
}
|
|
546
|
+
), /* @__PURE__ */ React.createElement(ListItemSecondaryAction, null, externalLinkElem, changeEvent.html_url === void 0 ? null : /* @__PURE__ */ React.createElement(Tooltip, { title: "View in PagerDuty", placement: "top" }, /* @__PURE__ */ React.createElement(
|
|
547
|
+
IconButton,
|
|
548
|
+
{
|
|
549
|
+
component: Link,
|
|
550
|
+
to: changeEvent.html_url,
|
|
551
|
+
color: "primary"
|
|
552
|
+
},
|
|
553
|
+
/* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
|
|
554
|
+
))));
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const ChangeEventEmptyState = () => {
|
|
558
|
+
return /* @__PURE__ */ React.createElement(Grid, { container: true, justify: "center", direction: "column", alignItems: "center" }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h5" }, "No change events to display yet.")), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React.createElement(
|
|
559
|
+
"img",
|
|
560
|
+
{
|
|
561
|
+
src: EmptyStateImage,
|
|
562
|
+
alt: "EmptyState",
|
|
563
|
+
"data-testid": "emptyStateImg"
|
|
564
|
+
}
|
|
565
|
+
)));
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const ChangeEvents = ({ serviceId, refreshEvents }) => {
|
|
569
|
+
const api = useApi(pagerDutyApiRef);
|
|
570
|
+
const [{ value: changeEvents, loading, error }, getChangeEvents] = useAsyncFn(
|
|
571
|
+
async () => {
|
|
572
|
+
const { change_events } = await api.getChangeEventsByServiceId(serviceId);
|
|
573
|
+
return change_events;
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
getChangeEvents();
|
|
578
|
+
}, [refreshEvents, getChangeEvents]);
|
|
579
|
+
if (error) {
|
|
580
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Error encountered while fetching information. ", error.message);
|
|
581
|
+
}
|
|
582
|
+
if (loading) {
|
|
583
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
584
|
+
}
|
|
585
|
+
if (!(changeEvents == null ? void 0 : changeEvents.length)) {
|
|
586
|
+
return /* @__PURE__ */ React.createElement(ChangeEventEmptyState, null);
|
|
587
|
+
}
|
|
588
|
+
return /* @__PURE__ */ React.createElement(List, { dense: true }, changeEvents.map((changeEvent, index) => /* @__PURE__ */ React.createElement(
|
|
589
|
+
ChangeEventListItem,
|
|
590
|
+
{
|
|
591
|
+
key: changeEvent.id + index,
|
|
592
|
+
changeEvent
|
|
593
|
+
}
|
|
594
|
+
)));
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const BasicCard = ({ children }) => /* @__PURE__ */ React.createElement(InfoCard, { title: "PagerDuty" }, children);
|
|
598
|
+
const PagerDutyCard$1 = (props) => {
|
|
599
|
+
const { readOnly, disableChangeEvents, integrationKey, name } = props;
|
|
600
|
+
const api = useApi(pagerDutyApiRef);
|
|
601
|
+
const [refreshIncidents, setRefreshIncidents] = useState(false);
|
|
602
|
+
const [refreshChangeEvents, setRefreshChangeEvents] = useState(false);
|
|
603
|
+
const [dialogShown, setDialogShown] = useState(false);
|
|
604
|
+
const showDialog = useCallback(() => {
|
|
605
|
+
setDialogShown(true);
|
|
606
|
+
}, [setDialogShown]);
|
|
607
|
+
const hideDialog = useCallback(() => {
|
|
608
|
+
setDialogShown(false);
|
|
609
|
+
}, [setDialogShown]);
|
|
610
|
+
const handleRefresh = useCallback(() => {
|
|
611
|
+
setRefreshIncidents((x) => !x);
|
|
612
|
+
setRefreshChangeEvents((x) => !x);
|
|
613
|
+
}, []);
|
|
614
|
+
const {
|
|
615
|
+
value: service,
|
|
616
|
+
loading,
|
|
617
|
+
error
|
|
618
|
+
} = useAsync(async () => {
|
|
619
|
+
const { service: foundService } = await api.getServiceByPagerDutyEntity(
|
|
620
|
+
props
|
|
621
|
+
);
|
|
622
|
+
return {
|
|
623
|
+
id: foundService.id,
|
|
624
|
+
name: foundService.name,
|
|
625
|
+
url: foundService.html_url,
|
|
626
|
+
policyId: foundService.escalation_policy.id,
|
|
627
|
+
policyLink: foundService.escalation_policy.html_url
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
if (error) {
|
|
631
|
+
let errorNode;
|
|
632
|
+
switch (error.constructor) {
|
|
633
|
+
case UnauthorizedError:
|
|
634
|
+
errorNode = /* @__PURE__ */ React.createElement(MissingTokenError, null);
|
|
635
|
+
break;
|
|
636
|
+
case NotFoundError:
|
|
637
|
+
errorNode = /* @__PURE__ */ React.createElement(ServiceNotFoundError, null);
|
|
638
|
+
break;
|
|
639
|
+
default:
|
|
640
|
+
errorNode = /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Error encountered while fetching information. ", error.message);
|
|
641
|
+
}
|
|
642
|
+
return /* @__PURE__ */ React.createElement(BasicCard, null, errorNode);
|
|
643
|
+
}
|
|
644
|
+
if (loading) {
|
|
645
|
+
return /* @__PURE__ */ React.createElement(BasicCard, null, /* @__PURE__ */ React.createElement(Progress, null));
|
|
646
|
+
}
|
|
647
|
+
const serviceLink = {
|
|
648
|
+
label: "Service Directory",
|
|
649
|
+
href: service.url,
|
|
650
|
+
icon: /* @__PURE__ */ React.createElement(WebIcon, null)
|
|
651
|
+
};
|
|
652
|
+
const createIncidentDisabled = !integrationKey;
|
|
653
|
+
const triggerLink = {
|
|
654
|
+
label: "Create Incident",
|
|
655
|
+
onClick: showDialog,
|
|
656
|
+
icon: /* @__PURE__ */ React.createElement(AlarmAddIcon, null),
|
|
657
|
+
color: "secondary",
|
|
658
|
+
disabled: createIncidentDisabled,
|
|
659
|
+
title: createIncidentDisabled ? "Must provide an integration-key to create incidents" : ""
|
|
660
|
+
};
|
|
661
|
+
const escalationPolicyLink = {
|
|
662
|
+
label: "Escalation Policy",
|
|
663
|
+
href: service.policyLink,
|
|
664
|
+
icon: /* @__PURE__ */ React.createElement(DateRangeIcon, null)
|
|
665
|
+
};
|
|
666
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Card, { "data-testid": "pagerduty-card" }, /* @__PURE__ */ React.createElement(
|
|
667
|
+
CardHeader,
|
|
668
|
+
{
|
|
669
|
+
title: "PagerDuty",
|
|
670
|
+
subheader: /* @__PURE__ */ React.createElement(
|
|
671
|
+
HeaderIconLinkRow,
|
|
672
|
+
{
|
|
673
|
+
links: !readOnly ? [serviceLink, triggerLink, escalationPolicyLink] : [serviceLink, escalationPolicyLink]
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(TabbedCard, null, /* @__PURE__ */ React.createElement(CardTab, { label: "Incidents" }, /* @__PURE__ */ React.createElement(
|
|
678
|
+
Incidents,
|
|
679
|
+
{
|
|
680
|
+
serviceId: service.id,
|
|
681
|
+
refreshIncidents
|
|
682
|
+
}
|
|
683
|
+
)), disableChangeEvents !== true ? /* @__PURE__ */ React.createElement(CardTab, { label: "Change Events" }, /* @__PURE__ */ React.createElement(
|
|
684
|
+
ChangeEvents,
|
|
685
|
+
{
|
|
686
|
+
serviceId: service.id,
|
|
687
|
+
refreshEvents: refreshChangeEvents
|
|
688
|
+
}
|
|
689
|
+
)) : /* @__PURE__ */ React.createElement(React.Fragment, null)), /* @__PURE__ */ React.createElement(EscalationPolicy, { policyId: service.policyId }))), !createIncidentDisabled && /* @__PURE__ */ React.createElement(
|
|
690
|
+
TriggerDialog,
|
|
691
|
+
{
|
|
692
|
+
"data-testid": "trigger-dialog",
|
|
693
|
+
showDialog: dialogShown,
|
|
694
|
+
handleDialog: hideDialog,
|
|
695
|
+
onIncidentCreated: handleRefresh,
|
|
696
|
+
name,
|
|
697
|
+
integrationKey
|
|
698
|
+
}
|
|
699
|
+
));
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const isPluginApplicableToEntity = (entity) => {
|
|
703
|
+
var _a, _b;
|
|
704
|
+
return Boolean(
|
|
705
|
+
((_a = entity.metadata.annotations) == null ? void 0 : _a[PAGERDUTY_INTEGRATION_KEY]) || ((_b = entity.metadata.annotations) == null ? void 0 : _b[PAGERDUTY_SERVICE_ID])
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
const EntityPagerDutyCard = (props) => {
|
|
709
|
+
const { readOnly, disableChangeEvents } = props;
|
|
710
|
+
const { entity } = useEntity();
|
|
711
|
+
const pagerDutyEntity = getPagerDutyEntity(entity);
|
|
712
|
+
return /* @__PURE__ */ React.createElement(
|
|
713
|
+
PagerDutyCard$1,
|
|
714
|
+
{
|
|
715
|
+
...pagerDutyEntity,
|
|
716
|
+
readOnly,
|
|
717
|
+
disableChangeEvents
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
var index = /*#__PURE__*/Object.freeze({
|
|
723
|
+
__proto__: null,
|
|
724
|
+
isPluginApplicableToEntity: isPluginApplicableToEntity,
|
|
725
|
+
EntityPagerDutyCard: EntityPagerDutyCard
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
function usePagerdutyEntity() {
|
|
729
|
+
const { entity } = useEntity();
|
|
730
|
+
return getPagerDutyEntity(entity);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const useStyles = makeStyles((theme) => ({
|
|
734
|
+
buttonStyle: {
|
|
735
|
+
backgroundColor: theme.palette.error.main,
|
|
736
|
+
color: theme.palette.error.contrastText,
|
|
737
|
+
"&:hover": {
|
|
738
|
+
backgroundColor: theme.palette.error.dark
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}));
|
|
742
|
+
function TriggerButton(props) {
|
|
743
|
+
var _a;
|
|
744
|
+
const { buttonStyle } = useStyles();
|
|
745
|
+
const { integrationKey, name } = usePagerdutyEntity();
|
|
746
|
+
const [dialogShown, setDialogShown] = useState(false);
|
|
747
|
+
const showDialog = useCallback(() => {
|
|
748
|
+
setDialogShown(true);
|
|
749
|
+
}, [setDialogShown]);
|
|
750
|
+
const hideDialog = useCallback(() => {
|
|
751
|
+
setDialogShown(false);
|
|
752
|
+
}, [setDialogShown]);
|
|
753
|
+
const disabled = !integrationKey;
|
|
754
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
755
|
+
Button,
|
|
756
|
+
{
|
|
757
|
+
onClick: showDialog,
|
|
758
|
+
variant: "contained",
|
|
759
|
+
className: disabled ? "" : buttonStyle,
|
|
760
|
+
disabled
|
|
761
|
+
},
|
|
762
|
+
integrationKey ? (_a = props.children) != null ? _a : "Create Incident" : "Missing integration key"
|
|
763
|
+
), integrationKey && /* @__PURE__ */ React.createElement(
|
|
764
|
+
TriggerDialog,
|
|
765
|
+
{
|
|
766
|
+
showDialog: dialogShown,
|
|
767
|
+
handleDialog: hideDialog,
|
|
768
|
+
integrationKey,
|
|
769
|
+
name
|
|
770
|
+
}
|
|
771
|
+
));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const PagerDutyCard = EntityPagerDutyCard;
|
|
775
|
+
|
|
776
|
+
export { EntityPagerDutyCard$1 as E, HomePagePagerDutyCard as H, PagerDutyCard$1 as P, TriggerButton as T, UnauthorizedError as U, PagerDutyClient as a, pagerDutyApiRef as b, PagerDutyCard as c, isPluginApplicableToEntity as i, pagerDutyPlugin as p };
|
|
777
|
+
//# sourceMappingURL=index-7a206e9e.esm.js.map
|