@incident-io/backstage 0.0.2
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/README.md +115 -0
- package/config.d.ts +64 -0
- package/dist/esm/index-91fad684.esm.js +62 -0
- package/dist/esm/index-91fad684.esm.js.map +1 -0
- package/dist/esm/index-c77be796.esm.js +182 -0
- package/dist/esm/index-c77be796.esm.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.esm.js +3 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +67 -0
- package/src/api/client.ts +85 -0
- package/src/api/types.ts +13242 -0
- package/src/components/EntityIncidentCard/index.tsx +182 -0
- package/src/components/IncidentListItem/index.tsx +125 -0
- package/src/config.ts +30 -0
- package/src/index.ts +16 -0
- package/src/plugin.test.ts +22 -0
- package/src/plugin.ts +52 -0
- package/src/setupTests.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# incident.io
|
|
2
|
+
|
|
3
|
+
Use this plugin to display on-going and historic incidents against Backstage
|
|
4
|
+
components, and to provide quick-links to open new incidents for that service
|
|
5
|
+
inside of incident.io.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
[importer]: https://github.com/incident/catalog-importer
|
|
10
|
+
|
|
11
|
+
Once you've configured the [catalog-importer][importer] to sync your Backstage
|
|
12
|
+
catalog into incident.io, you can visit your incident.io dashboard to create a
|
|
13
|
+
custom field that is powered by the Backstage Component catalog type.
|
|
14
|
+
|
|
15
|
+
We recommend creating a multi-select field called something like "Affected
|
|
16
|
+
services" or "Impacted components".
|
|
17
|
+
|
|
18
|
+
Remember the custom field ID (taken from the incident.io dashboard) as you'll
|
|
19
|
+
need it later.
|
|
20
|
+
|
|
21
|
+
## Install the plugin
|
|
22
|
+
|
|
23
|
+
The file paths mentioned in the following steps are relative to your app's root
|
|
24
|
+
directory — for example, the directory created by following the [Getting
|
|
25
|
+
Started](https://backstage.io/docs/getting-started/) guide and creating your app
|
|
26
|
+
with `npx @backstage/create-app`.
|
|
27
|
+
|
|
28
|
+
First, install the incident plugin via a CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# From your Backstage app root directory
|
|
32
|
+
yarn add --cwd packages/app @incident-io/backstage
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Next, add the plugin to `EntityPage.tsx` in
|
|
36
|
+
`packages/app/src/components/catalog` by adding the following code snippets.
|
|
37
|
+
|
|
38
|
+
Add the following imports to the top of the file:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { EntityIncidentCard } from "@incident-io/backstage";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Find `const overviewContent` in `EntityPage.tsx`, and add the following snippet
|
|
45
|
+
inside the outermost `Grid` defined there, just before the closing `</Grid>`
|
|
46
|
+
tag:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
<EntitySwitch>
|
|
50
|
+
<EntitySwitch.Case if={isIncidentAvailable}>
|
|
51
|
+
<Grid item md={6}>
|
|
52
|
+
<EntityIncidentCard />
|
|
53
|
+
</Grid>
|
|
54
|
+
</EntitySwitch.Case>
|
|
55
|
+
</EntitySwitch>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
When you're done, the `overviewContent` definition should look something like
|
|
59
|
+
this:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const overviewContent = (
|
|
63
|
+
<Grid ...>
|
|
64
|
+
...
|
|
65
|
+
<EntitySwitch>
|
|
66
|
+
<EntitySwitch.Case if={isIncidentAvailable}>
|
|
67
|
+
<Grid item md={6}>
|
|
68
|
+
<EntityIncidentCard />
|
|
69
|
+
</Grid>
|
|
70
|
+
</EntitySwitch.Case>
|
|
71
|
+
</EntitySwitch>
|
|
72
|
+
</Grid>
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configure the plugin
|
|
77
|
+
|
|
78
|
+
[annotate]: https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional
|
|
79
|
+
|
|
80
|
+
First, [annotate][annotate] the appropriate entity with the incident.io
|
|
81
|
+
integration key in its `.yaml` configuration file:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
annotations:
|
|
85
|
+
incident.io/api-key: [API_KEY]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
[api-keys]: https://app.incident.io/settings/api-keys/
|
|
89
|
+
[api-docs]: https://api-docs.incident.io/
|
|
90
|
+
|
|
91
|
+
Next, provide the [API key][api-keys] that the client will use to make requests
|
|
92
|
+
to the [incident.io API][api-docs].
|
|
93
|
+
|
|
94
|
+
Add the proxy configuration in `app-config.yaml`:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
proxy:
|
|
98
|
+
...
|
|
99
|
+
'/incident/api':
|
|
100
|
+
target: https://api.incident.io
|
|
101
|
+
headers:
|
|
102
|
+
Authorization: Bearer ${INCIDENT_API_KEY}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Finally, for any of the custom fields you've configured in incident that are
|
|
106
|
+
powered by Backstage catalog types, fill out the following `app-config.yaml`:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
integrations:
|
|
110
|
+
incident:
|
|
111
|
+
api-field: "<id-of-api-custom-field>"
|
|
112
|
+
component-field: "<id-of-component-custom-field>"
|
|
113
|
+
system-field: "<id-of-system-custom-field>"
|
|
114
|
+
domain-field: "<id-of-domain-custom-field>"
|
|
115
|
+
```
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export interface Config {
|
|
17
|
+
/* Configuration options for the incident plugin
|
|
18
|
+
* @visibility frontend
|
|
19
|
+
*/
|
|
20
|
+
incident?: {
|
|
21
|
+
/**
|
|
22
|
+
* The API key that provides access to the incident.io API.
|
|
23
|
+
* @see https://app.incident.io/settings/api-keys
|
|
24
|
+
*/
|
|
25
|
+
apiKey: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The base URL of the incident dashboard, only useful in development.
|
|
29
|
+
* @visibility frontend
|
|
30
|
+
*/
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The endpoint to use for API calls, only useful for development.
|
|
35
|
+
*/
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* @visibility frontend
|
|
40
|
+
*/
|
|
41
|
+
fields: {
|
|
42
|
+
/**
|
|
43
|
+
* The custom field ID that associated API entities to incidents.
|
|
44
|
+
* @visibility frontend
|
|
45
|
+
*/
|
|
46
|
+
api?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The custom field ID that associated Component entities to incidents.
|
|
49
|
+
* @visibility frontend
|
|
50
|
+
*/
|
|
51
|
+
component?: string;
|
|
52
|
+
/**
|
|
53
|
+
* The custom field ID that associated Domain entities to incidents.
|
|
54
|
+
* @visibility frontend
|
|
55
|
+
*/
|
|
56
|
+
domain?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The custom field ID that associated System entities to incidents.
|
|
59
|
+
* @visibility frontend
|
|
60
|
+
*/
|
|
61
|
+
system?: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createApiRef, createPlugin, createApiFactory, discoveryApiRef, identityApiRef, createComponentExtension } from '@backstage/core-plugin-api';
|
|
2
|
+
|
|
3
|
+
const IncidentApiRef = createApiRef({
|
|
4
|
+
id: "plugin.incident.service"
|
|
5
|
+
});
|
|
6
|
+
const DEFAULT_PROXY_PATH = "/incident/api";
|
|
7
|
+
class IncidentApi {
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
var _a;
|
|
10
|
+
this.discoveryApi = opts.discoveryApi;
|
|
11
|
+
this.identityApi = opts.identityApi;
|
|
12
|
+
this.proxyPath = (_a = opts.proxyPath) != null ? _a : DEFAULT_PROXY_PATH;
|
|
13
|
+
}
|
|
14
|
+
async request({
|
|
15
|
+
path,
|
|
16
|
+
method = "GET",
|
|
17
|
+
body
|
|
18
|
+
}) {
|
|
19
|
+
const apiUrl = await this.discoveryApi.getBaseUrl("proxy") + this.proxyPath;
|
|
20
|
+
const { token } = await this.identityApi.getCredentials();
|
|
21
|
+
const resp = await fetch(`${apiUrl}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
body,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${token}`
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (!resp.ok) {
|
|
29
|
+
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
return await resp.json();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const incidentPlugin = createPlugin({
|
|
36
|
+
id: "incident",
|
|
37
|
+
apis: [
|
|
38
|
+
createApiFactory({
|
|
39
|
+
api: IncidentApiRef,
|
|
40
|
+
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
|
41
|
+
factory: ({ discoveryApi, identityApi }) => {
|
|
42
|
+
return new IncidentApi({
|
|
43
|
+
discoveryApi,
|
|
44
|
+
identityApi
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
const EntityIncidentCard = incidentPlugin.provide(
|
|
51
|
+
createComponentExtension({
|
|
52
|
+
name: "EntityIncidentCard",
|
|
53
|
+
component: {
|
|
54
|
+
lazy: () => import('./index-c77be796.esm.js').then(
|
|
55
|
+
(m) => m.EntityIncidentCard
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export { EntityIncidentCard as E, IncidentApiRef as I, incidentPlugin as i };
|
|
62
|
+
//# sourceMappingURL=index-91fad684.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-91fad684.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;AApD7B,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,IAAO,GAAA,MAAM,KAAM,CAAA,CAAA,EAAG,SAAS,IAAQ,CAAA,CAAA,EAAA;AAAA,MAC3C,MAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAS,EAAA;AAAA,QACP,eAAe,CAAU,OAAA,EAAA,KAAA,CAAA,CAAA;AAAA,OAC3B;AAAA,KACD,CAAA,CAAA;AACD,IAAI,IAAA,CAAC,KAAK,EAAI,EAAA;AACZ,MAAA,MAAM,IAAI,KAAM,CAAA,CAAA,EAAG,IAAK,CAAA,MAAA,CAAA,CAAA,EAAU,KAAK,UAAY,CAAA,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;;;;"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { HeaderIconLinkRow, Progress } from '@backstage/core-components';
|
|
2
|
+
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
|
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';
|
|
5
|
+
import Link from '@material-ui/core/Link';
|
|
6
|
+
import CachedIcon from '@material-ui/icons/Cached';
|
|
7
|
+
import HistoryIcon from '@material-ui/icons/History';
|
|
8
|
+
import WhatshotIcon from '@material-ui/icons/Whatshot';
|
|
9
|
+
import { Alert } from '@material-ui/lab';
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { useAsync } from 'react-use';
|
|
12
|
+
import { I as IncidentApiRef } from './index-91fad684.esm.js';
|
|
13
|
+
import { DateTime, Duration } from 'luxon';
|
|
14
|
+
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
|
15
|
+
|
|
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
|
+
))));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const EntityIncidentCard = ({
|
|
95
|
+
maxIncidents = 2
|
|
96
|
+
}) => {
|
|
97
|
+
var _a;
|
|
98
|
+
const config = useApi(configApiRef);
|
|
99
|
+
const baseUrl = getBaseUrl(config);
|
|
100
|
+
const { entity } = useEntity();
|
|
101
|
+
const IncidentApi = useApi(IncidentApiRef);
|
|
102
|
+
const [reload, setReload] = useState(false);
|
|
103
|
+
const entityFieldID = getEntityFieldID(config, entity);
|
|
104
|
+
const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;
|
|
105
|
+
const query = new URLSearchParams();
|
|
106
|
+
query.set(`custom_field[${entityFieldID}][one_of]`, entityID);
|
|
107
|
+
const queryLive = new URLSearchParams(query);
|
|
108
|
+
queryLive.set(`status_category[one_of]`, "live");
|
|
109
|
+
const createIncidentLink = {
|
|
110
|
+
label: "Create incident",
|
|
111
|
+
disabled: false,
|
|
112
|
+
icon: /* @__PURE__ */ React.createElement(WhatshotIcon, null),
|
|
113
|
+
href: `${baseUrl}/incidents/create`
|
|
114
|
+
};
|
|
115
|
+
const viewIncidentsLink = {
|
|
116
|
+
label: "View past incidents",
|
|
117
|
+
disabled: false,
|
|
118
|
+
icon: /* @__PURE__ */ React.createElement(HistoryIcon, null),
|
|
119
|
+
href: `${baseUrl}/incidents?${query.toString()}`
|
|
120
|
+
};
|
|
121
|
+
const {
|
|
122
|
+
value: incidentsResponse,
|
|
123
|
+
loading: incidentsLoading,
|
|
124
|
+
error: incidentsError
|
|
125
|
+
} = useAsync(async () => {
|
|
126
|
+
return await IncidentApi.request({
|
|
127
|
+
path: `/v2/incidents?${queryLive.toString()}`
|
|
128
|
+
});
|
|
129
|
+
}, [reload]);
|
|
130
|
+
const incidents = incidentsResponse == null ? void 0 : incidentsResponse.incidents;
|
|
131
|
+
return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(
|
|
132
|
+
CardHeader,
|
|
133
|
+
{
|
|
134
|
+
title: "Incidents",
|
|
135
|
+
action: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
136
|
+
IconButton,
|
|
137
|
+
{
|
|
138
|
+
component: Link,
|
|
139
|
+
"aria-label": "Refresh",
|
|
140
|
+
disabled: false,
|
|
141
|
+
title: "Refresh",
|
|
142
|
+
onClick: () => setReload(!reload)
|
|
143
|
+
},
|
|
144
|
+
/* @__PURE__ */ React.createElement(CachedIcon, null)
|
|
145
|
+
)),
|
|
146
|
+
subheader: /* @__PURE__ */ React.createElement(HeaderIconLinkRow, { links: [createIncidentLink, viewIncidentsLink] })
|
|
147
|
+
}
|
|
148
|
+
), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(CardContent, null, incidentsLoading && /* @__PURE__ */ React.createElement(Progress, null), incidentsError && /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, incidentsError.message), !incidentsLoading && !incidentsError && incidents && /* @__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 involving ", /* @__PURE__ */ React.createElement("strong", null, entity.metadata.name), "."), incidents && incidents.length === 0 && /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "No ongoing incidents."), /* @__PURE__ */ React.createElement(List, { dense: true }, (_a = incidents == null ? void 0 : incidents.slice(0, maxIncidents)) == null ? void 0 : _a.map((incident) => {
|
|
149
|
+
return /* @__PURE__ */ React.createElement(
|
|
150
|
+
IncidentListItem,
|
|
151
|
+
{
|
|
152
|
+
key: incident.id,
|
|
153
|
+
incident,
|
|
154
|
+
baseUrl
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
})), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "Click to", " ", /* @__PURE__ */ React.createElement(
|
|
158
|
+
Link,
|
|
159
|
+
{
|
|
160
|
+
target: "_blank",
|
|
161
|
+
href: `${baseUrl}/incidents?${queryLive.toString()}`
|
|
162
|
+
},
|
|
163
|
+
"see more."
|
|
164
|
+
)))));
|
|
165
|
+
};
|
|
166
|
+
function getEntityFieldID(config, entity) {
|
|
167
|
+
switch (entity.kind) {
|
|
168
|
+
case "API":
|
|
169
|
+
return config.get("integrations.incident.fields.api");
|
|
170
|
+
case "Component":
|
|
171
|
+
return config.get("integrations.incident.fields.component");
|
|
172
|
+
case "Domain":
|
|
173
|
+
return config.get("integrations.incident.fields.domain");
|
|
174
|
+
case "System":
|
|
175
|
+
return config.get("integrations.incident.fields.system");
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`unrecognised entity kind: ${entity.kind}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { EntityIncidentCard };
|
|
182
|
+
//# sourceMappingURL=index-c77be796.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-c77be796.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 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=\"Incidents\"\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.get('integrations.incident.fields.api');\n case 'Component':\n return config.get('integrations.incident.fields.component');\n case 'Domain':\n return config.get('integrations.incident.fields.domain');\n case 'System':\n return config.get('integrations.incident.fields.system');\n default:\n throw new Error(`unrecognised entity kind: ${entity.kind}`);\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
|
|
4
|
+
|
|
5
|
+
declare const incidentPlugin: _backstage_core_plugin_api.BackstagePlugin<{}, {}, {}>;
|
|
6
|
+
declare const EntityIncidentCard: ({ maxIncidents, }: {
|
|
7
|
+
maxIncidents?: number | undefined;
|
|
8
|
+
}) => react.JSX.Element;
|
|
9
|
+
|
|
10
|
+
export { EntityIncidentCard, incidentPlugin };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@incident-io/backstage",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"main": "dist/index.esm.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"main": "dist/index.esm.js",
|
|
10
|
+
"types": "dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"backstage": {
|
|
13
|
+
"role": "frontend-plugin"
|
|
14
|
+
},
|
|
15
|
+
"configSchema": "config.d.ts",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "backstage-cli package start",
|
|
18
|
+
"build": "backstage-cli package build",
|
|
19
|
+
"lint": "backstage-cli package lint",
|
|
20
|
+
"test": "backstage-cli package test",
|
|
21
|
+
"clean": "backstage-cli package clean",
|
|
22
|
+
"prepack": "backstage-cli package prepack",
|
|
23
|
+
"postpack": "backstage-cli package postpack"
|
|
24
|
+
},
|
|
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-cost-insights": "^0.12.8",
|
|
33
|
+
"@backstage/plugin-search-react": "^1.6.2",
|
|
34
|
+
"@backstage/theme": "^0.4.0",
|
|
35
|
+
"@material-ui/core": "^4.9.13",
|
|
36
|
+
"@material-ui/icons": "^4.9.1",
|
|
37
|
+
"@material-ui/lab": "^4.0.0-alpha.60",
|
|
38
|
+
"luxon": "^3.3.0",
|
|
39
|
+
"moment": "^2.29.4",
|
|
40
|
+
"qs": "^6.11.2",
|
|
41
|
+
"react-use": "^17.2.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^16.13.1 || ^17.0.0",
|
|
45
|
+
"react-dom": "^16.13.1 || ^17.0.0",
|
|
46
|
+
"react-router": "^6.0.0",
|
|
47
|
+
"react-router-dom": "^6.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@backstage/cli": "^0.22.8",
|
|
51
|
+
"@backstage/core-app-api": "^1.8.1",
|
|
52
|
+
"@backstage/dev-utils": "^1.0.16",
|
|
53
|
+
"@backstage/test-utils": "^1.4.0",
|
|
54
|
+
"@testing-library/jest-dom": "^5.10.1",
|
|
55
|
+
"@testing-library/react": "^12.1.3",
|
|
56
|
+
"@testing-library/user-event": "^14.0.0",
|
|
57
|
+
"@types/node": "*",
|
|
58
|
+
"cross-fetch": "^3.1.5",
|
|
59
|
+
"msw": "^1.0.0"
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"config.d.ts",
|
|
63
|
+
"dist",
|
|
64
|
+
"src"
|
|
65
|
+
],
|
|
66
|
+
"module": "./dist/index.esm.js"
|
|
67
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
DiscoveryApi,
|
|
18
|
+
IdentityApi,
|
|
19
|
+
createApiRef,
|
|
20
|
+
} from '@backstage/core-plugin-api';
|
|
21
|
+
|
|
22
|
+
export const IncidentApiRef = createApiRef<Incident>({
|
|
23
|
+
id: 'plugin.incident.service',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type HTTPMethods = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE';
|
|
27
|
+
|
|
28
|
+
export interface Incident {
|
|
29
|
+
request<T>({
|
|
30
|
+
method,
|
|
31
|
+
path,
|
|
32
|
+
body,
|
|
33
|
+
}: {
|
|
34
|
+
method?: HTTPMethods;
|
|
35
|
+
path: string;
|
|
36
|
+
body?: string;
|
|
37
|
+
}): Promise<T>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_PROXY_PATH = '/incident/api';
|
|
41
|
+
|
|
42
|
+
type Options = {
|
|
43
|
+
discoveryApi: DiscoveryApi;
|
|
44
|
+
identityApi: IdentityApi;
|
|
45
|
+
proxyPath?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class IncidentApi implements Incident {
|
|
49
|
+
private readonly discoveryApi: DiscoveryApi;
|
|
50
|
+
private readonly identityApi: IdentityApi;
|
|
51
|
+
private readonly proxyPath: string;
|
|
52
|
+
|
|
53
|
+
constructor(opts: Options) {
|
|
54
|
+
this.discoveryApi = opts.discoveryApi;
|
|
55
|
+
this.identityApi = opts.identityApi;
|
|
56
|
+
this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async request<T = any>({
|
|
60
|
+
path,
|
|
61
|
+
method = 'GET',
|
|
62
|
+
body,
|
|
63
|
+
}: {
|
|
64
|
+
path: string;
|
|
65
|
+
method?: HTTPMethods;
|
|
66
|
+
body?: string;
|
|
67
|
+
}): Promise<T> {
|
|
68
|
+
const apiUrl =
|
|
69
|
+
(await this.discoveryApi.getBaseUrl('proxy')) + this.proxyPath;
|
|
70
|
+
const { token } = await this.identityApi.getCredentials();
|
|
71
|
+
|
|
72
|
+
const resp = await fetch(`${apiUrl}${path}`, {
|
|
73
|
+
method: method,
|
|
74
|
+
body: body,
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${token}`,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!resp.ok) {
|
|
80
|
+
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return await resp.json();
|
|
84
|
+
}
|
|
85
|
+
}
|