@incident-io/backstage 0.0.9 → 0.0.10

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.
@@ -0,0 +1,98 @@
1
+ import { useApi } from '@backstage/core-plugin-api';
2
+ import { useAsync } from 'react-use';
3
+ import { I as IncidentApiRef } from './index-6eb32695.esm.js';
4
+ import { DateTime, Duration } from 'luxon';
5
+ import { makeStyles, ListItem, ListItemText, Chip, Typography, ListItemSecondaryAction, Tooltip, IconButton } from '@material-ui/core';
6
+ import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
7
+ import React from 'react';
8
+
9
+ function getBaseUrl(config) {
10
+ try {
11
+ const baseUrl = config.getString("incident.baseUrl");
12
+ if (baseUrl !== "") {
13
+ return baseUrl;
14
+ }
15
+ } catch (e) {
16
+ }
17
+ return "https://app.incident.io";
18
+ }
19
+
20
+ const useIncidentList = (query, deps) => {
21
+ const IncidentApi = useApi(IncidentApiRef);
22
+ const { value, loading, error } = useAsync(async () => {
23
+ return await IncidentApi.request({
24
+ path: `/v2/incidents?${query.toString()}`
25
+ });
26
+ }, deps);
27
+ return { loading, error, value };
28
+ };
29
+
30
+ const useStyles = makeStyles((theme) => ({
31
+ listItemPrimary: {
32
+ display: "flex",
33
+ // vertically align with chip
34
+ fontWeight: "bold"
35
+ },
36
+ warning: {
37
+ borderColor: theme.palette.status.warning,
38
+ color: theme.palette.status.warning,
39
+ "& *": {
40
+ color: theme.palette.status.warning
41
+ }
42
+ },
43
+ error: {
44
+ borderColor: theme.palette.status.error,
45
+ color: theme.palette.status.error,
46
+ "& *": {
47
+ color: theme.palette.status.error
48
+ }
49
+ }
50
+ }));
51
+ const IncidentListItem = ({
52
+ baseUrl,
53
+ incident
54
+ }) => {
55
+ var _a, _b;
56
+ const classes = useStyles();
57
+ const reportedAt = (_a = incident.incident_timestamp_values) == null ? void 0 : _a.find(
58
+ (ts) => ts.incident_timestamp.name.match(/reported/i)
59
+ );
60
+ const reportedAtDate = ((_b = reportedAt == null ? void 0 : reportedAt.value) == null ? void 0 : _b.value) || incident.created_at;
61
+ const sinceReported = (/* @__PURE__ */ new Date()).getTime() - new Date(reportedAtDate).getTime();
62
+ const sinceReportedLabel = DateTime.local().minus(Duration.fromMillis(sinceReported)).toRelative({ locale: "en" });
63
+ const lead = incident.incident_role_assignments.find((roleAssignment) => {
64
+ return roleAssignment.role.role_type === "lead";
65
+ });
66
+ return /* @__PURE__ */ React.createElement(ListItem, { dense: true, key: incident.id }, /* @__PURE__ */ React.createElement(
67
+ ListItemText,
68
+ {
69
+ primary: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
70
+ Chip,
71
+ {
72
+ "data-testid": `chip-${incident.incident_status.id}`,
73
+ label: incident.incident_status.name,
74
+ size: "small",
75
+ variant: "outlined",
76
+ className: ["live"].includes(incident.incident_status.category) ? classes.error : classes.warning
77
+ }
78
+ ), incident.reference, " ", incident.name),
79
+ primaryTypographyProps: {
80
+ variant: "body1",
81
+ className: classes.listItemPrimary
82
+ },
83
+ secondary: /* @__PURE__ */ React.createElement(Typography, { noWrap: true, variant: "body2", color: "textSecondary" }, "Reported ", sinceReportedLabel, " and", " ", (lead == null ? void 0 : lead.assignee) ? `${lead.assignee.name} is lead` : "the lead is unassigned", ".")
84
+ }
85
+ ), /* @__PURE__ */ React.createElement(ListItemSecondaryAction, null, /* @__PURE__ */ React.createElement(Tooltip, { title: "View in incident.io", placement: "top" }, /* @__PURE__ */ React.createElement(
86
+ IconButton,
87
+ {
88
+ href: `${baseUrl}/incidents/${incident.id}`,
89
+ target: "_blank",
90
+ rel: "noopener noreferrer",
91
+ color: "primary"
92
+ },
93
+ /* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
94
+ ))));
95
+ };
96
+
97
+ export { IncidentListItem as I, getBaseUrl as g, useIncidentList as u };
98
+ //# sourceMappingURL=index-40a020a8.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-40a020a8.esm.js","sources":["../../src/config.ts","../../src/hooks/useIncidentRequest.ts","../../src/components/IncidentListItem/index.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ConfigApi } from '@backstage/core-plugin-api';\n\n// Find the baseUrl of the incident dashboard.\nexport function getBaseUrl(config: ConfigApi) {\n try {\n const baseUrl = config.getString('incident.baseUrl');\n if (baseUrl !== '') {\n return baseUrl;\n }\n } catch (e) {\n // no action\n }\n\n return 'https://app.incident.io';\n}\n","import { useApi } from '@backstage/core-plugin-api';\nimport { useAsync } from 'react-use';\nimport { IncidentApiRef } from '../api/client';\nimport { definitions } from '../api/types';\nimport { DependencyList } from 'react';\n\nexport const useIncidentList = (\n query: URLSearchParams,\n deps?: DependencyList\n) => {\n const IncidentApi = useApi(IncidentApiRef);\n\n const { value, loading, error } = useAsync(async () => {\n return await IncidentApi.request<\n definitions['IncidentsV2ListResponseBody']\n >({\n path: `/v2/incidents?${query.toString()}`,\n });\n }, deps);\n\n return { loading, error, value };\n};\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { DateTime, Duration } from 'luxon';\nimport { BackstageTheme } from '@backstage/theme';\nimport {\n Chip,\n IconButton,\n ListItem,\n ListItemSecondaryAction,\n ListItemText,\n Tooltip,\n Typography,\n makeStyles,\n} from '@material-ui/core';\nimport OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';\nimport React from 'react';\nimport { definitions } from '../../api/types';\n\nconst useStyles = makeStyles<BackstageTheme>(theme => ({\n listItemPrimary: {\n display: 'flex', // vertically align with chip\n fontWeight: 'bold',\n },\n warning: {\n borderColor: theme.palette.status.warning,\n color: theme.palette.status.warning,\n '& *': {\n color: theme.palette.status.warning,\n },\n },\n error: {\n borderColor: theme.palette.status.error,\n color: theme.palette.status.error,\n '& *': {\n color: theme.palette.status.error,\n },\n },\n}));\n\n// Single item in the list of on-going incidents.\nexport const IncidentListItem = ({\n baseUrl,\n incident,\n}: {\n baseUrl: string;\n incident: definitions['IncidentV2ResponseBody'];\n}) => {\n const classes = useStyles();\n const reportedAt = incident.incident_timestamp_values?.find(ts =>\n ts.incident_timestamp.name.match(/reported/i),\n );\n\n // If reported isn't here for some reason, use created at.\n const reportedAtDate = reportedAt?.value?.value || incident.created_at;\n\n const sinceReported =\n new Date().getTime() - new Date(reportedAtDate).getTime();\n const sinceReportedLabel = DateTime.local()\n .minus(Duration.fromMillis(sinceReported))\n .toRelative({ locale: 'en' });\n const lead = incident.incident_role_assignments.find(roleAssignment => {\n return roleAssignment.role.role_type === 'lead';\n });\n\n return (\n <ListItem dense key={incident.id}>\n <ListItemText\n primary={\n <>\n <Chip\n data-testid={`chip-${incident.incident_status.id}`}\n label={incident.incident_status.name}\n size=\"small\"\n variant=\"outlined\"\n className={\n ['live'].includes(incident.incident_status.category)\n ? classes.error\n : classes.warning\n }\n />\n {incident.reference} {incident.name}\n </>\n }\n primaryTypographyProps={{\n variant: 'body1',\n className: classes.listItemPrimary,\n }}\n secondary={\n <Typography noWrap variant=\"body2\" color=\"textSecondary\">\n Reported {sinceReportedLabel} and{' '}\n {lead?.assignee\n ? `${lead.assignee.name} is lead`\n : 'the lead is unassigned'}\n .\n </Typography>\n }\n />\n <ListItemSecondaryAction>\n <Tooltip title=\"View in incident.io\" placement=\"top\">\n <IconButton\n href={`${baseUrl}/incidents/${incident.id}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n color=\"primary\"\n >\n <OpenInBrowserIcon />\n </IconButton>\n </Tooltip>\n </ListItemSecondaryAction>\n </ListItem>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { createApiRef, createPlugin, createApiFactory, discoveryApiRef, identityApiRef, createComponentExtension } from '@backstage/core-plugin-api';
2
+ import { createCardExtension } from '@backstage/plugin-home-react';
2
3
 
3
4
  var __defProp = Object.defineProperty;
4
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -60,12 +61,19 @@ const EntityIncidentCard = incidentPlugin.provide(
60
61
  createComponentExtension({
61
62
  name: "EntityIncidentCard",
62
63
  component: {
63
- lazy: () => import('./index-08133e09.esm.js').then(
64
+ lazy: () => import('./index-a76fa310.esm.js').then(
64
65
  (m) => m.EntityIncidentCard
65
66
  )
66
67
  }
67
68
  })
68
69
  );
70
+ const HomePageIncidentCard = incidentPlugin.provide(
71
+ createCardExtension({
72
+ name: "HomePageIncidentCard",
73
+ title: "Ongoing Incidents",
74
+ components: () => import('./index-ba2cdf39.esm.js')
75
+ })
76
+ );
69
77
 
70
- export { EntityIncidentCard as E, IncidentApiRef as I, incidentPlugin as i };
71
- //# sourceMappingURL=index-14b311c0.esm.js.map
78
+ export { EntityIncidentCard as E, HomePageIncidentCard as H, IncidentApiRef as I, incidentPlugin as i };
79
+ //# sourceMappingURL=index-6eb32695.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-6eb32695.esm.js","sources":["../../src/api/client.ts","../../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n DiscoveryApi,\n IdentityApi,\n createApiRef,\n} from '@backstage/core-plugin-api';\n\nexport const IncidentApiRef = createApiRef<Incident>({\n id: 'plugin.incident.service',\n});\n\ntype HTTPMethods = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE';\n\nexport interface Incident {\n request<T>({\n method,\n path,\n body,\n }: {\n method?: HTTPMethods;\n path: string;\n body?: string;\n }): Promise<T>;\n}\n\nconst DEFAULT_PROXY_PATH = '/incident/api';\n\ntype Options = {\n discoveryApi: DiscoveryApi;\n identityApi: IdentityApi;\n proxyPath?: string;\n};\n\nexport class IncidentApi implements Incident {\n private readonly discoveryApi: DiscoveryApi;\n private readonly identityApi: IdentityApi;\n private readonly proxyPath: string;\n\n constructor(opts: Options) {\n this.discoveryApi = opts.discoveryApi;\n this.identityApi = opts.identityApi;\n this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;\n }\n\n async request<T = any>({\n path,\n method = 'GET',\n body,\n }: {\n path: string;\n method?: HTTPMethods;\n body?: string;\n }): Promise<T> {\n const apiUrl =\n (await this.discoveryApi.getBaseUrl('proxy')) + this.proxyPath;\n const { token } = await this.identityApi.getCredentials();\n\n const resp = await fetch(`${apiUrl}${path}`, {\n method: method,\n body: body,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n if (!resp.ok) {\n throw new Error(`${resp.status} ${resp.statusText}`);\n }\n\n return await resp.json();\n }\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n createApiFactory,\n createComponentExtension,\n createPlugin,\n discoveryApiRef,\n identityApiRef,\n} from '@backstage/core-plugin-api';\nimport { createCardExtension } from '@backstage/plugin-home-react';\n\nimport { IncidentApi, IncidentApiRef } from './api/client';\n\nexport const incidentPlugin = createPlugin({\n id: 'incident',\n apis: [\n createApiFactory({\n api: IncidentApiRef,\n deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },\n factory: ({ discoveryApi, identityApi }) => {\n return new IncidentApi({\n discoveryApi: discoveryApi,\n identityApi: identityApi,\n });\n },\n }),\n ],\n});\n\nexport const EntityIncidentCard = incidentPlugin.provide(\n createComponentExtension({\n name: 'EntityIncidentCard',\n component: {\n lazy: () =>\n import('./components/EntityIncidentCard').then(\n (m) => m.EntityIncidentCard\n ),\n },\n })\n);\n\nexport const HomePageIncidentCard = incidentPlugin.provide(\n createCardExtension({\n name: 'HomePageIncidentCard',\n title: 'Ongoing Incidents',\n components: () => import('./components/HomePageIncidentCard'),\n })\n);\n"],"names":[],"mappings":";;;;;;;;;AAqBO,MAAM,iBAAiB,YAAuB,CAAA;AAAA,EACnD,EAAI,EAAA,yBAAA;AACN,CAAC,EAAA;AAgBD,MAAM,kBAAqB,GAAA,eAAA,CAAA;AAQpB,MAAM,WAAgC,CAAA;AAAA,EAK3C,YAAY,IAAe,EAAA;AAJ3B,IAAiB,aAAA,CAAA,IAAA,EAAA,cAAA,CAAA,CAAA;AACjB,IAAiB,aAAA,CAAA,IAAA,EAAA,aAAA,CAAA,CAAA;AACjB,IAAiB,aAAA,CAAA,IAAA,EAAA,WAAA,CAAA,CAAA;AAlDnB,IAAA,IAAA,EAAA,CAAA;AAqDI,IAAA,IAAA,CAAK,eAAe,IAAK,CAAA,YAAA,CAAA;AACzB,IAAA,IAAA,CAAK,cAAc,IAAK,CAAA,WAAA,CAAA;AACxB,IAAK,IAAA,CAAA,SAAA,GAAA,CAAY,EAAK,GAAA,IAAA,CAAA,SAAA,KAAL,IAAkB,GAAA,EAAA,GAAA,kBAAA,CAAA;AAAA,GACrC;AAAA,EAEA,MAAM,OAAiB,CAAA;AAAA,IACrB,IAAA;AAAA,IACA,MAAS,GAAA,KAAA;AAAA,IACT,IAAA;AAAA,GAKa,EAAA;AACb,IAAA,MAAM,SACH,MAAM,IAAA,CAAK,aAAa,UAAW,CAAA,OAAO,IAAK,IAAK,CAAA,SAAA,CAAA;AACvD,IAAA,MAAM,EAAE,KAAM,EAAA,GAAI,MAAM,IAAA,CAAK,YAAY,cAAe,EAAA,CAAA;AAExD,IAAA,MAAM,OAAO,MAAM,KAAA,CAAM,GAAG,MAAM,CAAA,EAAG,IAAI,CAAI,CAAA,EAAA;AAAA,MAC3C,MAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAS,EAAA;AAAA,QACP,aAAA,EAAe,UAAU,KAAK,CAAA,CAAA;AAAA,OAChC;AAAA,KACD,CAAA,CAAA;AACD,IAAI,IAAA,CAAC,KAAK,EAAI,EAAA;AACZ,MAAM,MAAA,IAAI,MAAM,CAAG,EAAA,IAAA,CAAK,MAAM,CAAI,CAAA,EAAA,IAAA,CAAK,UAAU,CAAE,CAAA,CAAA,CAAA;AAAA,KACrD;AAEA,IAAO,OAAA,MAAM,KAAK,IAAK,EAAA,CAAA;AAAA,GACzB;AACF;;AC1DO,MAAM,iBAAiB,YAAa,CAAA;AAAA,EACzC,EAAI,EAAA,UAAA;AAAA,EACJ,IAAM,EAAA;AAAA,IACJ,gBAAiB,CAAA;AAAA,MACf,GAAK,EAAA,cAAA;AAAA,MACL,IAAM,EAAA,EAAE,YAAc,EAAA,eAAA,EAAiB,aAAa,cAAe,EAAA;AAAA,MACnE,OAAS,EAAA,CAAC,EAAE,YAAA,EAAc,aAAkB,KAAA;AAC1C,QAAA,OAAO,IAAI,WAAY,CAAA;AAAA,UACrB,YAAA;AAAA,UACA,WAAA;AAAA,SACD,CAAA,CAAA;AAAA,OACH;AAAA,KACD,CAAA;AAAA,GACH;AACF,CAAC,EAAA;AAEM,MAAM,qBAAqB,cAAe,CAAA,OAAA;AAAA,EAC/C,wBAAyB,CAAA;AAAA,IACvB,IAAM,EAAA,oBAAA;AAAA,IACN,SAAW,EAAA;AAAA,MACT,IAAM,EAAA,MACJ,OAAO,yBAAiC,CAAE,CAAA,IAAA;AAAA,QACxC,CAAC,MAAM,CAAE,CAAA,kBAAA;AAAA,OACX;AAAA,KACJ;AAAA,GACD,CAAA;AACH,EAAA;AAEO,MAAM,uBAAuB,cAAe,CAAA,OAAA;AAAA,EACjD,mBAAoB,CAAA;AAAA,IAClB,IAAM,EAAA,sBAAA;AAAA,IACN,KAAO,EAAA,mBAAA;AAAA,IACP,UAAA,EAAY,MAAM,OAAO,yBAAmC,CAAA;AAAA,GAC7D,CAAA;AACH;;;;"}
@@ -1,96 +1,23 @@
1
1
  import { HeaderIconLinkRow, Progress } from '@backstage/core-components';
2
2
  import { useApi, configApiRef } from '@backstage/core-plugin-api';
3
3
  import { useEntity } from '@backstage/plugin-catalog-react';
4
- import { makeStyles, ListItem, ListItemText, Chip, Typography, ListItemSecondaryAction, Tooltip, IconButton, Card, CardHeader, Divider, CardContent, List } from '@material-ui/core';
4
+ import { Card, CardHeader, IconButton, Divider, CardContent, Typography, List } from '@material-ui/core';
5
5
  import Link from '@material-ui/core/Link';
6
6
  import CachedIcon from '@material-ui/icons/Cached';
7
7
  import HistoryIcon from '@material-ui/icons/History';
8
8
  import WhatshotIcon from '@material-ui/icons/Whatshot';
9
9
  import { Alert } from '@material-ui/lab';
10
10
  import React, { useState } from 'react';
11
- import { useAsync } from 'react-use';
12
- import { I as IncidentApiRef } from './index-14b311c0.esm.js';
13
- import { DateTime, Duration } from 'luxon';
14
- import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
11
+ import { g as getBaseUrl, u as useIncidentList, I as IncidentListItem } from './index-40a020a8.esm.js';
12
+ import 'react-use';
13
+ import './index-6eb32695.esm.js';
14
+ import '@backstage/plugin-home-react';
15
+ import 'luxon';
16
+ import '@material-ui/icons/OpenInBrowser';
15
17
 
16
- function getBaseUrl(config) {
17
- try {
18
- const baseUrl = config.getString("incident.baseUrl");
19
- if (baseUrl !== "") {
20
- return baseUrl;
21
- }
22
- } catch (e) {
23
- }
24
- return "https://app.incident.io";
25
- }
26
-
27
- const useStyles = makeStyles((theme) => ({
28
- listItemPrimary: {
29
- display: "flex",
30
- // vertically align with chip
31
- fontWeight: "bold"
32
- },
33
- warning: {
34
- borderColor: theme.palette.status.warning,
35
- color: theme.palette.status.warning,
36
- "& *": {
37
- color: theme.palette.status.warning
38
- }
39
- },
40
- error: {
41
- borderColor: theme.palette.status.error,
42
- color: theme.palette.status.error,
43
- "& *": {
44
- color: theme.palette.status.error
45
- }
46
- }
47
- }));
48
- const IncidentListItem = ({
49
- baseUrl,
50
- incident
51
- }) => {
52
- var _a, _b;
53
- const classes = useStyles();
54
- const reportedAt = (_a = incident.incident_timestamp_values) == null ? void 0 : _a.find(
55
- (ts) => ts.incident_timestamp.name.match(/reported/i)
56
- );
57
- const reportedAtDate = ((_b = reportedAt == null ? void 0 : reportedAt.value) == null ? void 0 : _b.value) || incident.created_at;
58
- const sinceReported = (/* @__PURE__ */ new Date()).getTime() - new Date(reportedAtDate).getTime();
59
- const sinceReportedLabel = DateTime.local().minus(Duration.fromMillis(sinceReported)).toRelative({ locale: "en" });
60
- const lead = incident.incident_role_assignments.find((roleAssignment) => {
61
- return roleAssignment.role.role_type === "lead";
62
- });
63
- return /* @__PURE__ */ React.createElement(ListItem, { dense: true, key: incident.id }, /* @__PURE__ */ React.createElement(
64
- ListItemText,
65
- {
66
- primary: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
67
- Chip,
68
- {
69
- "data-testid": `chip-${incident.incident_status.id}`,
70
- label: incident.incident_status.name,
71
- size: "small",
72
- variant: "outlined",
73
- className: ["live"].includes(incident.incident_status.category) ? classes.error : classes.warning
74
- }
75
- ), incident.reference, " ", incident.name),
76
- primaryTypographyProps: {
77
- variant: "body1",
78
- className: classes.listItemPrimary
79
- },
80
- secondary: /* @__PURE__ */ React.createElement(Typography, { noWrap: true, variant: "body2", color: "textSecondary" }, "Reported ", sinceReportedLabel, " and", " ", (lead == null ? void 0 : lead.assignee) ? `${lead.assignee.name} is lead` : "the lead is unassigned", ".")
81
- }
82
- ), /* @__PURE__ */ React.createElement(ListItemSecondaryAction, null, /* @__PURE__ */ React.createElement(Tooltip, { title: "View in incident.io", placement: "top" }, /* @__PURE__ */ React.createElement(
83
- IconButton,
84
- {
85
- href: `${baseUrl}/incidents/${incident.id}`,
86
- target: "_blank",
87
- rel: "noopener noreferrer",
88
- color: "primary"
89
- },
90
- /* @__PURE__ */ React.createElement(OpenInBrowserIcon, null)
91
- ))));
18
+ const IncorrectConfigCard = () => {
19
+ return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "incident.io" }), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "No custom field configuration was found. In order to display incidents, this entity must be mapped to an incident.io custom field ID in Backstage's app-config.yaml.")));
92
20
  };
