fix issues after first review for iLert plugin

Signed-off-by: yacut <roman.frey2020@gmail.com>
This commit is contained in:
yacut
2021-05-07 11:05:20 +02:00
parent 40ce1954f1
commit 78a71f46ae
35 changed files with 312 additions and 299 deletions
+19 -19
View File
@@ -31,12 +31,6 @@ cd packages/app
yarn add @backstage/plugin-ilert
```
Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin:
```js
export { plugin as ILert } from '@backstage/plugin-ilert';
```
Add it to the `EntityPage.tsx`:
```ts
@@ -44,13 +38,16 @@ import {
isPluginApplicableToEntity as isILertAvailable,
EntityILertCard,
} from '@backstage/plugin-ilert';
{
isILertAvailable(entity) && (
<Grid item md={6}>
// ...
<EntitySwitch>
<EntitySwitch.Case if={isILertAvailable}>
<Grid item sm={6}>
<EntityILertCard />
</Grid>
);
}
</EntitySwitch.Case>
</EntitySwitch>;
// ...
```
> To force an iLert card for each entity just add the `<EntityILertCard />` component. An instruction card will appear if no integration key is set.
@@ -61,11 +58,11 @@ Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blo
```tsx
import { ILertPage } from '@backstage/plugin-ilert';
<Routes>
<FlatRoutes>
// ...
<Route path="/ilert" element={<ILertPage />} />
// ...
</Routes>;
</FlatRoutes>;
```
Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported by the plugin - for example:
@@ -104,14 +101,13 @@ proxy:
allowedMethods: ['GET', 'POST', 'PUT']
allowedHeaders: ['Authorization']
headers:
Authorization:
$env: ILERT_AUTH_HEADER
Authorization: ${ILERT_AUTH_HEADER}
```
Then start the backend, passing the token as environment variable:
Then start the backend, passing the authorization header (bearer token or basic auth) as environment variable:
```bash
$ ILERT_AUTH_HEADER='Basic <TOKEN>' yarn start
$ ILERT_AUTH_HEADER='<ILERT_AUTH>' yarn start
```
## Integration Key
@@ -123,6 +119,10 @@ The information displayed for each entity is based on the alert source integrati
If you want to use this plugin for an entity, you need to label it with the below annotation:
```yml
annotations:
ilert.com/integration-key: [INTEGRATION_KEY]
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example
annotations:
ilert.com/integration-key: [INTEGRATION_KEY]
```
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
ilert: {
/**
* Domain used by users to access iLert web UI.
* Example: https://my-app.ilert.com/
* @visibility frontend
*/
baseUrl: string;
/**
* Path to use for requests via the proxy, defaults to /ilert/api
* @visibility frontend
*/
proxyPath?: string;
};
}
+8 -6
View File
@@ -22,16 +22,16 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.6",
"@backstage/core": "^0.7.7",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.6",
"@date-io/date-fns": "1.x",
"@date-io/luxon": "1.x",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@material-ui/pickers": "^3.3.10",
"date-fns": "^2.20.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"humanize-duration": "^3.26.0",
"luxon": "^1.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
@@ -49,6 +49,8 @@
"msw": "^0.21.2"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+88 -142
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core';
import { AuthenticationError, ResponseError } from '@backstage/errors';
import {
AlertSource,
EscalationPolicy,
@@ -32,8 +33,7 @@ import {
Options,
EventRequest,
} from './types';
import moment from 'moment';
import momentTimezone from 'moment-timezone';
import { DateTime as dt } from 'luxon';
export const ilertApiRef = createApiRef<ILertApi>({
id: 'plugin.ilert.service',
@@ -41,7 +41,10 @@ export const ilertApiRef = createApiRef<ILertApi>({
});
const DEFAULT_PROXY_PATH = '/ilert';
export class UnauthorizedError extends Error {}
const JSON_HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
export class ILertClient implements ILertApi {
private readonly discoveryApi: DiscoveryApi;
@@ -55,14 +58,15 @@ export class ILertClient implements ILertApi {
return new ILertClient({
discoveryApi: discoveryApi,
baseUrl,
proxyPath: configApi.getOptionalString('ilert.proxyPath'),
proxyPath:
configApi.getOptionalString('ilert.proxyPath') ?? DEFAULT_PROXY_PATH,
});
}
constructor(opts: Options) {
this.discoveryApi = opts.discoveryApi;
this.baseUrl = opts.baseUrl;
this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;
this.proxyPath = opts.proxyPath;
}
private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> {
@@ -70,40 +74,37 @@ export class ILertClient implements ILertApi {
const response = await fetch(`${apiUrl}${input}`, init);
if (response.status === 401) {
throw new UnauthorizedError('');
}
if (!response.ok) {
throw new Error(
`Request failed with ${response.status} ${response.statusText}`,
throw new AuthenticationError(
'This request requires HTTP authentication.',
);
}
if (!response.ok || response.status >= 400) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const query = new URLSearchParams();
if (opts && opts.maxResults) {
query.append('max-results', `${opts.maxResults}`);
if (opts?.maxResults !== undefined) {
query.append('max-results', String(opts.maxResults));
}
if (opts && opts.startIndex) {
query.append('start-index', `${opts.startIndex}`);
if (opts?.startIndex !== undefined) {
query.append('start-index', String(opts.startIndex));
}
if (opts && opts.alertSources) {
if (opts?.alertSources !== undefined && Array.isArray(opts.alertSources)) {
opts.alertSources.forEach((a: number) => {
if (a) {
query.append('alert-source', `${a}`);
query.append('alert-source', String(a));
}
});
}
if (opts && opts.states && Array.isArray(opts.states)) {
if (opts?.states !== undefined && Array.isArray(opts.states)) {
opts.states.forEach(state => {
query.append('state', state);
});
@@ -118,10 +119,7 @@ export class ILertClient implements ILertApi {
async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const query = new URLSearchParams();
if (opts && opts.states && Array.isArray(opts.states)) {
@@ -137,14 +135,21 @@ export class ILertClient implements ILertApi {
return response && response.count ? response.count : 0;
}
async fetchIncident(id: number): Promise<Incident> {
const init = {
headers: JSON_HEADERS,
};
const response = await this.fetch(`/api/v1/incidents/${id}`, init);
return response;
}
async fetchIncidentResponders(
incident: Incident,
): Promise<IncidentResponder[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch(
@@ -157,10 +162,7 @@ export class ILertClient implements ILertApi {
async fetchIncidentActions(incident: Incident): Promise<IncidentAction[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch(
@@ -171,40 +173,42 @@ export class ILertClient implements ILertApi {
return response;
}
async acceptIncident(incident: Incident): Promise<Incident> {
async acceptIncident(
incident: Incident,
userName: string,
): Promise<Incident> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ summary: 'Backstage — iLert plugin' }),
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({
apiKey: incident.alertSource?.integrationKey || '',
incidentKey: incident.incidentKey,
summary: `from ${userName} via Backstage plugin`,
eventType: 'ACCEPT',
}),
};
const response = await this.fetch(
`/api/v1/incidents/${incident.id}/accept`,
init,
);
return response;
await this.fetch('/api/v1/events', init);
return this.fetchIncident(incident.id);
}
async resolveIncident(incident: Incident): Promise<Incident> {
async resolveIncident(
incident: Incident,
userName: string,
): Promise<Incident> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ summary: 'Backstage — iLert plugin' }),
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({
apiKey: incident.alertSource?.integrationKey || '',
incidentKey: incident.incidentKey,
summary: `from ${userName} via Backstage plugin`,
eventType: 'RESOLVE',
}),
};
const response = await this.fetch(
`/api/v1/incidents/${incident.id}/resolve`,
init,
);
return response;
await this.fetch('/api/v1/events', init);
return this.fetchIncident(incident.id);
}
async assignIncident(
@@ -213,22 +217,19 @@ export class ILertClient implements ILertApi {
): Promise<Incident> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const query = new URLSearchParams();
switch (responder.group) {
case 'ESCALATION_POLICY':
query.append('policy-id', `${responder.id}`);
query.append('policy-id', String(responder.id));
break;
case 'ON_CALL_SCHEDULE':
query.append('schedule-id', `${responder.id}`);
query.append('schedule-id', String(responder.id));
break;
default:
query.append('user-id', `${responder.id}`);
query.append('user-id', String(responder.id));
break;
}
@@ -246,10 +247,7 @@ export class ILertClient implements ILertApi {
): Promise<void> {
const init = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({
webhookId: action.webhookId,
extensionId: action.extensionId,
@@ -264,10 +262,7 @@ export class ILertClient implements ILertApi {
async createIncident(eventRequest: EventRequest): Promise<boolean> {
const init = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({
apiKey: eventRequest.integrationKey,
summary: eventRequest.summary,
@@ -286,15 +281,12 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch('/api/v1/events', init);
return response.responseCode === 'NEW_INCIDENT_CREATED';
return response;
}
async fetchUptimeMonitors(): Promise<UptimeMonitor[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/uptime-monitors', init);
@@ -304,10 +296,7 @@ export class ILertClient implements ILertApi {
async fetchUptimeMonitor(id: number): Promise<UptimeMonitor> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response: UptimeMonitor = await this.fetch(
@@ -323,10 +312,7 @@ export class ILertClient implements ILertApi {
): Promise<UptimeMonitor> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({ ...uptimeMonitor, paused: true }),
};
@@ -343,10 +329,7 @@ export class ILertClient implements ILertApi {
): Promise<UptimeMonitor> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({ ...uptimeMonitor, paused: false }),
};
@@ -360,10 +343,7 @@ export class ILertClient implements ILertApi {
async fetchAlertSources(): Promise<AlertSource[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/alert-sources', init);
@@ -375,10 +355,7 @@ export class ILertClient implements ILertApi {
idOrIntegrationKey: number | string,
): Promise<AlertSource> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch(
@@ -391,16 +368,13 @@ export class ILertClient implements ILertApi {
async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch(
`/api/v1/on-calls?policies=${
alertSource.escalationPolicy.id
}&expand=user&expand=escalationPolicy&timezone=${momentTimezone.tz.guess()}`,
}&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`,
init,
);
@@ -410,10 +384,7 @@ export class ILertClient implements ILertApi {
async enableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({ ...alertSource, active: true }),
};
@@ -428,10 +399,7 @@ export class ILertClient implements ILertApi {
async disableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({ ...alertSource, active: false }),
};
@@ -449,16 +417,13 @@ export class ILertClient implements ILertApi {
): Promise<void> {
const init = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({
start: moment().utc().toISOString(),
end: moment().add(minutes, 'minutes').utc().toISOString(),
start: dt.utc().toISO(),
end: dt.utc().plus({ minutes }).toISO(),
description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`,
createdBy: 'Backstage',
timezone: momentTimezone.tz.guess(),
timezone: dt.local().zoneName,
alertSources: [{ id: alertSourceId }],
}),
};
@@ -470,10 +435,7 @@ export class ILertClient implements ILertApi {
async fetchOnCallSchedules(): Promise<Schedule[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/schedules', init);
@@ -483,10 +445,7 @@ export class ILertClient implements ILertApi {
async fetchUsers(): Promise<User[]> {
const init = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/users', init);
@@ -502,10 +461,7 @@ export class ILertClient implements ILertApi {
): Promise<Schedule> {
const init = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
headers: JSON_HEADERS,
body: JSON.stringify({ user: { id: userId }, start, end }),
};
@@ -541,17 +497,7 @@ export class ILertClient implements ILertApi {
}
getUserPhoneNumber(user: User | null) {
if (!user) {
return '';
}
if (user.mobile) {
return user.mobile.number;
}
if (user.landline) {
return user.landline.number;
}
return '';
return user?.mobile?.number || user?.landline?.number || '';
}
getUserInitials(user: User | null) {
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export { ILertClient, ilertApiRef, UnauthorizedError } from './client';
export { ILertClient, ilertApiRef } from './client';
export type {
ILertApi,
GetIncidentsCountOpts,
+4 -3
View File
@@ -54,10 +54,11 @@ export type EventRequest = {
export interface ILertApi {
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
fetchIncident(id: number): Promise<Incident>;
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
acceptIncident(incident: Incident): Promise<Incident>;
resolveIncident(incident: Incident): Promise<Incident>;
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
assignIncident(
incident: Incident,
responder: IncidentResponder,
@@ -115,5 +116,5 @@ export type Options = {
/**
* Path to use for requests via the proxy, defaults to /ilert/api
*/
proxyPath?: string;
proxyPath: string;
};
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core';
import Link from '@material-ui/core/Link';
import { useApi, Link } from '@backstage/core';
import Grid from '@material-ui/core/Grid';
import { AlertSource } from '../../types';
import { ilertApiRef } from '../../api';
@@ -61,7 +60,7 @@ export const AlertSourceLink = ({
<Grid item xs={10}>
<Link
className={classes.link}
href={ilertApi.getAlertSourceDetailsURL(alertSource)}
to={ilertApi.getAlertSourceDetailsURL(alertSource)}
>
{alertSource.name}
</Link>
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core';
import Link from '@material-ui/core/Link';
import { useApi, Link } from '@backstage/core';
import { makeStyles } from '@material-ui/core/styles';
import { EscalationPolicy } from '../../types';
import { ilertApiRef } from '../../api';
@@ -41,7 +40,7 @@ export const EscalationPolicyLink = ({
return (
<Link
className={classes.link}
href={ilertApi.getEscalationPolicyDetailsURL(escalationPolicy)}
to={ilertApi.getEscalationPolicyDetailsURL(escalationPolicy)}
>
{escalationPolicy.name}
</Link>
@@ -15,14 +15,14 @@
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { UnauthorizedError } from '../../api';
import Alert from '@material-ui/lab/Alert';
import { AuthenticationError } from '@backstage/errors';
import { ResponseErrorPanel } from '@backstage/core';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import Divider from '@material-ui/core/Divider';
import { makeStyles } from '@material-ui/core/styles';
import { ILERT_INTEGRATION_KEY } from '../../constants';
import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../../constants';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useIncidents } from '../../hooks/useIncidents';
import { IncidentsTable } from '../IncidentsPage';
@@ -36,7 +36,7 @@ import { ILertCardEmptyState } from './ILertCardEmptyState';
import { ILertCardOnCall } from './ILertCardOnCall';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]);
Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION]);
const useStyles = makeStyles({
content: {
@@ -79,15 +79,11 @@ export const ILertCard = () => {
] = React.useState(false);
if (error) {
if (error instanceof UnauthorizedError) {
if (error instanceof AuthenticationError) {
return <MissingAuthorizationHeaderError />;
}
return (
<Alert data-testid="error-message" severity="error">
{error.message}
</Alert>
);
return <ResponseErrorPanel error={error} />;
}
if (!integrationKey) {
@@ -40,14 +40,12 @@ spec:
const useStyles = makeStyles<BackstageTheme>(theme => ({
code: {
borderRadius: 6,
margin: `${theme.spacing(2)}px 0px`,
margin: theme.spacing(2, 0),
background: theme.palette.type === 'dark' ? '#444' : '#fff',
},
header: {
display: 'inline-block',
padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing(
2,
)}px ${theme.spacing(2.5)}px`,
padding: theme.spacing(2, 2, 2, 2.5),
},
}));
@@ -29,7 +29,7 @@ import { makeStyles } from '@material-ui/core/styles';
import { OnCall } from '../../types';
import { useApi } from '@backstage/core';
import { ilertApiRef } from '../../api';
import moment from 'moment';
import { DateTime as dt } from 'luxon';
const useStyles = makeStyles({
listItemPrimary: {
@@ -123,8 +123,8 @@ export const ILertCardOnCallItem = ({
<Tooltip
title={
'On call shift ' +
`${moment(onCall.start).format('D MMM, HH:mm')} - ` +
`${moment(onCall.end).format('D MMM, HH:mm')}`
`${dt.fromISO(onCall.start).toFormat('D MMM, HH:mm')} - ` +
`${dt.fromISO(onCall.end).toFormat('D MMM, HH:mm')}`
}
placement="top-start"
>
@@ -14,9 +14,14 @@
* limitations under the License.
*/
import React from 'react';
import { alertApiRef, Progress, useApi } from '@backstage/core';
import {
alertApiRef,
Progress,
useApi,
identityApiRef,
Link,
} from '@backstage/core';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import Link from '@material-ui/core/Link';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { ilertApiRef } from '../../api';
import { Incident, IncidentAction } from '../../types';
@@ -34,6 +39,8 @@ export const IncidentActionsMenu = ({
}) => {
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const callback = onIncidentChanged || ((_: Incident): void => {});
const setProcessing = setIsLoading || ((_: boolean): void => {});
@@ -59,7 +66,7 @@ export const IncidentActionsMenu = ({
try {
handleCloseMenu();
setProcessing(true);
const newIncident = await ilertApi.acceptIncident(incident);
const newIncident = await ilertApi.acceptIncident(incident, userName);
alertApi.post({ message: 'Incident accepted.' });
callback(newIncident);
@@ -74,7 +81,7 @@ export const IncidentActionsMenu = ({
try {
handleCloseMenu();
setProcessing(true);
const newIncident = await ilertApi.resolveIncident(incident);
const newIncident = await ilertApi.resolveIncident(incident, userName);
alertApi.post({ message: 'Incident resolved.' });
callback(newIncident);
@@ -179,7 +186,7 @@ export const IncidentActionsMenu = ({
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link href={ilertApi.getIncidentDetailsURL(incident)}>
<Link to={ilertApi.getIncidentDetailsURL(incident)}>
View in iLert
</Link>
</Typography>
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core';
import Link from '@material-ui/core/Link';
import { useApi, Link } from '@backstage/core';
import { makeStyles } from '@material-ui/core/styles';
import { Incident } from '../../types';
import { ilertApiRef } from '../../api';
@@ -37,7 +36,7 @@ export const IncidentLink = ({ incident }: { incident: Incident | null }) => {
return (
<Link
className={classes.link}
href={ilertApi.getIncidentDetailsURL(incident)}
to={ilertApi.getIncidentDetailsURL(incident)}
>
#{incident.id}
</Link>
@@ -98,17 +98,15 @@ export const IncidentNewModal = ({
setIsLoading(true);
setTimeout(async () => {
try {
const success = await ilertApi.createIncident({
await ilertApi.createIncident({
integrationKey,
summary,
details,
userName,
source,
});
if (success) {
alertApi.post({ message: 'Incident created.' });
refetchIncidents();
}
alertApi.post({ message: 'Incident created.' });
refetchIncidents();
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
@@ -14,11 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { Content, ContentHeader, SupportButton } from '@backstage/core';
import {
Content,
ContentHeader,
SupportButton,
ResponseErrorPanel,
} from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/Add';
import { UnauthorizedError } from '../../api';
import Alert from '@material-ui/lab/Alert';
import { IncidentsTable } from './IncidentsTable';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useIncidents } from '../../hooks/useIncidents';
@@ -44,14 +48,18 @@ export const IncidentsPage = () => {
};
if (error) {
if (error instanceof UnauthorizedError) {
return <MissingAuthorizationHeaderError />;
if (error instanceof AuthenticationError) {
return (
<Content>
<MissingAuthorizationHeaderError />
</Content>
);
}
return (
<Alert data-testid="error-message" severity="error">
{error.message}
</Alert>
<Content>
<ResponseErrorPanel error={error} />
</Content>
);
}
@@ -22,7 +22,8 @@ import { StatusChip } from './StatusChip';
import { AlertSourceLink } from '../AlertSource/AlertSourceLink';
import { TableTitle } from './TableTitle';
import Typography from '@material-ui/core/Typography';
import moment from 'moment';
import { DateTime as dt, Interval } from 'luxon';
import humanizeDuration from 'humanize-duration';
import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu';
import { IncidentLink } from '../Incident/IncidentLink';
@@ -116,16 +117,24 @@ export const IncidentsTable = ({
render: rowData => (
<Typography noWrap>
{(rowData as Incident).status !== 'RESOLVED'
? moment
.duration(moment((rowData as Incident).reportTime).diff(moment()))
.humanize()
: moment
.duration(
moment((rowData as Incident).reportTime).diff(
moment((rowData as Incident).resolvedOn),
),
? humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as Incident).reportTime),
dt.now(),
)
.humanize()}
.toDuration()
.valueOf(),
{ units: ['h', 'm', 's'], largest: 2, round: true },
)
: humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as Incident).reportTime),
dt.fromISO((rowData as Incident).resolvedOn),
)
.toDuration()
.valueOf(),
{ units: ['h', 'm', 's'], largest: 2, round: true },
)}
</Typography>
),
};
@@ -14,13 +14,12 @@
* limitations under the License.
*/
import React from 'react';
import { useApi, ItemCardGrid, Progress } from '@backstage/core';
import { useApi, ItemCardGrid, Progress, Link } from '@backstage/core';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import Typography from '@material-ui/core/Typography';
import Link from '@material-ui/core/Link';
import { Schedule } from '../../types';
import { ilertApiRef } from '../../api';
import { OnCallShiftItem } from './OnCallShiftItem';
@@ -132,7 +131,7 @@ export const OnCallSchedulesGrid = ({
classes={{ content: classes.cardHeader }}
title={
<Link
href={ilertApi.getScheduleDetailsURL(schedule)}
to={ilertApi.getScheduleDetailsURL(schedule)}
className={classes.link}
>
{schedule.name}
@@ -14,9 +14,13 @@
* limitations under the License.
*/
import React from 'react';
import { Content, ContentHeader, SupportButton } from '@backstage/core';
import { UnauthorizedError } from '../../api';
import Alert from '@material-ui/lab/Alert';
import {
Content,
ContentHeader,
SupportButton,
ResponseErrorPanel,
} from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { OnCallSchedulesGrid } from './OnCallSchedulesGrid';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useOnCallSchedules } from '../../hooks/useOnCallSchedules';
@@ -28,14 +32,18 @@ export const OnCallSchedulesPage = () => {
] = useOnCallSchedules();
if (error) {
if (error instanceof UnauthorizedError) {
return <MissingAuthorizationHeaderError />;
if (error instanceof AuthenticationError) {
return (
<Content>
<MissingAuthorizationHeaderError />
</Content>
);
}
return (
<Alert data-testid="error-message" severity="error">
{error.message}
</Alert>
<Content>
<ResponseErrorPanel error={error} />
</Content>
);
}
@@ -19,7 +19,7 @@ import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import RepeatIcon from '@material-ui/icons/Repeat';
import { Shift } from '../../types';
import moment from 'moment';
import { DateTime as dt } from 'luxon';
import { makeStyles } from '@material-ui/core/styles';
import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal';
@@ -77,9 +77,9 @@ export const OnCallShiftItem = ({
) : null}
<Grid item sm={12}>
<Typography variant="subtitle2" color="textSecondary">
{`${moment(shift.start).format('D MMM, HH:mm')} - ${moment(
shift.end,
).format('D MMM, HH:mm')}`}
{`${dt.fromISO(shift.start).toFormat('D MMM, HH:mm')} - ${dt
.fromISO(shift.end)
.toFormat('D MMM, HH:mm')}`}
</Typography>
</Grid>
<Grid item sm={12}>
@@ -28,7 +28,7 @@ import { ilertApiRef } from '../../api';
import { useShiftOverride } from '../../hooks/useShiftOverride';
import { Shift } from '../../types';
import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
import LuxonUtils from '@date-io/luxon';
const useStyles = makeStyles(() => ({
container: {
@@ -120,7 +120,7 @@ export const ShiftOverrideModal = ({
>
<DialogTitle id="override-shift-form-title">Shift override</DialogTitle>
<DialogContent>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<MuiPickersUtilsProvider utils={LuxonUtils}>
<Autocomplete
disabled={isLoading}
options={users}
@@ -161,7 +161,7 @@ export const ShiftOverrideModal = ({
value={start}
className={classes.formControl}
onChange={date => {
setStart(date ? date.toISOString() : '');
setStart(date ? date.toISO() : '');
}}
/>
<DateTimePicker
@@ -173,7 +173,7 @@ export const ShiftOverrideModal = ({
value={end}
className={classes.formControl}
onChange={date => {
setEnd(date ? date.toISOString() : '');
setEnd(date ? date.toISO() : '');
}}
/>
</MuiPickersUtilsProvider>
@@ -14,9 +14,8 @@
* limitations under the License.
*/
import React from 'react';
import { alertApiRef, useApi } from '@backstage/core';
import { alertApiRef, useApi, Link } from '@backstage/core';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import Link from '@material-ui/core/Link';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { ilertApiRef } from '../../api';
@@ -117,13 +116,15 @@ export const UptimeMonitorActionsMenu = ({
<MenuItem key="report" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link onClick={handleOpenReport}>View Report</Link>
<Link to="#" onClick={handleOpenReport}>
View Report
</Link>
</Typography>
</MenuItem>
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link href={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}>
<Link to={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}>
View in iLert
</Link>
</Typography>
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core';
import Link from '@material-ui/core/Link';
import { useApi, Link } from '@backstage/core';
import { makeStyles } from '@material-ui/core/styles';
import { UptimeMonitor } from '../../types';
import { ilertApiRef } from '../../api';
@@ -41,7 +40,7 @@ export const UptimeMonitorLink = ({
return (
<Link
className={classes.link}
href={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}
to={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}
>
#{uptimeMonitor.id}
</Link>
@@ -14,9 +14,13 @@
* limitations under the License.
*/
import React from 'react';
import { Content, ContentHeader, SupportButton } from '@backstage/core';
import { UnauthorizedError } from '../../api';
import Alert from '@material-ui/lab/Alert';
import {
Content,
ContentHeader,
SupportButton,
ResponseErrorPanel,
} from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { UptimeMonitorsTable } from './UptimeMonitorsTable';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useUptimeMonitors } from '../../hooks/useUptimeMonitors';
@@ -28,14 +32,18 @@ export const UptimeMonitorsPage = () => {
] = useUptimeMonitors();
if (error) {
if (error instanceof UnauthorizedError) {
return <MissingAuthorizationHeaderError />;
if (error instanceof AuthenticationError) {
return (
<Content>
<MissingAuthorizationHeaderError />
</Content>
);
}
return (
<Alert data-testid="error-message" severity="error">
{error.message}
</Alert>
<Content>
<ResponseErrorPanel error={error} />
</Content>
);
}
@@ -20,7 +20,8 @@ import { TableState } from '../../api';
import { UptimeMonitor } from '../../types';
import { StatusChip } from './StatusChip';
import Typography from '@material-ui/core/Typography';
import moment from 'moment';
import { DateTime as dt, Interval } from 'luxon';
import humanizeDuration from 'humanize-duration';
import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink';
import { UptimeMonitorCheckType } from './UptimeMonitorCheckType';
import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu';
@@ -99,13 +100,15 @@ export const UptimeMonitorsTable = ({
headerStyle: mdColumnStyle,
render: rowData => (
<Typography noWrap>
{moment
.duration(
moment((rowData as UptimeMonitor).lastStatusChange).diff(
moment(),
),
{humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as UptimeMonitor).lastStatusChange),
dt.now(),
)
.humanize()}
.toDuration()
.valueOf(),
{ units: ['h', 'm', 's'], largest: 2, round: true },
)}
</Typography>
),
},
+6 -3
View File
@@ -16,13 +16,16 @@
import { useEntity } from '@backstage/plugin-catalog-react';
import { ILERT_INTEGRATION_KEY } from '../constants';
import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../constants';
export function useILertEntity() {
const { entity } = useEntity();
const integrationKey =
entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || '';
entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION] || '';
const name = entity.metadata.name;
const identifier = `${entity.kind}:${
entity.metadata.namespace || 'default'
}/${entity.metadata.name}`;
return { integrationKey, name };
return { integrationKey, name, identifier };
}
+4 -3
View File
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { AlertSource, UptimeMonitor } from '../types';
@@ -46,7 +47,7 @@ export const useAlertSource = (integrationKey: string) => {
setIsAlertSourceLoading(false);
} catch (e) {
setIsAlertSourceLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
@@ -69,7 +70,7 @@ export const useAlertSource = (integrationKey: string) => {
setIsUptimeMonitorLoading(false);
} catch (e) {
setIsUptimeMonitorLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { AlertSource, OnCall } from '../types';
@@ -37,7 +38,7 @@ export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => {
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
+3 -2
View File
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { Incident, IncidentResponder } from '../types';
@@ -49,7 +50,7 @@ export const useAssignIncident = (incident: Incident | null, open: boolean) => {
setIncidentRespondersList(data);
}
} catch (e) {
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { Incident, IncidentAction } from '../types';
@@ -39,7 +40,7 @@ export const useIncidentActions = (
const data = await ilertApi.fetchIncidentActions(incident);
setIncidentActionsList(data);
} catch (e) {
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
+4 -8
View File
@@ -14,13 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import {
GetIncidentsOpts,
ilertApiRef,
TableState,
UnauthorizedError,
} from '../api';
import { GetIncidentsOpts, ilertApiRef, TableState } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import {
ACCEPTED,
@@ -68,7 +64,7 @@ export const useIncidents = (
setIncidentsList(data || []);
setIsLoading(false);
} catch (e) {
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
setIsLoading(false);
@@ -81,7 +77,7 @@ export const useIncidents = (
const count = await ilertApi.fetchIncidentsCount({ states });
setIncidentsCount(count || 0);
} catch (e) {
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
+3 -2
View File
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { AlertSource } from '../types';
@@ -44,7 +45,7 @@ export const useNewIncident = (
const count = await ilertApi.fetchAlertSources();
setAlertSourcesList(count || 0);
} catch (e) {
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { Schedule } from '../types';
@@ -36,7 +37,7 @@ export const useOnCallSchedules = () => {
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
+3 -2
View File
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, UnauthorizedError } from '../api';
import { ilertApiRef } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { User, Shift } from '../types';
@@ -38,7 +39,7 @@ export const useShiftOverride = (s: Shift, isModalOpened: boolean) => {
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
+3 -2
View File
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, TableState, UnauthorizedError } from '../api';
import { ilertApiRef, TableState } from '../api';
import { useApi, errorApiRef } from '@backstage/core';
import { AuthenticationError } from '@backstage/errors';
import { useAsyncRetry } from 'react-use';
import { UptimeMonitor } from '../types';
@@ -40,7 +41,7 @@ export const useUptimeMonitors = () => {
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (!(e instanceof UnauthorizedError)) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;