@incident-io/backstage 0.0.9 → 0.0.11
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 +3 -3
- package/dist/esm/index-354895e3.esm.js +115 -0
- package/dist/esm/index-354895e3.esm.js.map +1 -0
- package/dist/esm/index-98184150.esm.js +41 -0
- package/dist/esm/index-98184150.esm.js.map +1 -0
- package/dist/esm/{index-14b311c0.esm.js → index-c02bc084.esm.js} +11 -3
- package/dist/esm/index-c02bc084.esm.js.map +1 -0
- package/dist/esm/index-dfef90a3.esm.js +96 -0
- package/dist/esm/index-dfef90a3.esm.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +2 -1
- package/dist/index.esm.js.map +1 -1
- package/package.json +21 -15
- package/src/api/client.ts +6 -6
- package/src/api/types.ts +1008 -1003
- package/src/components/EntityIncidentCard/index.tsx +44 -44
- package/src/components/HomePageIncidentCard/Content.test.tsx +55 -0
- package/src/components/HomePageIncidentCard/Content.tsx +55 -0
- package/src/components/HomePageIncidentCard/index.ts +1 -0
- package/src/components/IncidentListItem/index.tsx +20 -20
- package/src/hooks/useIncidentRequest.ts +36 -0
- package/src/index.ts +5 -1
- package/src/plugin.ts +15 -6
- package/src/setupTests.ts +2 -2
- package/dist/esm/index-08133e09.esm.js +0 -188
- package/dist/esm/index-08133e09.esm.js.map +0 -1
- package/dist/esm/index-14b311c0.esm.js.map +0 -1
- package/src/config.ts +0 -30
|
@@ -36,12 +36,25 @@ import HistoryIcon from "@material-ui/icons/History";
|
|
|
36
36
|
import WhatshotIcon from "@material-ui/icons/Whatshot";
|
|
37
37
|
import { Alert } from "@material-ui/lab";
|
|
38
38
|
import React, { useState } from "react";
|
|
39
|
-
import {
|
|
40
|
-
import { IncidentApiRef } from "../../api/client";
|
|
41
|
-
import { definitions } from "../../api/types";
|
|
42
|
-
import { getBaseUrl } from "../../config";
|
|
39
|
+
import { useIncidentList, useIdentity } from "../../hooks/useIncidentRequest";
|
|
43
40
|
import { IncidentListItem } from "../IncidentListItem";
|
|
44
41
|
|
|
42
|
+
const IncorrectConfigCard = () => {
|
|
43
|
+
return (
|
|
44
|
+
<Card>
|
|
45
|
+
<CardHeader title="incident.io" />
|
|
46
|
+
<Divider />
|
|
47
|
+
<CardContent>
|
|
48
|
+
<Typography variant="subtitle1">
|
|
49
|
+
No custom field configuration was found. In order to display
|
|
50
|
+
incidents, this entity must be mapped to an incident.io custom field
|
|
51
|
+
ID in Backstage's app-config.yaml.
|
|
52
|
+
</Typography>
|
|
53
|
+
</CardContent>
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
45
58
|
// The card displayed on the entity page showing a handful of the most recent
|
|
46
59
|
// incidents that are on-going for that component.
|
|
47
60
|
export const EntityIncidentCard = ({
|
|
@@ -50,19 +63,16 @@ export const EntityIncidentCard = ({
|
|
|
50
63
|
maxIncidents?: number;
|
|
51
64
|
}) => {
|
|
52
65
|
const config = useApi(configApiRef);
|
|
53
|
-
const baseUrl = getBaseUrl(config);
|
|
54
66
|
const { entity } = useEntity();
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
const {
|
|
68
|
+
value: identityResponse,
|
|
69
|
+
loading: identityResponseLoading,
|
|
70
|
+
error: identityResponseError,
|
|
71
|
+
} = useIdentity();
|
|
57
72
|
|
|
58
73
|
const [reload, setReload] = useState(false);
|
|
59
74
|
|
|
60
75
|
const entityFieldID = getEntityFieldID(config, entity);
|
|
61
|
-
|
|
62
|
-
if (!entityFieldID) {
|
|
63
|
-
return <IncorrectConfigCard />;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
76
|
const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;
|
|
67
77
|
|
|
68
78
|
// This query filters incidents for those that are associated with this
|
|
@@ -72,7 +82,25 @@ export const EntityIncidentCard = ({
|
|
|
72
82
|
|
|
73
83
|
// This restricts the previous filter to focus only on live incidents.
|
|
74
84
|
const queryLive = new URLSearchParams(query);
|
|
75
|
-
queryLive.set(`status_category[one_of]`, "
|
|
85
|
+
queryLive.set(`status_category[one_of]`, "active");
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
value: incidentsResponse,
|
|
89
|
+
loading: incidentsLoading,
|
|
90
|
+
error: incidentsError,
|
|
91
|
+
} = useIncidentList(queryLive, [reload]);
|
|
92
|
+
|
|
93
|
+
const incidents = incidentsResponse?.incidents;
|
|
94
|
+
|
|
95
|
+
if (!entityFieldID) {
|
|
96
|
+
return <IncorrectConfigCard />;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (incidentsLoading || identityResponseLoading || !identityResponse) {
|
|
100
|
+
return <Progress />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const baseUrl = identityResponse.identity.dashboard_url;
|
|
76
104
|
|
|
77
105
|
const createIncidentLink: IconLinkVerticalProps = {
|
|
78
106
|
label: "Create incident",
|
|
@@ -88,20 +116,6 @@ export const EntityIncidentCard = ({
|
|
|
88
116
|
href: `${baseUrl}/incidents?${query.toString()}`,
|
|
89
117
|
};
|
|
90
118
|
|
|
91
|
-
const {
|
|
92
|
-
value: incidentsResponse,
|
|
93
|
-
loading: incidentsLoading,
|
|
94
|
-
error: incidentsError,
|
|
95
|
-
} = useAsync(async () => {
|
|
96
|
-
return await IncidentApi.request<
|
|
97
|
-
definitions["IncidentsV2ListResponseBody"]
|
|
98
|
-
>({
|
|
99
|
-
path: `/v2/incidents?${queryLive.toString()}`,
|
|
100
|
-
});
|
|
101
|
-
}, [reload]);
|
|
102
|
-
|
|
103
|
-
const incidents = incidentsResponse?.incidents;
|
|
104
|
-
|
|
105
119
|
return (
|
|
106
120
|
<Card>
|
|
107
121
|
<CardHeader
|
|
@@ -125,10 +139,12 @@ export const EntityIncidentCard = ({
|
|
|
125
139
|
/>
|
|
126
140
|
<Divider />
|
|
127
141
|
<CardContent>
|
|
128
|
-
{incidentsLoading && <Progress />}
|
|
129
142
|
{incidentsError && (
|
|
130
143
|
<Alert severity="error">{incidentsError.message}</Alert>
|
|
131
144
|
)}
|
|
145
|
+
{identityResponseError && (
|
|
146
|
+
<Alert severity="error">{identityResponseError.message}</Alert>
|
|
147
|
+
)}
|
|
132
148
|
{!incidentsLoading && !incidentsError && incidents && (
|
|
133
149
|
<>
|
|
134
150
|
{incidents && incidents.length > 0 && (
|
|
@@ -167,22 +183,6 @@ export const EntityIncidentCard = ({
|
|
|
167
183
|
);
|
|
168
184
|
};
|
|
169
185
|
|
|
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
186
|
// Find the ID of the custom field in incident that represents the association
|
|
187
187
|
// to this type of entity.
|
|
188
188
|
//
|
|
@@ -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: "active",
|
|
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,55 @@
|
|
|
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
|
+
|
|
10
|
+
export const HomePageIncidentCardContent = () => {
|
|
11
|
+
const config = useApi(configApiRef);
|
|
12
|
+
const baseUrl = config.getOptionalString('incident.baseUrl') || "https://app.incident.io";
|
|
13
|
+
|
|
14
|
+
const query = new URLSearchParams();
|
|
15
|
+
query.set(`status_category[one_of]`, "active");
|
|
16
|
+
const { loading, error, value } = useIncidentList(query);
|
|
17
|
+
const incidents = value?.incidents;
|
|
18
|
+
|
|
19
|
+
if (loading) return <Progress />;
|
|
20
|
+
if (error) return <Alert severity="error">{error.message}</Alert>;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{incidents && incidents.length > 0 && (
|
|
25
|
+
<Typography variant="subtitle1">
|
|
26
|
+
There are <strong>{incidents.length}</strong> ongoing incidents.
|
|
27
|
+
</Typography>
|
|
28
|
+
)}
|
|
29
|
+
{incidents && incidents.length === 0 && (
|
|
30
|
+
<Typography variant="subtitle1">No ongoing incidents.</Typography>
|
|
31
|
+
)}
|
|
32
|
+
<List dense>
|
|
33
|
+
{incidents?.map((incident) => {
|
|
34
|
+
return (
|
|
35
|
+
<IncidentListItem
|
|
36
|
+
key={incident.id}
|
|
37
|
+
incident={incident}
|
|
38
|
+
baseUrl={baseUrl}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</List>
|
|
43
|
+
<Typography variant="subtitle1">
|
|
44
|
+
Click to{" "}
|
|
45
|
+
<Link target="_blank" href={`${baseUrl}/incidents?${query.toString()}`}>
|
|
46
|
+
see more.
|
|
47
|
+
</Link>
|
|
48
|
+
</Typography>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Content = () => {
|
|
54
|
+
return <HomePageIncidentCardContent />;
|
|
55
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Content } from "./Content";
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { DateTime, Duration } from
|
|
17
|
-
import { BackstageTheme } from
|
|
16
|
+
import { DateTime, Duration } from "luxon";
|
|
17
|
+
import { BackstageTheme } from "@backstage/theme";
|
|
18
18
|
import {
|
|
19
19
|
Chip,
|
|
20
20
|
IconButton,
|
|
@@ -24,27 +24,27 @@ import {
|
|
|
24
24
|
Tooltip,
|
|
25
25
|
Typography,
|
|
26
26
|
makeStyles,
|
|
27
|
-
} from
|
|
28
|
-
import OpenInBrowserIcon from
|
|
29
|
-
import React from
|
|
30
|
-
import { definitions } from
|
|
27
|
+
} from "@material-ui/core";
|
|
28
|
+
import OpenInBrowserIcon from "@material-ui/icons/OpenInBrowser";
|
|
29
|
+
import React from "react";
|
|
30
|
+
import { definitions } from "../../api/types";
|
|
31
31
|
|
|
32
|
-
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
|
32
|
+
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
|
33
33
|
listItemPrimary: {
|
|
34
|
-
display:
|
|
35
|
-
fontWeight:
|
|
34
|
+
display: "flex", // vertically align with chip
|
|
35
|
+
fontWeight: "bold",
|
|
36
36
|
},
|
|
37
37
|
warning: {
|
|
38
38
|
borderColor: theme.palette.status.warning,
|
|
39
39
|
color: theme.palette.status.warning,
|
|
40
|
-
|
|
40
|
+
"& *": {
|
|
41
41
|
color: theme.palette.status.warning,
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
error: {
|
|
45
45
|
borderColor: theme.palette.status.error,
|
|
46
46
|
color: theme.palette.status.error,
|
|
47
|
-
|
|
47
|
+
"& *": {
|
|
48
48
|
color: theme.palette.status.error,
|
|
49
49
|
},
|
|
50
50
|
},
|
|
@@ -56,10 +56,10 @@ export const IncidentListItem = ({
|
|
|
56
56
|
incident,
|
|
57
57
|
}: {
|
|
58
58
|
baseUrl: string;
|
|
59
|
-
incident: definitions[
|
|
59
|
+
incident: definitions["IncidentV2ResponseBody"];
|
|
60
60
|
}) => {
|
|
61
61
|
const classes = useStyles();
|
|
62
|
-
const reportedAt = incident.incident_timestamp_values?.find(ts =>
|
|
62
|
+
const reportedAt = incident.incident_timestamp_values?.find((ts) =>
|
|
63
63
|
ts.incident_timestamp.name.match(/reported/i),
|
|
64
64
|
);
|
|
65
65
|
|
|
@@ -70,9 +70,9 @@ export const IncidentListItem = ({
|
|
|
70
70
|
new Date().getTime() - new Date(reportedAtDate).getTime();
|
|
71
71
|
const sinceReportedLabel = DateTime.local()
|
|
72
72
|
.minus(Duration.fromMillis(sinceReported))
|
|
73
|
-
.toRelative({ locale:
|
|
74
|
-
const lead = incident.incident_role_assignments.find(roleAssignment => {
|
|
75
|
-
return roleAssignment.role.role_type ===
|
|
73
|
+
.toRelative({ locale: "en" });
|
|
74
|
+
const lead = incident.incident_role_assignments.find((roleAssignment) => {
|
|
75
|
+
return roleAssignment.role.role_type === "lead";
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
return (
|
|
@@ -86,7 +86,7 @@ export const IncidentListItem = ({
|
|
|
86
86
|
size="small"
|
|
87
87
|
variant="outlined"
|
|
88
88
|
className={
|
|
89
|
-
[
|
|
89
|
+
["active"].includes(incident.incident_status.category)
|
|
90
90
|
? classes.error
|
|
91
91
|
: classes.warning
|
|
92
92
|
}
|
|
@@ -95,15 +95,15 @@ export const IncidentListItem = ({
|
|
|
95
95
|
</>
|
|
96
96
|
}
|
|
97
97
|
primaryTypographyProps={{
|
|
98
|
-
variant:
|
|
98
|
+
variant: "body1",
|
|
99
99
|
className: classes.listItemPrimary,
|
|
100
100
|
}}
|
|
101
101
|
secondary={
|
|
102
102
|
<Typography noWrap variant="body2" color="textSecondary">
|
|
103
|
-
Reported {sinceReportedLabel} and{
|
|
103
|
+
Reported {sinceReportedLabel} and{" "}
|
|
104
104
|
{lead?.assignee
|
|
105
105
|
? `${lead.assignee.name} is lead`
|
|
106
|
-
:
|
|
106
|
+
: "the lead is unassigned"}
|
|
107
107
|
.
|
|
108
108
|
</Typography>
|
|
109
109
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
};
|
|
23
|
+
|
|
24
|
+
export const useIdentity = () => {
|
|
25
|
+
const IncidentApi = useApi(IncidentApiRef);
|
|
26
|
+
|
|
27
|
+
const { value, loading, error } = useAsync(async () => {
|
|
28
|
+
return await IncidentApi.request<
|
|
29
|
+
definitions["UtilitiesV1IdentityResponseBody"]
|
|
30
|
+
>({
|
|
31
|
+
path: `/v1/identity`,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { value, loading, error };
|
|
36
|
+
};
|
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 {
|
|
16
|
+
export {
|
|
17
|
+
incidentPlugin,
|
|
18
|
+
EntityIncidentCard,
|
|
19
|
+
HomePageIncidentCard,
|
|
20
|
+
} from "./plugin";
|
package/src/plugin.ts
CHANGED
|
@@ -19,12 +19,13 @@ import {
|
|
|
19
19
|
createPlugin,
|
|
20
20
|
discoveryApiRef,
|
|
21
21
|
identityApiRef,
|
|
22
|
-
} from
|
|
22
|
+
} from "@backstage/core-plugin-api";
|
|
23
|
+
import {CardExtensionProps, createCardExtension} from "@backstage/plugin-home-react";
|
|
23
24
|
|
|
24
|
-
import { IncidentApi, IncidentApiRef } from
|
|
25
|
+
import { IncidentApi, IncidentApiRef } from "./api/client";
|
|
25
26
|
|
|
26
27
|
export const incidentPlugin = createPlugin({
|
|
27
|
-
id:
|
|
28
|
+
id: "incident",
|
|
28
29
|
apis: [
|
|
29
30
|
createApiFactory({
|
|
30
31
|
api: IncidentApiRef,
|
|
@@ -41,12 +42,20 @@ export const incidentPlugin = createPlugin({
|
|
|
41
42
|
|
|
42
43
|
export const EntityIncidentCard = incidentPlugin.provide(
|
|
43
44
|
createComponentExtension({
|
|
44
|
-
name:
|
|
45
|
+
name: "EntityIncidentCard",
|
|
45
46
|
component: {
|
|
46
47
|
lazy: () =>
|
|
47
|
-
import(
|
|
48
|
-
m => m.EntityIncidentCard,
|
|
48
|
+
import("./components/EntityIncidentCard").then(
|
|
49
|
+
(m) => m.EntityIncidentCard,
|
|
49
50
|
),
|
|
50
51
|
},
|
|
51
52
|
}),
|
|
52
53
|
);
|
|
54
|
+
|
|
55
|
+
export const HomePageIncidentCard: (props: CardExtensionProps<unknown>) => React.JSX.Element = incidentPlugin.provide(
|
|
56
|
+
createCardExtension({
|
|
57
|
+
name: "HomePageIncidentCard",
|
|
58
|
+
title: "Ongoing Incidents",
|
|
59
|
+
components: () => import("./components/HomePageIncidentCard"),
|
|
60
|
+
}),
|
|
61
|
+
);
|
package/src/setupTests.ts
CHANGED
|
@@ -1,188 +0,0 @@
|
|
|
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-14b311c0.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
|
-
if (!entityFieldID) {
|
|
105
|
-
return /* @__PURE__ */ React.createElement(IncorrectConfigCard, null);
|
|
106
|
-
}
|
|
107
|
-
const entityID = `${entity.metadata.namespace}/${entity.metadata.name}`;
|
|
108
|
-
const query = new URLSearchParams();
|
|
109
|
-
query.set(`custom_field[${entityFieldID}][one_of]`, entityID);
|
|
110
|
-
const queryLive = new URLSearchParams(query);
|
|
111
|
-
queryLive.set(`status_category[one_of]`, "live");
|
|
112
|
-
const createIncidentLink = {
|
|
113
|
-
label: "Create incident",
|
|
114
|
-
disabled: false,
|
|
115
|
-
icon: /* @__PURE__ */ React.createElement(WhatshotIcon, null),
|
|
116
|
-
href: `${baseUrl}/incidents/create`
|
|
117
|
-
};
|
|
118
|
-
const viewIncidentsLink = {
|
|
119
|
-
label: "View past incidents",
|
|
120
|
-
disabled: false,
|
|
121
|
-
icon: /* @__PURE__ */ React.createElement(HistoryIcon, null),
|
|
122
|
-
href: `${baseUrl}/incidents?${query.toString()}`
|
|
123
|
-
};
|
|
124
|
-
const {
|
|
125
|
-
value: incidentsResponse,
|
|
126
|
-
loading: incidentsLoading,
|
|
127
|
-
error: incidentsError
|
|
128
|
-
} = useAsync(async () => {
|
|
129
|
-
return await IncidentApi.request({
|
|
130
|
-
path: `/v2/incidents?${queryLive.toString()}`
|
|
131
|
-
});
|
|
132
|
-
}, [reload]);
|
|
133
|
-
const incidents = incidentsResponse == null ? void 0 : incidentsResponse.incidents;
|
|
134
|
-
return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(
|
|
135
|
-
CardHeader,
|
|
136
|
-
{
|
|
137
|
-
title: "incident.io",
|
|
138
|
-
action: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
139
|
-
IconButton,
|
|
140
|
-
{
|
|
141
|
-
component: Link,
|
|
142
|
-
"aria-label": "Refresh",
|
|
143
|
-
disabled: false,
|
|
144
|
-
title: "Refresh",
|
|
145
|
-
onClick: () => setReload(!reload)
|
|
146
|
-
},
|
|
147
|
-
/* @__PURE__ */ React.createElement(CachedIcon, null)
|
|
148
|
-
)),
|
|
149
|
-
subheader: /* @__PURE__ */ React.createElement(HeaderIconLinkRow, { links: [createIncidentLink, viewIncidentsLink] })
|
|
150
|
-
}
|
|
151
|
-
), /* @__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) => {
|
|
152
|
-
return /* @__PURE__ */ React.createElement(
|
|
153
|
-
IncidentListItem,
|
|
154
|
-
{
|
|
155
|
-
key: incident.id,
|
|
156
|
-
incident,
|
|
157
|
-
baseUrl
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
})), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, "Click to", " ", /* @__PURE__ */ React.createElement(
|
|
161
|
-
Link,
|
|
162
|
-
{
|
|
163
|
-
target: "_blank",
|
|
164
|
-
href: `${baseUrl}/incidents?${queryLive.toString()}`
|
|
165
|
-
},
|
|
166
|
-
"see more."
|
|
167
|
-
)))));
|
|
168
|
-
};
|
|
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
|
-
function getEntityFieldID(config, entity) {
|
|
173
|
-
switch (entity.kind) {
|
|
174
|
-
case "API":
|
|
175
|
-
return config.getOptional("incident.fields.api");
|
|
176
|
-
case "Component":
|
|
177
|
-
return config.getOptional("incident.fields.component");
|
|
178
|
-
case "Domain":
|
|
179
|
-
return config.getOptional("incident.fields.domain");
|
|
180
|
-
case "System":
|
|
181
|
-
return config.getOptional("incident.fields.system");
|
|
182
|
-
default:
|
|
183
|
-
throw new Error(`unrecognised entity kind: ${entity.kind}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export { EntityIncidentCard };
|
|
188
|
-
//# sourceMappingURL=index-08133e09.esm.js.map
|