93
-
94
21
  const EntityIncidentCard = ({
95
22
  maxIncidents = 2
96
23
  }) => {
@@ -98,12 +25,8 @@ const EntityIncidentCard = ({
98
25
  const config = useApi(configApiRef);
99
26
  const baseUrl = getBaseUrl(config);
100
27
  const { entity } = useEntity();
101
- const IncidentApi = useApi(IncidentApiRef);
102
28
  const [reload, setReload] = useState(false);
103
29
  const entityFieldID = getEntityFieldID(config, entity);
104
- if (!entityFieldID) {
105
- return /* @__PURE__ */ React.createElement(IncorrectConfigCard, null);
106
- }
107
30
  const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;
108
31
  const query = new URLSearchParams();
109
32
  query.set(`custom_field[${entityFieldID}][one_of]`, entityID);
@@ -125,12 +48,11 @@ const EntityIncidentCard = ({
125
48
  value: incidentsResponse,
126
49
  loading: incidentsLoading,
127
50
  error: incidentsError
128
- } = useAsync(async () => {
129
- return await IncidentApi.request({
130
- path: `/v2/incidents?${queryLive.toString()}`
131
- });
132
- }, [reload]);
51
+ } = useIncidentList(queryLive, [reload]);
133
52
  const incidents = incidentsResponse == null ? void 0 : incidentsResponse.incidents;
53
+ if (!entityFieldID) {
54
+ return /* @__PURE__ */ React.createElement(IncorrectConfigCard, null);
55
+ }
134
56
  return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(
135
57
  CardHeader,
136
58
  {
@@ -166,9 +88,6 @@ const EntityIncidentCard = ({
166
88
  "see more."
167
89
  )))));
168
90
  };
169
- const IncorrectConfigCard = () => {
170
- return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "incident.io" }), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "No custom field configuration was found. In order to display incidents, this entity must be mapped to an incident.io custom field ID in Backstage's app-config.yaml.")));
171
- };
172
91
  function getEntityFieldID(config, entity) {
173
92
  switch (entity.kind) {
174
93
  case "API":
@@ -185,4 +104,4 @@ function getEntityFieldID(config, entity) {
185
104
  }
186
105
 
187
106
  export { EntityIncidentCard };
188
- //# sourceMappingURL=index-08133e09.esm.js.map
107
+ //# sourceMappingURL=index-a76fa310.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-a76fa310.esm.js","sources":["../../src/components/EntityIncidentCard/index.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Entity } from '@backstage/catalog-model';\nimport {\n HeaderIconLinkRow,\n IconLinkVerticalProps,\n Progress,\n} from '@backstage/core-components';\nimport { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { useEntity } from '@backstage/plugin-catalog-react';\nimport {\n Card,\n CardContent,\n CardHeader,\n Divider,\n IconButton,\n List,\n Typography,\n} from '@material-ui/core';\nimport Link from '@material-ui/core/Link';\nimport CachedIcon from '@material-ui/icons/Cached';\nimport HistoryIcon from '@material-ui/icons/History';\nimport WhatshotIcon from '@material-ui/icons/Whatshot';\nimport { Alert } from '@material-ui/lab';\nimport React, { useState } from 'react';\nimport { getBaseUrl } from '../../config';\nimport { useIncidentList } from '../../hooks/useIncidentRequest';\nimport { IncidentListItem } from '../IncidentListItem';\n\nconst IncorrectConfigCard = () => {\n return (\n <Card>\n <CardHeader title=\"incident.io\" />\n <Divider />\n <CardContent>\n <Typography variant=\"subtitle1\">\n No custom field configuration was found. In order to display\n incidents, this entity must be mapped to an incident.io custom field\n ID in Backstage's app-config.yaml.\n </Typography>\n </CardContent>\n </Card>\n );\n};\n\n// The card displayed on the entity page showing a handful of the most recent\n// incidents that are on-going for that component.\nexport const EntityIncidentCard = ({\n maxIncidents = 2,\n}: {\n maxIncidents?: number;\n}) => {\n const config = useApi(configApiRef);\n const baseUrl = getBaseUrl(config);\n const { entity } = useEntity();\n\n const [reload, setReload] = useState(false);\n\n const entityFieldID = getEntityFieldID(config, entity);\n const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;\n\n // This query filters incidents for those that are associated with this\n // entity.\n const query = new URLSearchParams();\n query.set(`custom_field[${entityFieldID}][one_of]`, entityID);\n\n // This restricts the previous filter to focus only on live incidents.\n const queryLive = new URLSearchParams(query);\n queryLive.set(`status_category[one_of]`, 'live');\n\n const createIncidentLink: IconLinkVerticalProps = {\n label: 'Create incident',\n disabled: false,\n icon: <WhatshotIcon />,\n href: `${baseUrl}/incidents/create`,\n };\n\n const viewIncidentsLink: IconLinkVerticalProps = {\n label: 'View past incidents',\n disabled: false,\n icon: <HistoryIcon />,\n href: `${baseUrl}/incidents?${query.toString()}`,\n };\n\n const {\n value: incidentsResponse,\n loading: incidentsLoading,\n error: incidentsError,\n } = useIncidentList(queryLive, [reload]);\n\n const incidents = incidentsResponse?.incidents;\n\n if (!entityFieldID) {\n return <IncorrectConfigCard />;\n }\n\n return (\n <Card>\n <CardHeader\n title=\"incident.io\"\n action={\n <>\n <IconButton\n component={Link}\n aria-label=\"Refresh\"\n disabled={false}\n title=\"Refresh\"\n onClick={() => setReload(!reload)}\n >\n <CachedIcon />\n </IconButton>\n </>\n }\n subheader={\n <HeaderIconLinkRow links={[createIncidentLink, viewIncidentsLink]} />\n }\n />\n <Divider />\n <CardContent>\n {incidentsLoading && <Progress />}\n {incidentsError && (\n <Alert severity=\"error\">{incidentsError.message}</Alert>\n )}\n {!incidentsLoading && !incidentsError && incidents && (\n <>\n {incidents && incidents.length > 0 && (\n <Typography variant=\"subtitle1\">\n There are <strong>{incidents.length}</strong> ongoing incidents\n involving <strong>{entity.metadata.name}</strong>.\n </Typography>\n )}\n {incidents && incidents.length === 0 && (\n <Typography variant=\"subtitle1\">No ongoing incidents.</Typography>\n )}\n <List dense>\n {incidents?.slice(0, maxIncidents)?.map((incident) => {\n return (\n <IncidentListItem\n key={incident.id}\n incident={incident}\n baseUrl={baseUrl}\n />\n );\n })}\n </List>\n <Typography variant=\"subtitle1\">\n Click to{' '}\n <Link\n target=\"_blank\"\n href={`${baseUrl}/incidents?${queryLive.toString()}`}\n >\n see more.\n </Link>\n </Typography>\n </>\n )}\n </CardContent>\n </Card>\n );\n};\n\n// Find the ID of the custom field in incident that represents the association\n// to this type of entity.\n//\n// In practice, this will be kind=Component => ID of Affected components field.\nfunction getEntityFieldID(config: ConfigApi, entity: Entity) {\n switch (entity.kind) {\n case 'API':\n return config.getOptional('incident.fields.api');\n case 'Component':\n return config.getOptional('incident.fields.component');\n case 'Domain':\n return config.getOptional('incident.fields.domain');\n case 'System':\n return config.getOptional('incident.fields.system');\n default:\n throw new Error(`unrecognised entity kind: ${entity.kind}`);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { Progress } from '@backstage/core-components';
2
+ import Link from '@material-ui/core/Link';
3
+ import { Alert } from '@material-ui/lab';
4
+ import React from 'react';
5
+ import { g as getBaseUrl, u as useIncidentList, I as IncidentListItem } from './index-40a020a8.esm.js';
6
+ import { Typography, List } from '@material-ui/core';
7
+ import { useApi, configApiRef } from '@backstage/core-plugin-api';
8
+ import 'react-use';
9
+ import './index-6eb32695.esm.js';
10
+ import '@backstage/plugin-home-react';
11
+ import 'luxon';
12
+ import '@material-ui/icons/OpenInBrowser';
13
+
14
+ const HomePageIncidentCardContent = () => {
15
+ const config = useApi(configApiRef);
16
+ const baseUrl = getBaseUrl(config);
17
+ const query = new URLSearchParams();
18
+ query.set(`status_category[one_of]`, "live");
19
+ const { loading, error, value } = useIncidentList(query);
20
+ const incidents = value == null ? void 0 : value.incidents;
21
+ if (loading)
22
+ return /* @__PURE__ */ React.createElement(Progress, null);
23
+ if (error)
24
+ return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error.message);
25
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, incidents && incidents.length > 0 && /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "There are ", /* @__PURE__ */ React.createElement("strong", null, incidents.length), " ongoing incidents."), incidents && incidents.length === 0 && /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "No ongoing incidents."), /* @__PURE__ */ React.createElement(List, { dense: true }, incidents == null ? void 0 : incidents.map((incident) => {
26
+ return /* @__PURE__ */ React.createElement(
27
+ IncidentListItem,
28
+ {
29
+ key: incident.id,
30
+ incident,
31
+ baseUrl
32
+ }
33
+ );
34
+ })), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "Click to", " ", /* @__PURE__ */ React.createElement(Link, { target: "_blank", href: `${baseUrl}/incidents?${query.toString()}` }, "see more.")));
35
+ };
36
+ const Content = () => {
37
+ return /* @__PURE__ */ React.createElement(HomePageIncidentCardContent, null);
38
+ };
39
+
40
+ export { Content };
41
+ //# sourceMappingURL=index-ba2cdf39.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-ba2cdf39.esm.js","sources":["../../src/components/HomePageIncidentCard/Content.tsx"],"sourcesContent":["import { Progress } from '@backstage/core-components';\nimport Link from '@material-ui/core/Link';\nimport { Alert } from '@material-ui/lab';\nimport React from 'react';\nimport { useIncidentList } from '../../hooks/useIncidentRequest';\nimport { Typography, List } from '@material-ui/core';\nimport { IncidentListItem } from '../IncidentListItem';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport { getBaseUrl } from '../../config';\n\nexport const HomePageIncidentCardContent = () => {\n const config = useApi(configApiRef);\n const baseUrl = getBaseUrl(config);\n\n const query = new URLSearchParams();\n query.set(`status_category[one_of]`, 'live');\n const { loading, error, value } = useIncidentList(query);\n const incidents = value?.incidents;\n\n if (loading) return <Progress />;\n if (error) return <Alert severity=\"error\">{error.message}</Alert>;\n\n return (\n <>\n {incidents && incidents.length > 0 && (\n <Typography variant=\"subtitle1\">\n There are <strong>{incidents.length}</strong> ongoing incidents.\n </Typography>\n )}\n {incidents && incidents.length === 0 && (\n <Typography variant=\"subtitle1\">No ongoing incidents.</Typography>\n )}\n <List dense>\n {incidents?.map((incident) => {\n return (\n <IncidentListItem\n key={incident.id}\n incident={incident}\n baseUrl={baseUrl}\n />\n );\n })}\n </List>\n <Typography variant=\"subtitle1\">\n Click to{' '}\n <Link target=\"_blank\" href={`${baseUrl}/incidents?${query.toString()}`}>\n see more.\n </Link>\n </Typography>\n </>\n );\n};\n\nexport const Content = () => {\n return <HomePageIncidentCardContent />;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;AAUO,MAAM,8BAA8B,MAAM;AAC/C,EAAM,MAAA,MAAA,GAAS,OAAO,YAAY,CAAA,CAAA;AAClC,EAAM,MAAA,OAAA,GAAU,WAAW,MAAM,CAAA,CAAA;AAEjC,EAAM,MAAA,KAAA,GAAQ,IAAI,eAAgB,EAAA,CAAA;AAClC,EAAM,KAAA,CAAA,GAAA,CAAI,2BAA2B,MAAM,CAAA,CAAA;AAC3C,EAAA,MAAM,EAAE,OAAS,EAAA,KAAA,EAAO,KAAM,EAAA,GAAI,gBAAgB,KAAK,CAAA,CAAA;AACvD,EAAA,MAAM,YAAY,KAAO,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,KAAA,CAAA,SAAA,CAAA;AAEzB,EAAI,IAAA,OAAA;AAAS,IAAA,2CAAQ,QAAS,EAAA,IAAA,CAAA,CAAA;AAC9B,EAAI,IAAA,KAAA;AAAO,IAAA,uBAAQ,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EAAM,QAAS,EAAA,OAAA,EAAA,EAAS,MAAM,OAAQ,CAAA,CAAA;AAEzD,EAAA,uBAEK,KAAA,CAAA,aAAA,CAAA,KAAA,CAAA,QAAA,EAAA,IAAA,EAAA,SAAA,IAAa,SAAU,CAAA,MAAA,GAAS,qBAC9B,KAAA,CAAA,aAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,WAAA,EAAA,EAAY,8BACnB,KAAA,CAAA,aAAA,CAAA,QAAA,EAAA,IAAA,EAAQ,SAAU,CAAA,MAAO,GAAS,qBAC/C,CAAA,EAED,SAAa,IAAA,SAAA,CAAU,MAAW,KAAA,CAAA,oBAChC,KAAA,CAAA,aAAA,CAAA,UAAA,EAAA,EAAW,SAAQ,WAAY,EAAA,EAAA,uBAAqB,CAEvD,kBAAA,KAAA,CAAA,aAAA,CAAC,QAAK,KAAK,EAAA,IAAA,EAAA,EACR,SAAW,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,SAAA,CAAA,GAAA,CAAI,CAAC,QAAa,KAAA;AAC5B,IACE,uBAAA,KAAA,CAAA,aAAA;AAAA,MAAC,gBAAA;AAAA,MAAA;AAAA,QACC,KAAK,QAAS,CAAA,EAAA;AAAA,QACd,QAAA;AAAA,QACA,OAAA;AAAA,OAAA;AAAA,KACF,CAAA;AAAA,GAEJ,CACF,mBACC,KAAA,CAAA,aAAA,CAAA,UAAA,EAAA,EAAW,SAAQ,WAAY,EAAA,EAAA,UAAA,EACrB,GACT,kBAAA,KAAA,CAAA,aAAA,CAAC,IAAK,EAAA,EAAA,MAAA,EAAO,UAAS,IAAM,EAAA,CAAA,EAAG,OAAO,CAAc,WAAA,EAAA,KAAA,CAAM,UAAU,CAAA,CAAA,EAAA,EAAI,WAExE,CACF,CACF,CAAA,CAAA;AAEJ,CAAA,CAAA;AAEO,MAAM,UAAU,MAAM;AAC3B,EAAA,2CAAQ,2BAA4B,EAAA,IAAA,CAAA,CAAA;AACtC;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="react" />
2
+ import * as _backstage_plugin_home_react from '@backstage/plugin-home-react';
2
3
  import * as react from 'react';
