add incident actions to ilert plugin
Signed-off-by: yacut <roman.rogozhnikov@gmail.com>
This commit is contained in:
@@ -17,6 +17,9 @@ This plugin provides:
|
||||
- A list of incidents
|
||||
- A way to trigger a new incident
|
||||
- A way to reassign/acknowledge/resolve an incident
|
||||
- A way to trigger an incident action
|
||||
- A way to trigger an immediate maintenance
|
||||
- A way to disable/enable an alert source
|
||||
- A list of uptime monitors
|
||||
|
||||
## Setup instructions
|
||||
@@ -41,7 +44,6 @@ import {
|
||||
isPluginApplicableToEntity as isILertAvailable,
|
||||
EntityILertCard,
|
||||
} from '@backstage/plugin-ilert';
|
||||
// add to code
|
||||
{
|
||||
isILertAvailable(entity) && (
|
||||
<Grid item md={6}>
|
||||
@@ -58,10 +60,8 @@ import {
|
||||
Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example:
|
||||
|
||||
```tsx
|
||||
// At the top imports
|
||||
import { ILertPage } from '@backstage/plugin-ilert';
|
||||
|
||||
// Inside App component
|
||||
<Routes>
|
||||
// ...
|
||||
<Route path="/ilert" element={<ILertPage />} />
|
||||
@@ -72,10 +72,8 @@ import { ILertPage } from '@backstage/plugin-ilert';
|
||||
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 from the plugin, for example:
|
||||
|
||||
```tsx
|
||||
// At the top imports
|
||||
import { ILertIcon } from '@backstage/plugin-ilert';
|
||||
|
||||
// Inside Sidebar component
|
||||
<Sidebar>
|
||||
// ...
|
||||
<SidebarItem icon={ILertIcon} to="ilert" text="iLert" />
|
||||
@@ -91,7 +89,7 @@ In `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
ilert:
|
||||
domain: https://my-org.ilert.com/
|
||||
baseUrl: https://my-org.ilert.com/
|
||||
```
|
||||
|
||||
## Providing the Authorization Header
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
AlertSource,
|
||||
EscalationPolicy,
|
||||
Incident,
|
||||
IncidentAction,
|
||||
IncidentResponder,
|
||||
Schedule,
|
||||
UptimeMonitor,
|
||||
@@ -44,22 +45,22 @@ export class UnauthorizedError extends Error {}
|
||||
export class ILertClient implements ILertApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly proxyPath: string;
|
||||
private readonly domain: string;
|
||||
private readonly baseUrl: string;
|
||||
|
||||
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
|
||||
const domainUrl: string =
|
||||
configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com';
|
||||
const baseUrl: string =
|
||||
configApi.getOptionalString('ilert.baseUrl') ?? 'https://app.ilert.com';
|
||||
|
||||
return new ILertClient({
|
||||
discoveryApi: discoveryApi,
|
||||
domain: domainUrl,
|
||||
baseUrl,
|
||||
proxyPath: configApi.getOptionalString('ilert.proxyPath'),
|
||||
});
|
||||
}
|
||||
|
||||
constructor(opts: Options) {
|
||||
this.discoveryApi = opts.discoveryApi;
|
||||
this.domain = opts.domain;
|
||||
this.baseUrl = opts.baseUrl;
|
||||
this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export class ILertClient implements ILertApi {
|
||||
query.append('start-index', `${opts.startIndex}`);
|
||||
}
|
||||
if (opts && opts.alertSources) {
|
||||
opts.alertSources.forEach((a: string | number) => {
|
||||
opts.alertSources.forEach((a: number) => {
|
||||
if (a) {
|
||||
query.append('alert-source', `${a}`);
|
||||
}
|
||||
@@ -153,6 +154,22 @@ export class ILertClient implements ILertApi {
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchIncidentActions(incident: Incident): Promise<IncidentAction[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${incident.id}/actions`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async acceptIncident(incident: Incident): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
@@ -222,6 +239,27 @@ export class ILertClient implements ILertApi {
|
||||
return response;
|
||||
}
|
||||
|
||||
async triggerIncidentAction(
|
||||
incident: Incident,
|
||||
action: IncidentAction,
|
||||
): Promise<void> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
webhookId: action.webhookId,
|
||||
extensionId: action.extensionId,
|
||||
type: action.type,
|
||||
name: action.name,
|
||||
}),
|
||||
};
|
||||
|
||||
await this.fetch(`/api/v1/incidents/${incident.id}/actions`, init);
|
||||
}
|
||||
|
||||
async createIncident(eventRequest: EventRequest): Promise<boolean> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
@@ -461,26 +499,26 @@ export class ILertClient implements ILertApi {
|
||||
}
|
||||
|
||||
getIncidentDetailsURL(incident: Incident): string {
|
||||
return `${this.domain}/incident/view.jsf?id=${incident.id}`;
|
||||
return `${this.baseUrl}/incident/view.jsf?id=${incident.id}`;
|
||||
}
|
||||
|
||||
getAlertSourceDetailsURL(alertSource: AlertSource | null): string {
|
||||
if (!alertSource) {
|
||||
return '';
|
||||
}
|
||||
return `${this.domain}/source/view.jsf?id=${alertSource.id}`;
|
||||
return `${this.baseUrl}/source/view.jsf?id=${alertSource.id}`;
|
||||
}
|
||||
|
||||
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string {
|
||||
return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`;
|
||||
return `${this.baseUrl}/policy/view.jsf?id=${escalationPolicy.id}`;
|
||||
}
|
||||
|
||||
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string {
|
||||
return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`;
|
||||
return `${this.baseUrl}/uptime/view.jsf?id=${uptimeMonitor.id}`;
|
||||
}
|
||||
|
||||
getScheduleDetailsURL(schedule: Schedule): string {
|
||||
return `${this.domain}/schedule/view.jsf?id=${schedule.id}`;
|
||||
return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`;
|
||||
}
|
||||
|
||||
getUserInitials(assignedTo: User | null) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
EscalationPolicy,
|
||||
Schedule,
|
||||
IncidentResponder,
|
||||
IncidentAction,
|
||||
} from '../types';
|
||||
|
||||
export type TableState = {
|
||||
@@ -34,7 +35,7 @@ export type GetIncidentsOpts = {
|
||||
maxResults?: number;
|
||||
startIndex?: number;
|
||||
states?: IncidentStatus[];
|
||||
alertSources?: number[] | string[];
|
||||
alertSources?: number[];
|
||||
};
|
||||
|
||||
export type GetIncidentsCountOpts = {
|
||||
@@ -53,6 +54,7 @@ export interface ILertApi {
|
||||
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
|
||||
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
|
||||
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
|
||||
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
|
||||
acceptIncident(incident: Incident): Promise<Incident>;
|
||||
resolveIncident(incident: Incident): Promise<Incident>;
|
||||
assignIncident(
|
||||
@@ -60,6 +62,10 @@ export interface ILertApi {
|
||||
responder: IncidentResponder,
|
||||
): Promise<Incident>;
|
||||
createIncident(eventRequest: EventRequest): Promise<boolean>;
|
||||
triggerIncidentAction(
|
||||
incident: Incident,
|
||||
action: IncidentAction,
|
||||
): Promise<void>;
|
||||
|
||||
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
|
||||
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
|
||||
@@ -98,10 +104,10 @@ export type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
|
||||
/**
|
||||
* Domain used by users to access iLert web UI.
|
||||
* URL used by users to access iLert web UI.
|
||||
* Example: https://my-org.ilert.com/
|
||||
*/
|
||||
domain: string;
|
||||
baseUrl: string;
|
||||
|
||||
/**
|
||||
* Path to use for requests via the proxy, defaults to /ilert/api
|
||||
|
||||
@@ -56,7 +56,6 @@ export const ILertCard = () => {
|
||||
{ alertSource, uptimeMonitor },
|
||||
{ setAlertSource, refetchAlertSource },
|
||||
] = useAlertSource(integrationKey);
|
||||
const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey];
|
||||
const [
|
||||
{ tableState, states, incidents, incidentsCount, isLoading, error },
|
||||
{
|
||||
@@ -67,7 +66,7 @@ export const ILertCard = () => {
|
||||
refetchIncidents,
|
||||
setIsLoading,
|
||||
},
|
||||
] = useIncidents(false, alertSourcesFilter);
|
||||
] = useIncidents(false, true, alertSource);
|
||||
|
||||
const [
|
||||
isNewIncidentModalOpened,
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { alertApiRef, useApi } from '@backstage/core';
|
||||
import { alertApiRef, Progress, useApi } 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 } from '../../types';
|
||||
import { Incident, IncidentAction } from '../../types';
|
||||
import { IncidentAssignModal } from './IncidentAssignModal';
|
||||
import { useIncidentActions } from '../../hooks/useIncidentActions';
|
||||
|
||||
export const IncidentActionsMenu = ({
|
||||
incident,
|
||||
@@ -41,6 +42,11 @@ export const IncidentActionsMenu = ({
|
||||
setIsAssignIncidentModalOpened,
|
||||
] = React.useState(false);
|
||||
|
||||
const [{ incidentActions, isLoading }] = useIncidentActions(
|
||||
incident,
|
||||
Boolean(anchorEl),
|
||||
);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@@ -84,6 +90,40 @@ export const IncidentActionsMenu = ({
|
||||
setIsAssignIncidentModalOpened(true);
|
||||
};
|
||||
|
||||
const handleTriggerAction = (action: IncidentAction) => async () => {
|
||||
try {
|
||||
handleCloseMenu();
|
||||
setProcessing(true);
|
||||
await ilertApi.triggerIncidentAction(incident, action);
|
||||
alertApi.post({ message: 'Incident action triggered.' });
|
||||
setProcessing(false);
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const actions: React.ReactNode[] = incidentActions.map(a => {
|
||||
const successTrigger = a.history
|
||||
? a.history.find(h => h.success)
|
||||
: undefined;
|
||||
const triggeredBy =
|
||||
successTrigger && successTrigger.actor
|
||||
? `${successTrigger.actor.firstName} ${successTrigger.actor.lastName}`
|
||||
: '';
|
||||
return (
|
||||
<MenuItem
|
||||
key={a.webhookId}
|
||||
onClick={handleTriggerAction(a)}
|
||||
disabled={!!successTrigger}
|
||||
>
|
||||
<Typography variant="inherit" noWrap>
|
||||
{triggeredBy ? `${a.name} (by ${triggeredBy})` : a.name}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -102,7 +142,7 @@ export const IncidentActionsMenu = ({
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
style: { maxHeight: 48 * 4.5 },
|
||||
style: { maxHeight: 48 * 5.5 },
|
||||
}}
|
||||
>
|
||||
{incident.status === 'PENDING' ? (
|
||||
@@ -129,6 +169,14 @@ export const IncidentActionsMenu = ({
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<MenuItem key="loading">
|
||||
<Progress style={{ width: '100%' }} />
|
||||
</MenuItem>
|
||||
) : (
|
||||
actions
|
||||
)}
|
||||
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link href={ilertApi.getIncidentDetailsURL(incident)}>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ilertApiRef, UnauthorizedError } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { Incident, IncidentAction } from '../types';
|
||||
|
||||
export const useIncidentActions = (
|
||||
incident: Incident | null,
|
||||
open: boolean,
|
||||
) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [incidentActionsList, setIncidentActionsList] = React.useState<
|
||||
IncidentAction[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
if (!incident || !open) {
|
||||
return;
|
||||
}
|
||||
const data = await ilertApi.fetchIncidentActions(incident);
|
||||
setIncidentActionsList(data);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [incident, open]);
|
||||
|
||||
return [
|
||||
{
|
||||
incidentActions: incidentActionsList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setIncidentActionsList,
|
||||
setIsLoading,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -22,11 +22,18 @@ import {
|
||||
} from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types';
|
||||
import {
|
||||
ACCEPTED,
|
||||
PENDING,
|
||||
Incident,
|
||||
IncidentStatus,
|
||||
AlertSource,
|
||||
} from '../types';
|
||||
|
||||
export const useIncidents = (
|
||||
paging: boolean,
|
||||
alertSources?: number[] | string[],
|
||||
singleSource?: boolean,
|
||||
alertSource?: AlertSource | null,
|
||||
) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
@@ -45,10 +52,13 @@ export const useIncidents = (
|
||||
|
||||
const fetchIncidentsCall = async () => {
|
||||
try {
|
||||
if (singleSource && !alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const opts: GetIncidentsOpts = {
|
||||
states,
|
||||
alertSources,
|
||||
alertSources: alertSource ? [alertSource.id] : [],
|
||||
};
|
||||
if (paging) {
|
||||
opts.maxResults = tableState.pageSize;
|
||||
@@ -80,6 +90,8 @@ export const useIncidents = (
|
||||
const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [
|
||||
tableState,
|
||||
states,
|
||||
singleSource,
|
||||
alertSource,
|
||||
]);
|
||||
|
||||
const refetchIncidents = () => {
|
||||
|
||||
@@ -310,3 +310,19 @@ export interface IncidentResponder {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface IncidentAction {
|
||||
name: string;
|
||||
type: string;
|
||||
webhookId: string;
|
||||
extensionId?: string;
|
||||
history?: IncidentActionHistory[];
|
||||
}
|
||||
|
||||
export interface IncidentActionHistory {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
incidentId: number;
|
||||
actor: User;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user