3
4
  import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
4
5
 
@@ -6,5 +7,6 @@ declare const incidentPlugin: _backstage_core_plugin_api.BackstagePlugin<{}, {},
6
7
  declare const EntityIncidentCard: ({ maxIncidents, }: {
7
8
  maxIncidents?: number | undefined;
8
9
  }) => react.JSX.Element;
10
+ declare const HomePageIncidentCard: (props: _backstage_plugin_home_react.CardExtensionProps<unknown>) => react.JSX.Element;
9
11
 
10
- export { EntityIncidentCard, incidentPlugin };
12
+ export { EntityIncidentCard, HomePageIncidentCard, incidentPlugin };
package/dist/index.esm.js CHANGED
@@ -1,3 +1,4 @@
1
- export { E as EntityIncidentCard, i as incidentPlugin } from './esm/index-14b311c0.esm.js';
1
+ export { E as EntityIncidentCard, H as HomePageIncidentCard, i as incidentPlugin } from './esm/index-6eb32695.esm.js';
2
2
  import '@backstage/core-plugin-api';
3
+ import '@backstage/plugin-home-react';
3
4
  //# sourceMappingURL=index.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incident-io/backstage",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "main": "dist/index.esm.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "Apache-2.0",
@@ -23,14 +23,15 @@
23
23
  "postpack": "backstage-cli package postpack"
24
24
  },
25
25
  "dependencies": {
26
- "@backstage/backend-tasks": "^0.5.3",
27
- "@backstage/catalog-model": "^1.4.0",
28
- "@backstage/core-components": "^0.13.2",
29
- "@backstage/core-plugin-api": "^1.5.2",
30
- "@backstage/errors": "^1.2.0",
31
- "@backstage/plugin-catalog-react": "^1.7.0",
32
- "@backstage/plugin-search-react": "^1.6.2",
33
- "@backstage/theme": "^0.4.0",
26
+ "@backstage/backend-tasks": "^0.5.11",
27
+ "@backstage/catalog-model": "^1.4.3",
28
+ "@backstage/core-components": "^0.13.6",
29
+ "@backstage/core-plugin-api": "^1.7.0",
30
+ "@backstage/errors": "^1.2.3",
31
+ "@backstage/plugin-catalog-react": "^1.8.5",
32
+ "@backstage/plugin-home-react": "^0.1.4",
33
+ "@backstage/plugin-search-react": "^1.7.1",
34
+ "@backstage/theme": "^0.4.3",
34
35
  "@material-ui/core": "^4.9.13",
35
36
  "@material-ui/icons": "^4.9.1",
36
37
  "@material-ui/lab": "^4.0.0-alpha.60",
@@ -46,16 +47,19 @@
46
47
  "react-router-dom": "^6.0.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@backstage/cli": "^0.22.8",
50
- "@backstage/core-app-api": "^1.8.1",
51
- "@backstage/dev-utils": "^1.0.16",
52
- "@backstage/test-utils": "^1.4.0",
50
+ "@backstage/cli": "^0.23.1",
51
+ "@backstage/core-app-api": "^1.11.0",
52
+ "@backstage/dev-utils": "^1.0.22",
53
+ "@backstage/test-utils": "^1.4.4",
53
54
  "@testing-library/jest-dom": "^5.10.1",
54
55
  "@testing-library/react": "^12.1.3",
55
56
  "@testing-library/user-event": "^14.0.0",
56
57
  "@types/node": "*",
57
58
  "cross-fetch": "^3.1.5",
58
- "msw": "^1.0.0"
59
+ "msw": "^1.0.0",
60
+ "react": "^17.0.0",
61
+ "react-dom": "^17.0.0",
62
+ "react-router-dom": "^6.0.0"
59
63
  },
60
64
  "files": [
61
65
  "config.d.ts",
@@ -13,14 +13,14 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { Entity } from "@backstage/catalog-model";
16
+ import { Entity } from '@backstage/catalog-model';
17
17
  import {
18
18
  HeaderIconLinkRow,
19
19
  IconLinkVerticalProps,
20
20
  Progress,
21
- } from "@backstage/core-components";
22
- import { ConfigApi, configApiRef, useApi } from "@backstage/core-plugin-api";
23
- import { useEntity } from "@backstage/plugin-catalog-react";
21
+ } from '@backstage/core-components';
22
+ import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
23
+ import { useEntity } from '@backstage/plugin-catalog-react';
24
24
  import {
25
25
  Card,
26
26
  CardContent,
@@ -29,18 +29,32 @@ import {
29
29
  IconButton,
30
30
  List,
31
31
  Typography,
32
- } from "@material-ui/core";
33
- import Link from "@material-ui/core/Link";
34
- import CachedIcon from "@material-ui/icons/Cached";
35
- import HistoryIcon from "@material-ui/icons/History";
36
- import WhatshotIcon from "@material-ui/icons/Whatshot";
37
- import { Alert } from "@material-ui/lab";
38
- import React, { useState } from "react";
39
- import { useAsync } from "react-use";
40
- import { IncidentApiRef } from "../../api/client";
41
- import { definitions } from "../../api/types";
42
- import { getBaseUrl } from "../../config";
43
- import { IncidentListItem } from "../IncidentListItem";
32
+ } from '@material-ui/core';
33
+ import Link from '@material-ui/core/Link';
34
+ import CachedIcon from '@material-ui/icons/Cached';
35
+ import HistoryIcon from '@material-ui/icons/History';
36
+ import WhatshotIcon from '@material-ui/icons/Whatshot';
37
+ import { Alert } from '@material-ui/lab';
38
+ import React, { useState } from 'react';
39
+ import { getBaseUrl } from '../../config';
40
+ import { useIncidentList } from '../../hooks/useIncidentRequest';
41
+ import { IncidentListItem } from '../IncidentListItem';
42
+
43
+ const IncorrectConfigCard = () => {
44
+ return (
45
+ <Card>
46
+ <CardHeader title="incident.io" />
47
+ <Divider />
48
+ <CardContent>
49
+ <Typography variant="subtitle1">
50
+ No custom field configuration was found. In order to display
51
+ incidents, this entity must be mapped to an incident.io custom field
52
+ ID in Backstage's app-config.yaml.
53
+ </Typography>
54
+ </CardContent>
55
+ </Card>
56
+ );
57
+ };
44
58
 
45
59
  // The card displayed on the entity page showing a handful of the most recent
46
60
  // incidents that are on-going for that component.
@@ -53,16 +67,9 @@ export const EntityIncidentCard = ({
53
67
  const baseUrl = getBaseUrl(config);
54
68
  const { entity } = useEntity();
55
69
 
56
- const IncidentApi = useApi(IncidentApiRef);
57
-
58
70
  const [reload, setReload] = useState(false);
59
71
 
60
72
  const entityFieldID = getEntityFieldID(config, entity);
61
-
62
- if (!entityFieldID) {
63
- return <IncorrectConfigCard />;
64
- }
65
-
66
73
  const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;
67
74
 
68
75
  // This query filters incidents for those that are associated with this
@@ -72,17 +79,17 @@ export const EntityIncidentCard = ({
72
79
 
73
80
  // This restricts the previous filter to focus only on live incidents.
74
81
  const queryLive = new URLSearchParams(query);
75
- queryLive.set(`status_category[one_of]`, "live");
82
+ queryLive.set(`status_category[one_of]`, 'live');
76
83
 
77
84
  const createIncidentLink: IconLinkVerticalProps = {
78
- label: "Create incident",
85
+ label: 'Create incident',
79
86
  disabled: false,
80
87
  icon: <WhatshotIcon />,
81
88
  href: `${baseUrl}/incidents/create`,
82
89
  };
83
90
 
84
91
  const viewIncidentsLink: IconLinkVerticalProps = {
85
- label: "View past incidents",
92
+ label: 'View past incidents',
86
93
  disabled: false,
87
94
  icon: <HistoryIcon />,
88
95
  href: `${baseUrl}/incidents?${query.toString()}`,
@@ -92,16 +99,14 @@ export const EntityIncidentCard = ({
92
99
  value: incidentsResponse,
93
100
  loading: incidentsLoading,
94
101
  error: incidentsError,
95
- } = useAsync(async () => {
96
- return await IncidentApi.request<
97
- definitions["IncidentsV2ListResponseBody"]
98
- >({
99
- path: `/v2/incidents?${queryLive.toString()}`,
100
- });
101
- }, [reload]);
102
+ } = useIncidentList(queryLive, [reload]);
102
103
 
103
104
  const incidents = incidentsResponse?.incidents;
104
105
 
106
+ if (!entityFieldID) {
107
+ return <IncorrectConfigCard />;
108
+ }
109
+
105
110
  return (
106
111
  <Card>
107
112
  <CardHeader
@@ -152,7 +157,7 @@ export const EntityIncidentCard = ({
152
157
  })}
153
158
  </List>
154
159
  <Typography variant="subtitle1">
155
- Click to{" "}
160
+ Click to{' '}
156
161
  <Link
157
162
  target="_blank"
158
163
  href={`${baseUrl}/incidents?${queryLive.toString()}`}
@@ -167,36 +172,20 @@ export const EntityIncidentCard = ({
167
172
  );
168
173
  };
169
174
 
170
- const IncorrectConfigCard = () => {
171
- return (
172
- <Card>
173
- <CardHeader title="incident.io" />
174
- <Divider />
175
- <CardContent>
176
- <Typography variant="subtitle1">
177
- No custom field configuration was found. In order to display
178
- incidents, this entity must be mapped to an incident.io custom field
179
- ID in Backstage's app-config.yaml.
180
- </Typography>
181
- </CardContent>
182
- </Card>
183
- );
184
- };
185
-
186
175
  // Find the ID of the custom field in incident that represents the association
187
176
  // to this type of entity.
188
177
  //
189
178
  // In practice, this will be kind=Component => ID of Affected components field.
190
179
  function getEntityFieldID(config: ConfigApi, entity: Entity) {
191
180
  switch (entity.kind) {
192
- case "API":
193
- return config.getOptional("incident.fields.api");
194
- case "Component":
195
- return config.getOptional("incident.fields.component");
196
- case "Domain":
197
- return config.getOptional("incident.fields.domain");
198
- case "System":
199
- return config.getOptional("incident.fields.system");
181
+ case 'API':
182
+ return config.getOptional('incident.fields.api');
183
+ case 'Component':
184
+ return config.getOptional('incident.fields.component');
185
+ case 'Domain':
186
+ return config.getOptional('incident.fields.domain');
187
+ case 'System':
188
+ return config.getOptional('incident.fields.system');
200
189
  default:
201
190
  throw new Error(`unrecognised entity kind: ${entity.kind}`);
202
191
  }
@@ -0,0 +1,55 @@
1
+ import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
2
+ import React from 'react';
3
+ import { IncidentApi, IncidentApiRef } from '../../api/client';
4
+ import { HomePageIncidentCardContent } from './Content';
5
+
6
+ const mockIncidentApi: jest.Mocked<Partial<IncidentApi>> = {
7
+ request: jest.fn().mockResolvedValue({
8
+ incidents: [
9
+ {
10
+ id: 'incident-id',
11
+ name: 'Incident',
12
+ reference: 'INC-1',
13
+ incident_status: {
14
+ id: 'status-id',
15
+ category: 'live',
16
+ name: 'triage',
17
+ },
18
+ incident_role_assignments: [
19
+ {
20
+ assignee: {
21
+ name: 'John Smith',
22
+ },
23
+ role: {
24
+ role_type: 'lead',
25
+ },
26
+ },
27
+ ],
28
+ incident_timestamp_values: [
29
+ {
30
+ incident_timestamp: {
31
+ id: '01FCNDV6P870EA6S7TK1DSYD5H',
32
+ name: 'reported',
33
+ rank: 1,
34
+ },
35
+ value: {
36
+ value: '2021-08-17T13:28:57.801578Z',
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ }),
43
+ };
44
+
45
+ describe('HomePageIncidentCardContent', () => {
46
+ it('should render a list of live incidents', async () => {
47
+ const { getByTestId } = await renderInTestApp(
48
+ <TestApiProvider apis={[[IncidentApiRef, mockIncidentApi]]}>
49
+ <HomePageIncidentCardContent />
50
+ </TestApiProvider>
51
+ );
52
+
53
+ expect(getByTestId('chip-status-id')).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,56 @@
1
+ import { Progress } from '@backstage/core-components';
2
+ import Link from '@material-ui/core/Link';
3
+ import { Alert } from '@material-ui/lab';
4
+ import React from 'react';
5
+ import { useIncidentList } from '../../hooks/useIncidentRequest';
6
+ import { Typography, List } from '@material-ui/core';
7
+ import { IncidentListItem } from '../IncidentListItem';
8
+ import { configApiRef, useApi } from '@backstage/core-plugin-api';
9
+ import { getBaseUrl } from '../../config';
10
+
11
+ export const HomePageIncidentCardContent = () => {
12
+ const config = useApi(configApiRef);
13
+ const baseUrl = getBaseUrl(config);
14
+
15
+ const query = new URLSearchParams();
16
+ query.set(`status_category[one_of]`, 'live');
17
+ const { loading, error, value } = useIncidentList(query);
18
+ const incidents = value?.incidents;
19
+
20
+ if (loading) return <Progress />;
21
+ if (error) return <Alert severity="error">{error.message}</Alert>;
22
+
23
+ return (
24
+ <>
25
+ {incidents && incidents.length > 0 && (
26
+ <Typography variant="subtitle1">
27
+ There are <strong>{incidents.length}</strong> ongoing incidents.
28
+ </Typography>
29
+ )}
30
+ {incidents && incidents.length === 0 && (
31
+ <Typography variant="subtitle1">No ongoing incidents.</Typography>
32
+ )}
33
+ <List dense>
34
+ {incidents?.map((incident) => {
35
+ return (
36
+ <IncidentListItem
37
+ key={incident.id}
38
+ incident={incident}
39
+ baseUrl={baseUrl}
40
+ />
41
+ );
42
+ })}
43
+ </List>
44
+ <Typography variant="subtitle1">
45
+ Click to{' '}
46
+ <Link target="_blank" href={`${baseUrl}/incidents?${query.toString()}`}>
47
+ see more.
48
+ </Link>
49
+ </Typography>
50
+ </>
51
+ );
52
+ };
53
+
54
+ export const Content = () => {
55
+ return <HomePageIncidentCardContent />;
56
+ };
@@ -0,0 +1 @@
1
+ export { Content } from './Content';
@@ -0,0 +1,22 @@
1
+ import { useApi } from '@backstage/core-plugin-api';
2
+ import { useAsync } from 'react-use';
3
+ import { IncidentApiRef } from '../api/client';
4
+ import { definitions } from '../api/types';
5
+ import { DependencyList } from 'react';
6
+
7
+ export const useIncidentList = (
8
+ query: URLSearchParams,
9
+ deps?: DependencyList
10
+ ) => {
11
+ const IncidentApi = useApi(IncidentApiRef);
12
+
13
+ const { value, loading, error } = useAsync(async () => {
14
+ return await IncidentApi.request<
15
+ definitions['IncidentsV2ListResponseBody']
16
+ >({
17
+ path: `/v2/incidents?${query.toString()}`,
18
+ });
19
+ }, deps);
20
+
21
+ return { loading, error, value };
22
+ };
package/src/index.ts CHANGED
@@ -13,4 +13,8 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export { incidentPlugin, EntityIncidentCard } from './plugin';
16
+ export {
17
+ incidentPlugin,
18
+ EntityIncidentCard,
19
+ HomePageIncidentCard,
20
+ } from './plugin';
package/src/plugin.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  discoveryApiRef,
21
21
  identityApiRef,
22
22
  } from '@backstage/core-plugin-api';
23
+ import { createCardExtension } from '@backstage/plugin-home-react';
23
24
 
24
25
  import { IncidentApi, IncidentApiRef } from './api/client';
25
26
 
@@ -45,8 +46,16 @@ export const EntityIncidentCard = incidentPlugin.provide(
45
46
  component: {
46
47
  lazy: () =>
47
48
  import('./components/EntityIncidentCard').then(
48
- m => m.EntityIncidentCard,
49
+ (m) => m.EntityIncidentCard
49
50
  ),
50
51
  },
51
- }),
52
+ })
53
+ );
54
+
55
+ export const HomePageIncidentCard = incidentPlugin.provide(
56
+ createCardExtension({
57
+ name: 'HomePageIncidentCard',
58
+ title: 'Ongoing Incidents',
59
+ components: () => import('./components/HomePageIncidentCard'),
60
+ })
52
61
  );
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-08133e09.esm.js","sources":["../../src/config.ts","../../src/components/IncidentListItem/index.tsx","../../src/components/EntityIncidentCard/index.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ConfigApi } from '@backstage/core-plugin-api';\n\n// Find the baseUrl of the incident dashboard.\nexport function getBaseUrl(config: ConfigApi) {\n try {\n const baseUrl = config.getString('incident.baseUrl');\n if (baseUrl !== '') {\n return baseUrl;\n }\n } catch (e) {\n // no action\n }\n\n return 'https://app.incident.io';\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { DateTime, Duration } from 'luxon';\nimport { BackstageTheme } from '@backstage/theme';\nimport {\n Chip,\n IconButton,\n ListItem,\n ListItemSecondaryAction,\n ListItemText,\n Tooltip,\n Typography,\n makeStyles,\n} from '@material-ui/core';\nimport OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';\nimport React from 'react';\nimport { definitions } from '../../api/types';\n\nconst useStyles = makeStyles<BackstageTheme>(theme => ({\n listItemPrimary: {\n display: 'flex', // vertically align with chip\n fontWeight: 'bold',\n },\n warning: {\n borderColor: theme.palette.status.warning,\n color: theme.palette.status.warning,\n '& *': {\n color: theme.palette.status.warning,\n },\n },\n error: {\n borderColor: theme.palette.status.error,\n color: theme.palette.status.error,\n '& *': {\n color: theme.palette.status.error,\n },\n },\n}));\n\n// Single item in the list of on-going incidents.\nexport const IncidentListItem = ({\n baseUrl,\n incident,\n}: {\n baseUrl: string;\n incident: definitions['IncidentV2ResponseBody'];\n}) => {\n const classes = useStyles();\n const reportedAt = incident.incident_timestamp_values?.find(ts =>\n ts.incident_timestamp.name.match(/reported/i),\n );\n\n // If reported isn't here for some reason, use created at.\n const reportedAtDate = reportedAt?.value?.value || incident.created_at;\n\n const sinceReported =\n new Date().getTime() - new Date(reportedAtDate).getTime();\n const sinceReportedLabel = DateTime.local()\n .minus(Duration.fromMillis(sinceReported))\n .toRelative({ locale: 'en' });\n const lead = incident.incident_role_assignments.find(roleAssignment => {\n return roleAssignment.role.role_type === 'lead';\n });\n\n return (\n <ListItem dense key={incident.id}>\n <ListItemText\n primary={\n <>\n <Chip\n data-testid={`chip-${incident.incident_status.id}`}\n label={incident.incident_status.name}\n size=\"small\"\n variant=\"outlined\"\n className={\n ['live'].includes(incident.incident_status.category)\n ? classes.error\n : classes.warning\n }\n />\n {incident.reference} {incident.name}\n </>\n }\n primaryTypographyProps={{\n variant: 'body1',\n className: classes.listItemPrimary,\n }}\n secondary={\n <Typography noWrap variant=\"body2\" color=\"textSecondary\">\n Reported {sinceReportedLabel} and{' '}\n {lead?.assignee\n ? `${lead.assignee.name} is lead`\n : 'the lead is unassigned'}\n .\n </Typography>\n }\n />\n <ListItemSecondaryAction>\n <Tooltip title=\"View in incident.io\" placement=\"top\">\n <IconButton\n href={`${baseUrl}/incidents/${incident.id}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n color=\"primary\"\n >\n <OpenInBrowserIcon />\n </IconButton>\n </Tooltip>\n </ListItemSecondaryAction>\n </ListItem>\n );\n};\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Entity } from \"@backstage/catalog-model\";\nimport {\n HeaderIconLinkRow,\n IconLinkVerticalProps,\n Progress,\n} from \"@backstage/core-components\";\nimport { ConfigApi, configApiRef, useApi } from \"@backstage/core-plugin-api\";\nimport { useEntity } from \"@backstage/plugin-catalog-react\";\nimport {\n Card,\n CardContent,\n CardHeader,\n Divider,\n IconButton,\n List,\n Typography,\n} from \"@material-ui/core\";\nimport Link from \"@material-ui/core/Link\";\nimport CachedIcon from \"@material-ui/icons/Cached\";\nimport HistoryIcon from \"@material-ui/icons/History\";\nimport WhatshotIcon from \"@material-ui/icons/Whatshot\";\nimport { Alert } from \"@material-ui/lab\";\nimport React, { useState } from \"react\";\nimport { useAsync } from \"react-use\";\nimport { IncidentApiRef } from \"../../api/client\";\nimport { definitions } from \"../../api/types\";\nimport { getBaseUrl } from \"../../config\";\nimport { IncidentListItem } from \"../IncidentListItem\";\n\n// The card displayed on the entity page showing a handful of the most recent\n// incidents that are on-going for that component.\nexport const EntityIncidentCard = ({\n maxIncidents = 2,\n}: {\n maxIncidents?: number;\n}) => {\n const config = useApi(configApiRef);\n const baseUrl = getBaseUrl(config);\n const { entity } = useEntity();\n\n const IncidentApi = useApi(IncidentApiRef);\n\n const [reload, setReload] = useState(false);\n\n const entityFieldID = getEntityFieldID(config, entity);\n\n if (!entityFieldID) {\n return <IncorrectConfigCard />;\n }\n\n const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;\n\n // This query filters incidents for those that are associated with this\n // entity.\n const query = new URLSearchParams();\n query.set(`custom_field[${entityFieldID}][one_of]`, entityID);\n\n // This restricts the previous filter to focus only on live incidents.\n const queryLive = new URLSearchParams(query);\n queryLive.set(`status_category[one_of]`, \"live\");\n\n const createIncidentLink: IconLinkVerticalProps = {\n label: \"Create incident\",\n disabled: false,\n icon: <WhatshotIcon />,\n href: `${baseUrl}/incidents/create`,\n };\n\n const viewIncidentsLink: IconLinkVerticalProps = {\n label: \"View past incidents\",\n disabled: false,\n icon: <HistoryIcon />,\n href: `${baseUrl}/incidents?${query.toString()}`,\n };\n\n const {\n value: incidentsResponse,\n loading: incidentsLoading,\n error: incidentsError,\n } = useAsync(async () => {\n return await IncidentApi.request<\n definitions[\"IncidentsV2ListResponseBody\"]\n >({\n path: `/v2/incidents?${queryLive.toString()}`,\n });\n }, [reload]);\n\n const incidents = incidentsResponse?.incidents;\n\n return (\n <Card>\n <CardHeader\n title=\"incident.io\"\n action={\n <>\n <IconButton\n component={Link}\n aria-label=\"Refresh\"\n disabled={false}\n title=\"Refresh\"\n onClick={() => setReload(!reload)}\n >\n <CachedIcon />\n </IconButton>\n </>\n }\n subheader={\n <HeaderIconLinkRow links={[createIncidentLink, viewIncidentsLink]} />\n }\n />\n <Divider />\n <CardContent>\n {incidentsLoading && <Progress />}\n {incidentsError && (\n <Alert severity=\"error\">{incidentsError.message}</Alert>\n )}\n {!incidentsLoading && !incidentsError && incidents && (\n <>\n {incidents && incidents.length > 0 && (\n <Typography variant=\"subtitle1\">\n There are <strong>{incidents.length}</strong> ongoing incidents\n involving <strong>{entity.metadata.name}</strong>.\n </Typography>\n )}\n {incidents && incidents.length === 0 && (\n <Typography variant=\"subtitle1\">No ongoing incidents.</Typography>\n )}\n <List dense>\n {incidents?.slice(0, maxIncidents)?.map((incident) => {\n return (\n <IncidentListItem\n key={incident.id}\n incident={incident}\n baseUrl={baseUrl}\n />\n );\n })}\n </List>\n <Typography variant=\"subtitle1\">\n Click to{\" \"}\n <Link\n target=\"_blank\"\n href={`${baseUrl}/incidents?${queryLive.toString()}`}\n >\n see more.\n </Link>\n </Typography>\n </>\n )}\n </CardContent>\n </Card>\n );\n};\n\nconst IncorrectConfigCard = () => {\n return (\n <Card>\n <CardHeader title=\"incident.io\" />\n <Divider />\n <CardContent>\n <Typography variant=\"subtitle1\">\n No custom field configuration was found. In order to display\n incidents, this entity must be mapped to an incident.io custom field\n ID in Backstage's app-config.yaml.\n </Typography>\n </CardContent>\n </Card>\n );\n};\n\n// Find the ID of the custom field in incident that represents the association\n// to this type of entity.\n//\n// In practice, this will be kind=Component => ID of Affected components field.\nfunction getEntityFieldID(config: ConfigApi, entity: Entity) {\n switch (entity.kind) {\n case \"API\":\n return config.getOptional(\"incident.fields.api\");\n case \"Component\":\n return config.getOptional(\"incident.fields.component\");\n case \"Domain\":\n return config.getOptional(\"incident.fields.domain\");\n case \"System\":\n return config.getOptional(\"incident.fields.system\");\n default:\n throw new Error(`unrecognised entity kind: ${entity.kind}`);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-14b311c0.esm.js","sources":["../../src/api/client.ts","../../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n DiscoveryApi,\n IdentityApi,\n createApiRef,\n} from '@backstage/core-plugin-api';\n\nexport const IncidentApiRef = createApiRef<Incident>({\n id: 'plugin.incident.service',\n});\n\ntype HTTPMethods = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE';\n\nexport interface Incident {\n request<T>({\n method,\n path,\n body,\n }: {\n method?: HTTPMethods;\n path: string;\n body?: string;\n }): Promise<T>;\n}\n\nconst DEFAULT_PROXY_PATH = '/incident/api';\n\ntype Options = {\n discoveryApi: DiscoveryApi;\n identityApi: IdentityApi;\n proxyPath?: string;\n};\n\nexport class IncidentApi implements Incident {\n private readonly discoveryApi: DiscoveryApi;\n private readonly identityApi: IdentityApi;\n private readonly proxyPath: string;\n\n constructor(opts: Options) {\n this.discoveryApi = opts.discoveryApi;\n this.identityApi = opts.identityApi;\n this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;\n }\n\n async request<T = any>({\n path,\n method = 'GET',\n body,\n }: {\n path: string;\n method?: HTTPMethods;\n body?: string;\n }): Promise<T> {\n const apiUrl =\n (await this.discoveryApi.getBaseUrl('proxy')) + this.proxyPath;\n const { token } = await this.identityApi.getCredentials();\n\n const resp = await fetch(`${apiUrl}${path}`, {\n method: method,\n body: body,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n if (!resp.ok) {\n throw new Error(`${resp.status} ${resp.statusText}`);\n }\n\n return await resp.json();\n }\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n createApiFactory,\n createComponentExtension,\n createPlugin,\n discoveryApiRef,\n identityApiRef,\n} from '@backstage/core-plugin-api';\n\nimport { IncidentApi, IncidentApiRef } from './api/client';\n\nexport const incidentPlugin = createPlugin({\n id: 'incident',\n apis: [\n createApiFactory({\n api: IncidentApiRef,\n deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },\n factory: ({ discoveryApi, identityApi }) => {\n return new IncidentApi({\n discoveryApi: discoveryApi,\n identityApi: identityApi,\n });\n },\n }),\n ],\n});\n\nexport const EntityIncidentCard = incidentPlugin.provide(\n createComponentExtension({\n name: 'EntityIncidentCard',\n component: {\n lazy: () =>\n import('./components/EntityIncidentCard').then(\n m => m.EntityIncidentCard,\n ),\n },\n }),\n);\n"],"names":[],"mappings":";;;;;;;;AAqBO,MAAM,iBAAiB,YAAuB,CAAA;AAAA,EACnD,EAAI,EAAA,yBAAA;AACN,CAAC,EAAA;AAgBD,MAAM,kBAAqB,GAAA,eAAA,CAAA;AAQpB,MAAM,WAAgC,CAAA;AAAA,EAK3C,YAAY,IAAe,EAAA;AAJ3B,IAAiB,aAAA,CAAA,IAAA,EAAA,cAAA,CAAA,CAAA;AACjB,IAAiB,aAAA,CAAA,IAAA,EAAA,aAAA,CAAA,CAAA;AACjB,IAAiB,aAAA,CAAA,IAAA,EAAA,WAAA,CAAA,CAAA;AAlDnB,IAAA,IAAA,EAAA,CAAA;AAqDI,IAAA,IAAA,CAAK,eAAe,IAAK,CAAA,YAAA,CAAA;AACzB,IAAA,IAAA,CAAK,cAAc,IAAK,CAAA,WAAA,CAAA;AACxB,IAAK,IAAA,CAAA,SAAA,GAAA,CAAY,EAAK,GAAA,IAAA,CAAA,SAAA,KAAL,IAAkB,GAAA,EAAA,GAAA,kBAAA,CAAA;AAAA,GACrC;AAAA,EAEA,MAAM,OAAiB,CAAA;AAAA,IACrB,IAAA;AAAA,IACA,MAAS,GAAA,KAAA;AAAA,IACT,IAAA;AAAA,GAKa,EAAA;AACb,IAAA,MAAM,SACH,MAAM,IAAA,CAAK,aAAa,UAAW,CAAA,OAAO,IAAK,IAAK,CAAA,SAAA,CAAA;AACvD,IAAA,MAAM,EAAE,KAAM,EAAA,GAAI,MAAM,IAAA,CAAK,YAAY,cAAe,EAAA,CAAA;AAExD,IAAA,MAAM,OAAO,MAAM,KAAA,CAAM,GAAG,MAAM,CAAA,EAAG,IAAI,CAAI,CAAA,EAAA;AAAA,MAC3C,MAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAS,EAAA;AAAA,QACP,aAAA,EAAe,UAAU,KAAK,CAAA,CAAA;AAAA,OAChC;AAAA,KACD,CAAA,CAAA;AACD,IAAI,IAAA,CAAC,KAAK,EAAI,EAAA;AACZ,MAAM,MAAA,IAAI,MAAM,CAAG,EAAA,IAAA,CAAK,MAAM,CAAI,CAAA,EAAA,IAAA,CAAK,UAAU,CAAE,CAAA,CAAA,CAAA;AAAA,KACrD;AAEA,IAAO,OAAA,MAAM,KAAK,IAAK,EAAA,CAAA;AAAA,GACzB;AACF;;AC3DO,MAAM,iBAAiB,YAAa,CAAA;AAAA,EACzC,EAAI,EAAA,UAAA;AAAA,EACJ,IAAM,EAAA;AAAA,IACJ,gBAAiB,CAAA;AAAA,MACf,GAAK,EAAA,cAAA;AAAA,MACL,IAAM,EAAA,EAAE,YAAc,EAAA,eAAA,EAAiB,aAAa,cAAe,EAAA;AAAA,MACnE,OAAS,EAAA,CAAC,EAAE,YAAA,EAAc,aAAkB,KAAA;AAC1C,QAAA,OAAO,IAAI,WAAY,CAAA;AAAA,UACrB,YAAA;AAAA,UACA,WAAA;AAAA,SACD,CAAA,CAAA;AAAA,OACH;AAAA,KACD,CAAA;AAAA,GACH;AACF,CAAC,EAAA;AAEM,MAAM,qBAAqB,cAAe,CAAA,OAAA;AAAA,EAC/C,wBAAyB,CAAA;AAAA,IACvB,IAAM,EAAA,oBAAA;AAAA,IACN,SAAW,EAAA;AAAA,MACT,IAAM,EAAA,MACJ,OAAO,yBAAiC,CAAE,CAAA,IAAA;AAAA,QACxC,OAAK,CAAE,CAAA,kBAAA;AAAA,OACT;AAAA,KACJ;AAAA,GACD,CAAA;AACH;;;;"}