@@ -14,6 +14,7 @@
|
||||
/plugins/search-* @backstage/techdocs-core
|
||||
/plugins/techdocs @backstage/techdocs-core
|
||||
/plugins/techdocs-backend @backstage/techdocs-core
|
||||
/plugins/ilert @yacut
|
||||
/packages/techdocs-common @backstage/techdocs-core
|
||||
/.changeset/cost-insights-* @backstage/silver-lining
|
||||
/.changeset/techdocs-* @backstage/techdocs-core
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: iLert
|
||||
author: iLert
|
||||
authorUrl: https://github.com/iLert
|
||||
category: Monitoring
|
||||
description: iLert is a platform for alerting, on-call management and uptime monitoring.
|
||||
documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert
|
||||
iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4
|
||||
npmPackageName: '@backstage/plugin-ilert'
|
||||
tags:
|
||||
- monitoring
|
||||
- errors
|
||||
- alerting
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
rules: {
|
||||
quotes: ['error', 'single'],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
# @backstage/plugin-ilert
|
||||
|
||||
## Introduction
|
||||
|
||||
[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as [informing stakeholders](https://docs.ilert.com/getting-started/stakeholder-engagement) or creating tickets in external incident management tools.
|
||||
|
||||
## Overview
|
||||
|
||||
This plugin displays iLert information about an entity such as if there are any active incidents, wo is on-call now and uptime monitor status.
|
||||
|
||||
There is also an easy way to trigger an incident directly to the person who is currently on-call.
|
||||
|
||||
This plugin provides:
|
||||
|
||||
- Information details about the persons on-call
|
||||
- A way to override current person on-call
|
||||
- A list of incidents
|
||||
- A way to trigger a new incident
|
||||
- A way to reassign/acknowledge/resolve an incident
|
||||
- A list of uptime monitors
|
||||
|
||||
## Setup instructions
|
||||
|
||||
Install the plugin:
|
||||
|
||||
```bash
|
||||
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
|
||||
import {
|
||||
isPluginApplicableToEntity as isILertAvailable,
|
||||
EntityILertCard,
|
||||
} from '@backstage/plugin-ilert';
|
||||
// add to code
|
||||
{
|
||||
isILertAvailable(entity) && (
|
||||
<Grid item md={6}>
|
||||
<EntityILertCard />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> To force iLert card for each entity just add the `<EntityILertCard />` component, so an instruction card will appears if no integration key is set.
|
||||
|
||||
## Add iLert explorer to the App sidebar
|
||||
|
||||
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 />} />
|
||||
// ...
|
||||
</Routes>;
|
||||
```
|
||||
|
||||
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" />
|
||||
// ...
|
||||
</Sidebar>;
|
||||
```
|
||||
|
||||
## Client configuration
|
||||
|
||||
If you want to override the default URL for api calls and detail pages, you can add it to `app-config.yaml`.
|
||||
|
||||
In `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
ilert:
|
||||
domain: https://my-org.ilert.com/
|
||||
```
|
||||
|
||||
## Providing the Authorization Header
|
||||
|
||||
In order to make the API calls, you need to provide a new proxy config which will redirect to the [iLert API](https://api.ilert.com/api-docs/) endpoint it needs an [Authorization Header](https://api.ilert.com/api-docs/#section/Authentication).
|
||||
|
||||
Add the proxy configuration in `app-config.yaml`
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
...
|
||||
'/ilert':
|
||||
target: https://api.ilert.com
|
||||
allowedMethods: ['GET', 'POST', 'PUT']
|
||||
allowedHeaders: ['Authorization']
|
||||
headers:
|
||||
Authorization:
|
||||
$env: ILERT_AUTH_HEADER
|
||||
```
|
||||
|
||||
Then start the backend passing the token as an environment variable:
|
||||
|
||||
```bash
|
||||
$ ILERT_AUTH_HEADER='Basic <TOKEN>' yarn start
|
||||
```
|
||||
|
||||
## Integration Key
|
||||
|
||||
The information displayed for each entity is based on the [alert source integration key](https://docs.ilert.com/integrations/backstage).
|
||||
|
||||
### Adding the integration key to the entity annotation
|
||||
|
||||
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]
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { ilertPlugin } from '../src/plugin';
|
||||
import { IlertPage } from '../src';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(ilertPlugin)
|
||||
.addPage({
|
||||
element: <IlertPage />,
|
||||
title: 'Root Page',
|
||||
})
|
||||
.render();
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@backstage/plugin-ilert",
|
||||
"version": "0.1.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.6",
|
||||
"@backstage/core": "^0.7.4",
|
||||
"@backstage/plugin-catalog-react": "^0.1.4",
|
||||
"@backstage/theme": "^0.2.5",
|
||||
"@date-io/date-fns": "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",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.7",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* 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 { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core';
|
||||
import {
|
||||
AlertSource,
|
||||
EscalationPolicy,
|
||||
Incident,
|
||||
IncidentResponder,
|
||||
Schedule,
|
||||
UptimeMonitor,
|
||||
User,
|
||||
} from '../types';
|
||||
import {
|
||||
ILertApi,
|
||||
GetIncidentsOpts,
|
||||
GetIncidentsCountOpts,
|
||||
Options,
|
||||
EventRequest,
|
||||
} from './types';
|
||||
import moment from 'moment';
|
||||
import momentTimezone from 'moment-timezone';
|
||||
|
||||
export const ilertApiRef = createApiRef<ILertApi>({
|
||||
id: 'plugin.ilert.service',
|
||||
description: 'Used to make requests towards iLert API',
|
||||
});
|
||||
|
||||
const DEFAULT_PROXY_PATH = '/ilert';
|
||||
export class UnauthorizedError extends Error {}
|
||||
|
||||
export class ILertClient implements ILertApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly proxyPath: string;
|
||||
private readonly domain: string;
|
||||
|
||||
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
|
||||
const domainUrl: string =
|
||||
configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com';
|
||||
|
||||
return new ILertClient({
|
||||
discoveryApi: discoveryApi,
|
||||
domain: domainUrl,
|
||||
proxyPath: configApi.getOptionalString('ilert.proxyPath'),
|
||||
});
|
||||
}
|
||||
|
||||
constructor(opts: Options) {
|
||||
this.discoveryApi = opts.discoveryApi;
|
||||
this.domain = opts.domain;
|
||||
this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH;
|
||||
}
|
||||
|
||||
private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> {
|
||||
const apiUrl = await this.apiUrl();
|
||||
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
const query = new URLSearchParams();
|
||||
if (opts && opts.maxResults) {
|
||||
query.append('max-results', `${opts.maxResults}`);
|
||||
}
|
||||
if (opts && opts.startIndex) {
|
||||
query.append('start-index', `${opts.startIndex}`);
|
||||
}
|
||||
if (opts && opts.alertSources) {
|
||||
opts.alertSources.forEach((a: string | number) => {
|
||||
if (a) {
|
||||
query.append('alert-source', `${a}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (opts && opts.states && Array.isArray(opts.states)) {
|
||||
opts.states.forEach(state => {
|
||||
query.append('state', state);
|
||||
});
|
||||
}
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents?${query.toString()}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
const query = new URLSearchParams();
|
||||
if (opts && opts.states && Array.isArray(opts.states)) {
|
||||
opts.states.forEach(state => {
|
||||
query.append('state', state);
|
||||
});
|
||||
}
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/count?${query.toString()}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response && response.count ? response.count : 0;
|
||||
}
|
||||
|
||||
async fetchIncidentResponders(
|
||||
incident: Incident,
|
||||
): Promise<IncidentResponder[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${incident.id}/responders`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async acceptIncident(incident: Incident): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ summary: 'Backstage — iLert plugin' }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${incident.id}/accept`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async resolveIncident(incident: Incident): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ summary: 'Backstage — iLert plugin' }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${incident.id}/resolve`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async assignIncident(
|
||||
incident: Incident,
|
||||
responder: IncidentResponder,
|
||||
): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const query = new URLSearchParams();
|
||||
switch (responder.group) {
|
||||
case 'ESCALATION_POLICY':
|
||||
query.append('policy-id', `${responder.id}`);
|
||||
break;
|
||||
case 'ON_CALL_SCHEDULE':
|
||||
query.append('schedule-id', `${responder.id}`);
|
||||
break;
|
||||
default:
|
||||
query.append('user-id', `${responder.id}`);
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${incident.id}/assign?${query.toString()}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async createIncident(eventRequest: EventRequest): Promise<boolean> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKey: eventRequest.integrationKey,
|
||||
summary: eventRequest.summary,
|
||||
details: eventRequest.details,
|
||||
eventType: 'ALERT',
|
||||
links: [
|
||||
{
|
||||
href: eventRequest.source,
|
||||
text: 'Backstage Url',
|
||||
},
|
||||
],
|
||||
customDetails: {
|
||||
userName: eventRequest.userName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/events', init);
|
||||
return response.responseCode === 'NEW_INCIDENT_CREATED';
|
||||
}
|
||||
|
||||
async fetchUptimeMonitors(): Promise<UptimeMonitor[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/uptime-monitors', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchUptimeMonitor(id: number): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response: UptimeMonitor = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${id}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async pauseUptimeMonitor(
|
||||
uptimeMonitor: UptimeMonitor,
|
||||
): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ...uptimeMonitor, paused: true }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${uptimeMonitor.id}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async resumeUptimeMonitor(
|
||||
uptimeMonitor: UptimeMonitor,
|
||||
): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ...uptimeMonitor, paused: false }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${uptimeMonitor.id}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchAlertSources(): Promise<AlertSource[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/alert-sources', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchAlertSource(
|
||||
idOrIntegrationKey: number | string,
|
||||
): Promise<AlertSource> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${idOrIntegrationKey}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async enableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ...alertSource, active: true }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${alertSource.id}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async disableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ...alertSource, active: false }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${alertSource.id}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async addImmediateMaintenance(
|
||||
alertSourceId: number,
|
||||
minutes: number,
|
||||
): Promise<void> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start: moment().utc().toISOString(),
|
||||
end: moment().add(minutes, 'minutes').utc().toISOString(),
|
||||
description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`,
|
||||
createdBy: 'Backstage',
|
||||
timezone: momentTimezone.tz.guess(),
|
||||
alertSources: [{ id: alertSourceId }],
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/maintenance-windows', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchOnCallSchedules(): Promise<Schedule[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/schedules', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchUsers(): Promise<User[]> {
|
||||
const init = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/users', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async overrideShift(
|
||||
scheduleId: number,
|
||||
userId: number,
|
||||
start: string,
|
||||
end: string,
|
||||
): Promise<Schedule> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ user: { id: userId }, start, end }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/schedules/${scheduleId}/overrides`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
getIncidentDetailsURL(incident: Incident): string {
|
||||
return `${this.domain}/incident/view.jsf?id=${incident.id}`;
|
||||
}
|
||||
|
||||
getAlertSourceDetailsURL(alertSource: AlertSource | null): string {
|
||||
if (!alertSource) {
|
||||
return '';
|
||||
}
|
||||
return `${this.domain}/source/view.jsf?id=${alertSource.id}`;
|
||||
}
|
||||
|
||||
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string {
|
||||
return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`;
|
||||
}
|
||||
|
||||
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string {
|
||||
return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`;
|
||||
}
|
||||
|
||||
getScheduleDetailsURL(schedule: Schedule): string {
|
||||
return `${this.domain}/schedule/view.jsf?id=${schedule.id}`;
|
||||
}
|
||||
|
||||
getUserInitials(assignedTo: User | null) {
|
||||
if (!assignedTo) {
|
||||
return '';
|
||||
}
|
||||
if (!assignedTo.firstName && !assignedTo.lastName) {
|
||||
return assignedTo.username;
|
||||
}
|
||||
return `${assignedTo.firstName} ${assignedTo.lastName} (${assignedTo.username})`;
|
||||
}
|
||||
|
||||
private async apiUrl() {
|
||||
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
|
||||
return proxyUrl + this.proxyPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { ILertClient, ilertApiRef, UnauthorizedError } from './client';
|
||||
export type {
|
||||
ILertApi,
|
||||
GetIncidentsCountOpts,
|
||||
GetIncidentsOpts,
|
||||
TableState,
|
||||
} from './types';
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { DiscoveryApi } from '@backstage/core';
|
||||
import {
|
||||
AlertSource,
|
||||
Incident,
|
||||
User,
|
||||
IncidentStatus,
|
||||
UptimeMonitor,
|
||||
EscalationPolicy,
|
||||
Schedule,
|
||||
IncidentResponder,
|
||||
} from '../types';
|
||||
|
||||
export type TableState = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
export type GetIncidentsOpts = {
|
||||
maxResults?: number;
|
||||
startIndex?: number;
|
||||
states?: IncidentStatus[];
|
||||
alertSources?: number[] | string[];
|
||||
};
|
||||
|
||||
export type GetIncidentsCountOpts = {
|
||||
states?: IncidentStatus[];
|
||||
};
|
||||
|
||||
export type EventRequest = {
|
||||
integrationKey: string;
|
||||
summary: string;
|
||||
details: string;
|
||||
userName: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export interface ILertApi {
|
||||
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
|
||||
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
|
||||
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
|
||||
acceptIncident(incident: Incident): Promise<Incident>;
|
||||
resolveIncident(incident: Incident): Promise<Incident>;
|
||||
assignIncident(
|
||||
incident: Incident,
|
||||
responder: IncidentResponder,
|
||||
): Promise<Incident>;
|
||||
createIncident(eventRequest: EventRequest): Promise<boolean>;
|
||||
|
||||
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
|
||||
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
|
||||
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
|
||||
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
|
||||
|
||||
fetchAlertSources(): Promise<AlertSource[]>;
|
||||
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
|
||||
enableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
|
||||
disableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
|
||||
|
||||
addImmediateMaintenance(
|
||||
alertSourceId: number,
|
||||
minutes: number,
|
||||
): Promise<void>;
|
||||
|
||||
fetchOnCallSchedules(): Promise<Schedule[]>;
|
||||
fetchUsers(): Promise<User[]>;
|
||||
|
||||
overrideShift(
|
||||
scheduleId: number,
|
||||
userId: number,
|
||||
start: string,
|
||||
end: string,
|
||||
): Promise<Schedule>;
|
||||
|
||||
getIncidentDetailsURL(incident: Incident): string;
|
||||
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
|
||||
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
|
||||
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
|
||||
getScheduleDetailsURL(schedule: Schedule): string;
|
||||
getUserInitials(assignedTo: User | null): string;
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
|
||||
/**
|
||||
* Domain used by users to access iLert web UI.
|
||||
* Example: https://my-org.ilert.com/
|
||||
*/
|
||||
domain: string;
|
||||
|
||||
/**
|
||||
* Path to use for requests via the proxy, defaults to /ilert/api
|
||||
*/
|
||||
proxyPath?: string;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24pt" height="24pt" viewBox="0 0 24 24" version="1.1">
|
||||
<g id="Logo">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 0 9.277344 L 0 18.574219 C 0 20.832031 1.847656 22.679688 4.105469 22.679688 L 12.582031 22.679688 C 6.21875 21.007812 1.277344 15.792969 0 9.277344 Z M 0 9.277344 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 19.527344 22.5625 C 21.328125 22.128906 22.679688 20.503906 22.679688 18.574219 L 22.679688 16.273438 C 21.691406 16.820312 20.632812 17.222656 19.527344 17.46875 Z M 19.527344 22.5625 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.417969 17.46875 C 9.136719 16.296875 5.171875 11.578125 5.171875 5.945312 C 5.167969 3.855469 5.726562 1.804688 6.785156 0 L 4.105469 0 C 1.847656 0 0 1.847656 0 4.105469 L 0 6.882812 C 0.433594 14.816406 6.335938 21.316406 13.992188 22.679688 L 14.421875 22.679688 Z M 14.417969 17.46875 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 19.527344 12.375 L 19.527344 17.160156 C 20.632812 16.910156 21.695312 16.496094 22.679688 15.929688 L 22.679688 9.855469 C 21.902344 10.988281 20.804688 11.863281 19.527344 12.375 Z M 19.527344 12.375 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.417969 17.160156 L 14.417969 12.375 C 11.863281 11.355469 10.054688 8.859375 10.054688 5.945312 C 10.054688 3.503906 11.34375 1.246094 13.441406 0 L 7.128906 0 C 6.039062 1.792969 5.464844 3.847656 5.46875 5.945312 C 5.46875 11.410156 9.300781 15.996094 14.417969 17.160156 Z M 14.417969 17.160156 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 10.355469 5.945312 C 10.355469 8.613281 11.957031 11.019531 14.417969 12.050781 L 14.417969 9.917969 C 15.972656 10.925781 17.972656 10.925781 19.527344 9.917969 L 19.527344 12.050781 C 20.847656 11.496094 21.953125 10.53125 22.679688 9.300781 L 22.679688 4.105469 C 22.679688 1.847656 20.832031 0 18.574219 0 L 14.066406 0 C 11.796875 1.113281 10.355469 3.417969 10.355469 5.945312 Z M 16.972656 2.5625 C 18.84375 2.5625 20.355469 4.078125 20.355469 5.945312 C 20.355469 7.8125 18.84375 9.328125 16.972656 9.328125 C 15.105469 9.328125 13.589844 7.8125 13.589844 5.945312 C 13.59375 4.078125 15.105469 2.566406 16.972656 2.5625 Z M 16.972656 2.5625 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { AlertSource } from '../../types';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
image: {
|
||||
height: 22,
|
||||
paddingRight: 4,
|
||||
},
|
||||
link: {
|
||||
lineHeight: '22px',
|
||||
},
|
||||
});
|
||||
|
||||
export const AlertSourceLink = ({
|
||||
alertSource,
|
||||
}: {
|
||||
alertSource: AlertSource | null;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
|
||||
if (!alertSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={2}>
|
||||
<img
|
||||
src={prefersDarkMode ? alertSource.lightIconUrl : alertSource.iconUrl}
|
||||
alt={alertSource.name}
|
||||
className={classes.image}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<Link
|
||||
className={classes.link}
|
||||
href={ilertApi.getAlertSourceDetailsURL(alertSource)}
|
||||
>
|
||||
{alertSource.name}
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { EmptyState } from '@backstage/core';
|
||||
import { Button } from '@material-ui/core';
|
||||
|
||||
export const MissingAuthorizationHeaderError = () => (
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title="Missing or invalid iLert authorization header"
|
||||
description="The request to fetch data needs a valid authorization header. See README for more details."
|
||||
action={
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
href="https://github.com/backstage/backstage/blob/master/plugins/ilert/README.md"
|
||||
>
|
||||
Read More
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { MissingAuthorizationHeaderError } from './MissingAuthorizationHeaderError';
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { EscalationPolicy } from '../../types';
|
||||
import { ilertApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
link: {
|
||||
lineHeight: '22px',
|
||||
},
|
||||
});
|
||||
|
||||
export const EscalationPolicyLink = ({
|
||||
escalationPolicy,
|
||||
}: {
|
||||
escalationPolicy: EscalationPolicy | null;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!escalationPolicy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classes.link}
|
||||
href={ilertApi.getEscalationPolicyDetailsURL(escalationPolicy)}
|
||||
>
|
||||
{escalationPolicy.name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { UnauthorizedError } from '../../api';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
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 { MissingAuthorizationHeaderError } from '../Errors';
|
||||
import { useIncidents } from '../../hooks/useIncidents';
|
||||
import { IncidentsTable } from '../IncidentsPage';
|
||||
import { IncidentNewModal } from '../Incident/IncidentNewModal';
|
||||
import { ILertCardActionsHeader } from './ILertCardActionsHeader';
|
||||
import { useAlertSource } from '../../hooks/useAlertSource';
|
||||
import { useILertEntity } from '../../hooks';
|
||||
import { ILertCardHeaderStatus } from './ILertCardHeaderStatus';
|
||||
import { ILertCardMaintenanceModal } from './ILertCardMaintenanceModal';
|
||||
import { ILertCardEmptyState } from './ILertCardEmptyState';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]);
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
paddingLeft: '0 !important',
|
||||
paddingRight: '0 !important',
|
||||
paddingBottom: '0 !important',
|
||||
paddingTop: '0 !important',
|
||||
'& div div': {
|
||||
boxShadow: 'none !important',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ILertCard = () => {
|
||||
const classes = useStyles();
|
||||
const { integrationKey, name } = useILertEntity();
|
||||
const [
|
||||
{ alertSource, uptimeMonitor },
|
||||
{ setAlertSource, refetchAlertSource },
|
||||
] = useAlertSource(integrationKey);
|
||||
const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey];
|
||||
const [
|
||||
{ tableState, states, incidents, incidentsCount, isLoading, error },
|
||||
{
|
||||
onIncidentStatesChange,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
onIncidentChanged,
|
||||
refetchIncidents,
|
||||
setIsLoading,
|
||||
},
|
||||
] = useIncidents(false, alertSourcesFilter);
|
||||
|
||||
const [
|
||||
isNewIncidentModalOpened,
|
||||
setIsNewIncidentModalOpened,
|
||||
] = React.useState(false);
|
||||
const [
|
||||
isMaintenanceModalOpened,
|
||||
setIsMaintenanceModalOpened,
|
||||
] = React.useState(false);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingAuthorizationHeaderError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert data-testid="error-message" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!integrationKey) {
|
||||
return <ILertCardEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card data-testid="ilert-card">
|
||||
<CardHeader
|
||||
title="iLert"
|
||||
subheader={
|
||||
<ILertCardActionsHeader
|
||||
alertSource={alertSource}
|
||||
setAlertSource={setAlertSource}
|
||||
setIsNewIncidentModalOpened={setIsNewIncidentModalOpened}
|
||||
setIsMaintenanceModalOpened={setIsMaintenanceModalOpened}
|
||||
uptimeMonitor={uptimeMonitor}
|
||||
/>
|
||||
}
|
||||
action={<ILertCardHeaderStatus alertSource={alertSource} />}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent className={classes.content}>
|
||||
<IncidentsTable
|
||||
incidents={incidents}
|
||||
incidentsCount={incidentsCount}
|
||||
tableState={tableState}
|
||||
states={states}
|
||||
onIncidentChanged={onIncidentChanged}
|
||||
onIncidentStatesChange={onIncidentStatesChange}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
compact
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<IncidentNewModal
|
||||
isModalOpened={isNewIncidentModalOpened}
|
||||
setIsModalOpened={setIsNewIncidentModalOpened}
|
||||
refetchIncidents={refetchIncidents}
|
||||
initialAlertSource={alertSource}
|
||||
entityName={name}
|
||||
/>
|
||||
<ILertCardMaintenanceModal
|
||||
alertSource={alertSource}
|
||||
refetchAlertSource={refetchAlertSource}
|
||||
isModalOpened={isMaintenanceModalOpened}
|
||||
setIsModalOpened={setIsMaintenanceModalOpened}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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 {
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
useApi,
|
||||
alertApiRef,
|
||||
} from '@backstage/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
import BuildIcon from '@material-ui/icons/Build';
|
||||
import PauseIcon from '@material-ui/icons/Pause';
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import TimelineIcon from '@material-ui/icons/Timeline';
|
||||
import WebIcon from '@material-ui/icons/Web';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { AlertSource, UptimeMonitor } from '../../types';
|
||||
|
||||
export const ILertCardActionsHeader = ({
|
||||
alertSource,
|
||||
setAlertSource,
|
||||
setIsNewIncidentModalOpened,
|
||||
setIsMaintenanceModalOpened,
|
||||
uptimeMonitor,
|
||||
}: {
|
||||
alertSource: AlertSource | null;
|
||||
setAlertSource: (alertSource: AlertSource) => void;
|
||||
setIsNewIncidentModalOpened: (isOpen: boolean) => void;
|
||||
setIsMaintenanceModalOpened: (isOpen: boolean) => void;
|
||||
uptimeMonitor: UptimeMonitor | null;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [isDisableModalOpened, setIsDisableModalOpened] = React.useState(false);
|
||||
|
||||
const handleCreateNewIncident = () => {
|
||||
setIsNewIncidentModalOpened(true);
|
||||
};
|
||||
|
||||
const handleEnableAlertSource = async () => {
|
||||
try {
|
||||
if (!alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const newAlertSource = await ilertApi.enableAlertSource(alertSource);
|
||||
alertApi.post({ message: 'Alert source enabled.' });
|
||||
setIsLoading(false);
|
||||
setAlertSource(newAlertSource);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
const handleDisableAlertSource = async () => {
|
||||
try {
|
||||
if (!alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsDisableModalOpened(false);
|
||||
setIsLoading(true);
|
||||
const newAlertSource = await ilertApi.disableAlertSource(alertSource);
|
||||
alertApi.post({ message: 'Alert source disabled.' });
|
||||
setIsLoading(false);
|
||||
setAlertSource(newAlertSource);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisableAlertSourceWarningOpen = () => {
|
||||
setIsDisableModalOpened(true);
|
||||
};
|
||||
|
||||
const handleDisableAlertSourceWarningClose = () => {
|
||||
setIsDisableModalOpened(false);
|
||||
};
|
||||
|
||||
const handleMaintenanceAlertSource = () => {
|
||||
setIsMaintenanceModalOpened(true);
|
||||
};
|
||||
|
||||
const alertSourceLink: IconLinkVerticalProps = {
|
||||
label: 'Alert Source',
|
||||
href: ilertApi.getAlertSourceDetailsURL(alertSource),
|
||||
icon: <WebIcon />,
|
||||
};
|
||||
|
||||
const createIncidentLink: IconLinkVerticalProps = {
|
||||
label: 'Create Incident',
|
||||
onClick: handleCreateNewIncident,
|
||||
icon: <AlarmAddIcon />,
|
||||
color: 'secondary',
|
||||
disabled:
|
||||
!alertSource ||
|
||||
alertSource.status === 'DISABLED' ||
|
||||
alertSource.status === 'IN_MAINTENANCE',
|
||||
};
|
||||
|
||||
const enableAlertSourceLink: IconLinkVerticalProps = {
|
||||
label: 'Enable',
|
||||
onClick: handleEnableAlertSource,
|
||||
icon: <PlayArrowIcon />,
|
||||
disabled: !alertSource || isLoading,
|
||||
};
|
||||
|
||||
const disableAlertSourceLink: IconLinkVerticalProps = {
|
||||
label: 'Disable',
|
||||
onClick: handleDisableAlertSourceWarningOpen,
|
||||
icon: <PauseIcon />,
|
||||
disabled: !alertSource || isLoading,
|
||||
};
|
||||
|
||||
const maintenanceAlertSourceLink: IconLinkVerticalProps = {
|
||||
label: 'Immediate maintenance',
|
||||
onClick: handleMaintenanceAlertSource,
|
||||
icon: <BuildIcon />,
|
||||
disabled: !alertSource || isLoading,
|
||||
};
|
||||
|
||||
const uptimeMonitorReportLink: IconLinkVerticalProps = {
|
||||
label: 'Uptime Report',
|
||||
href: uptimeMonitor ? uptimeMonitor.shareUrl : '',
|
||||
icon: <TimelineIcon />,
|
||||
disabled: !alertSource || !uptimeMonitor || isLoading,
|
||||
};
|
||||
|
||||
const links: IconLinkVerticalProps[] = [
|
||||
alertSourceLink,
|
||||
createIncidentLink,
|
||||
alertSource && alertSource.active
|
||||
? disableAlertSourceLink
|
||||
: enableAlertSourceLink,
|
||||
];
|
||||
|
||||
if (alertSource && alertSource.integrationType === 'MONITOR') {
|
||||
links.push(uptimeMonitorReportLink);
|
||||
}
|
||||
|
||||
if (alertSource && alertSource.status !== 'IN_MAINTENANCE') {
|
||||
links.push(maintenanceAlertSourceLink);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderIconLinkRow links={links} />
|
||||
<Dialog
|
||||
open={isDisableModalOpened}
|
||||
onClose={handleDisableAlertSourceWarningClose}
|
||||
aria-labelledby="alert-source-disable-form-title"
|
||||
>
|
||||
<DialogTitle id="alert-source-disable-form-title">
|
||||
Disable alert source
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
<Typography variant="body1" align="justify">
|
||||
Do you really want to disable this alert source? A disabled alert
|
||||
source cannot create new incidents.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleDisableAlertSource}
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
>
|
||||
Disable
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDisableAlertSourceWarningClose}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { CodeSnippet } from '@backstage/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
const ENTITY_YAML = `apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: example
|
||||
description: example.com
|
||||
annotations:
|
||||
ilert.com/integration-key: [INTEGRATION_KEY]
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
owner: guest`;
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
code: {
|
||||
borderRadius: 6,
|
||||
margin: `${theme.spacing(2)}px 0px`,
|
||||
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`,
|
||||
},
|
||||
}));
|
||||
|
||||
export const ILertCardEmptyState = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card data-testid="ilert-empty-card">
|
||||
<CardHeader title="iLert" className={classes.header} />
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Typography variant="body1">
|
||||
No integration key defined for this entity. You can add integration
|
||||
key to your entity YAML as shown in the highlighted example below:
|
||||
</Typography>
|
||||
<div className={classes.code}>
|
||||
<CodeSnippet
|
||||
text={ENTITY_YAML}
|
||||
language="yaml"
|
||||
showLineNumbers
|
||||
highlightedNumbers={[6, 7]}
|
||||
customStyle={{ background: 'inherit', fontSize: '115%' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
href="https://github.com/backstage/backstage/blob/master/plugins/ilert/README.md"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 Chip from '@material-ui/core/Chip';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { AlertSource } from '../../types';
|
||||
|
||||
const MaintenanceChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#92949c',
|
||||
color: 'white',
|
||||
marginTop: 8,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
export const ILertCardHeaderStatus = ({
|
||||
alertSource,
|
||||
}: {
|
||||
alertSource: AlertSource | null;
|
||||
}) => {
|
||||
if (!alertSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (alertSource.status) {
|
||||
case 'IN_MAINTENANCE':
|
||||
return <MaintenanceChip label="MAINTENANCE" size="small" />;
|
||||
case 'DISABLED':
|
||||
return <MaintenanceChip label="INACTIVE" size="small" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 { alertApiRef, useApi } from '@backstage/core';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { AlertSource } from '../../types';
|
||||
|
||||
export const ILertCardMaintenanceModal = ({
|
||||
alertSource,
|
||||
refetchAlertSource,
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
}: {
|
||||
alertSource: AlertSource | null;
|
||||
refetchAlertSource: () => void;
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (isModalOpened: boolean) => void;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const [minutes, setMinutes] = React.useState(5);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsModalOpened(false);
|
||||
};
|
||||
|
||||
const handleImmediateMaintenance = () => {
|
||||
if (!alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsModalOpened(false);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await ilertApi.addImmediateMaintenance(alertSource.id, minutes);
|
||||
alertApi.post({ message: 'Maintenance started.' });
|
||||
refetchAlertSource();
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const handleMinutesChange = (event: any) => {
|
||||
setMinutes(event.target.value);
|
||||
};
|
||||
|
||||
const minuteOptions = [
|
||||
{
|
||||
value: 5,
|
||||
label: '5 minutes',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: '10 minutes',
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
label: '15 minutes',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '30 minutes',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '60 minutes',
|
||||
},
|
||||
];
|
||||
|
||||
if (!alertSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpened}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="maintenance-form-title"
|
||||
>
|
||||
<DialogTitle id="maintenance-form-title">
|
||||
New maintenance window
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Keep your alert sources quiet, when your systems are under
|
||||
maintenance.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
select
|
||||
label="Duration"
|
||||
value={minutes}
|
||||
onChange={handleMinutesChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
{minuteOptions.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleImmediateMaintenance}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 * from './ILertCard';
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 {
|
||||
Page,
|
||||
Header,
|
||||
HeaderTabs,
|
||||
HeaderLabel,
|
||||
Content,
|
||||
} from '@backstage/core';
|
||||
import { IncidentsPage } from '../IncidentsPage';
|
||||
import { UptimeMonitorsPage } from '../UptimeMonitorsPage';
|
||||
import { OnCallSchedulesPage } from '../OnCallSchedulesPage';
|
||||
|
||||
export const ILertPage = () => {
|
||||
const [selectedTab, setSelectedTab] = React.useState<number>(0);
|
||||
const tabs = [
|
||||
{ label: 'Who is on call?' },
|
||||
{ label: 'Incidents' },
|
||||
{ label: 'Uptime Monitors' },
|
||||
];
|
||||
const renderTab = () => {
|
||||
switch (selectedTab) {
|
||||
case 0:
|
||||
return <OnCallSchedulesPage />;
|
||||
case 1:
|
||||
return <IncidentsPage />;
|
||||
case 2:
|
||||
return <UptimeMonitorsPage />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page themeId="website">
|
||||
<Header title="iLert" type="tool">
|
||||
<HeaderLabel label="Owner" value="iLert" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<HeaderTabs
|
||||
selectedIndex={selectedTab}
|
||||
onChange={index => setSelectedTab(index)}
|
||||
tabs={tabs.map(({ label }, index) => ({
|
||||
id: index.toString(),
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Content noPadding>{renderTab()}</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ILertPage;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 * from './ILertPage';
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 { alertApiRef, 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 { IncidentAssignModal } from './IncidentAssignModal';
|
||||
|
||||
export const IncidentActionsMenu = ({
|
||||
incident,
|
||||
onIncidentChanged,
|
||||
setIsLoading,
|
||||
}: {
|
||||
incident: Incident;
|
||||
onIncidentChanged?: (incident: Incident) => void;
|
||||
setIsLoading?: (isLoading: boolean) => void;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const callback = onIncidentChanged || ((_: Incident): void => {});
|
||||
const setProcessing = setIsLoading || ((_: boolean): void => {});
|
||||
const [
|
||||
isAssignIncidentModalOpened,
|
||||
setIsAssignIncidentModalOpened,
|
||||
] = React.useState(false);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleAccept = async (): Promise<void> => {
|
||||
try {
|
||||
handleCloseMenu();
|
||||
setProcessing(true);
|
||||
const newIncident = await ilertApi.acceptIncident(incident);
|
||||
alertApi.post({ message: 'Incident accepted.' });
|
||||
|
||||
callback(newIncident);
|
||||
setProcessing(false);
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = async (): Promise<void> => {
|
||||
try {
|
||||
handleCloseMenu();
|
||||
setProcessing(true);
|
||||
const newIncident = await ilertApi.resolveIncident(incident);
|
||||
alertApi.post({ message: 'Incident resolved.' });
|
||||
|
||||
callback(newIncident);
|
||||
setProcessing(false);
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
handleCloseMenu();
|
||||
setIsAssignIncidentModalOpened(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id={`incident-actions-menu-${incident.id}`}
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
style: { maxHeight: 48 * 4.5 },
|
||||
}}
|
||||
>
|
||||
{incident.status === 'PENDING' ? (
|
||||
<MenuItem key="ack" onClick={handleAccept}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
Accept
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
{incident.status !== 'RESOLVED' ? (
|
||||
<MenuItem key="close" onClick={handleResolve}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
Resolve
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
{incident.status !== 'RESOLVED' ? (
|
||||
<MenuItem key="assign" onClick={handleAssign}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
Assign
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link href={ilertApi.getIncidentDetailsURL(incident)}>
|
||||
View in iLert
|
||||
</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<IncidentAssignModal
|
||||
incident={incident}
|
||||
setIsModalOpened={setIsAssignIncidentModalOpened}
|
||||
isModalOpened={isAssignIncidentModalOpened}
|
||||
onIncidentChanged={onIncidentChanged}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 { alertApiRef, useApi } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { useAssignIncident } from '../../hooks/useAssignIncident';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { Incident } from '../../types';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
width: '100%',
|
||||
},
|
||||
option: {
|
||||
fontSize: 15,
|
||||
'& > span': {
|
||||
marginRight: 10,
|
||||
fontSize: 18,
|
||||
},
|
||||
},
|
||||
optionWrapper: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
sourceImage: {
|
||||
height: 22,
|
||||
paddingRight: 4,
|
||||
},
|
||||
}));
|
||||
|
||||
export const IncidentAssignModal = ({
|
||||
incident,
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
onIncidentChanged,
|
||||
}: {
|
||||
incident: Incident | null;
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (open: boolean) => void;
|
||||
onIncidentChanged?: (incident: Incident) => void;
|
||||
}) => {
|
||||
const [
|
||||
{ incidentRespondersList, incidentResponder, isLoading },
|
||||
{ setIsLoading, setIncidentResponder, setIncidentRespondersList },
|
||||
] = useAssignIncident(incident, isModalOpened);
|
||||
const callback = onIncidentChanged || ((_: Incident): void => {});
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setIncidentRespondersList([]);
|
||||
setIsModalOpened(false);
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
if (!incident || !incidentResponder) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setIncidentRespondersList([]);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const newIncident = await ilertApi.assignIncident(
|
||||
incident,
|
||||
incidentResponder,
|
||||
);
|
||||
callback(newIncident);
|
||||
alertApi.post({ message: 'Incident assigned.' });
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsModalOpened(false);
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const canAssign = !!incidentResponder;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpened}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="assign-incident-form-title"
|
||||
>
|
||||
<DialogTitle id="assign-incident-form-title">
|
||||
Select responder to assign
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
<Typography variant="body1" gutterBottom align="justify">
|
||||
This action will assign the incident to the selected responder.
|
||||
</Typography>
|
||||
</Alert>
|
||||
<Autocomplete
|
||||
disabled={isLoading}
|
||||
options={incidentRespondersList}
|
||||
value={incidentResponder}
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
option: classes.option,
|
||||
}}
|
||||
onChange={(_event: any, newValue: any) => {
|
||||
setIncidentResponder(newValue);
|
||||
}}
|
||||
autoHighlight
|
||||
groupBy={option => {
|
||||
switch (option.group) {
|
||||
case 'SUGGESTED':
|
||||
return 'Suggested responders';
|
||||
case 'USER':
|
||||
return 'Users';
|
||||
case 'ESCALATION_POLICY':
|
||||
return 'Escalation policies';
|
||||
case 'ON_CALL_SCHEDULE':
|
||||
return 'Schedules';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
getOptionLabel={a => a.name}
|
||||
renderOption={a => (
|
||||
<div className={classes.optionWrapper}>
|
||||
<Typography noWrap>{a.name}</Typography>
|
||||
</div>
|
||||
)}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Responder"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: 'new-password', // disable autocomplete and autofill
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={!canAssign}
|
||||
onClick={handleAssign}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Incident } from '../../types';
|
||||
import { ilertApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
link: {
|
||||
lineHeight: '22px',
|
||||
},
|
||||
});
|
||||
|
||||
export const IncidentLink = ({ incident }: { incident: Incident | null }) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!incident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classes.link}
|
||||
href={ilertApi.getIncidentDetailsURL(incident)}
|
||||
>
|
||||
#{incident.id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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 { alertApiRef, identityApiRef, useApi } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { useNewIncident } from '../../hooks/useNewIncident';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { AlertSource } from '../../types';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
width: '100%',
|
||||
},
|
||||
option: {
|
||||
fontSize: 15,
|
||||
'& > span': {
|
||||
marginRight: 10,
|
||||
fontSize: 18,
|
||||
},
|
||||
},
|
||||
optionWrapper: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
sourceImage: {
|
||||
height: 22,
|
||||
paddingRight: 4,
|
||||
},
|
||||
}));
|
||||
|
||||
export const IncidentNewModal = ({
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
refetchIncidents,
|
||||
initialAlertSource,
|
||||
entityName,
|
||||
}: {
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (open: boolean) => void;
|
||||
refetchIncidents: () => void;
|
||||
initialAlertSource?: AlertSource | null;
|
||||
entityName?: string;
|
||||
}) => {
|
||||
const [
|
||||
{ alertSources, alertSource, summary, details, isLoading },
|
||||
{ setAlertSource, setSummary, setDetails, setIsLoading },
|
||||
] = useNewIncident(isModalOpened, initialAlertSource);
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const userName = identityApi.getUserId();
|
||||
const source = window.location.toString();
|
||||
const classes = useStyles();
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleClose = () => {
|
||||
setIsModalOpened(false);
|
||||
};
|
||||
|
||||
let integrationKey = '';
|
||||
if (initialAlertSource && initialAlertSource.integrationKey) {
|
||||
integrationKey = initialAlertSource.integrationKey;
|
||||
} else if (alertSource && alertSource.integrationKey) {
|
||||
integrationKey = alertSource.integrationKey;
|
||||
}
|
||||
const handleCreate = () => {
|
||||
if (!integrationKey) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const success = await ilertApi.createIncident({
|
||||
integrationKey,
|
||||
summary,
|
||||
details,
|
||||
userName,
|
||||
source,
|
||||
});
|
||||
if (success) {
|
||||
alertApi.post({ message: 'Incident created.' });
|
||||
refetchIncidents();
|
||||
}
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
setIsModalOpened(false);
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const canCreate = !!integrationKey && !!summary;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpened}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="create-incident-form-title"
|
||||
>
|
||||
<DialogTitle id="create-incident-form-title">
|
||||
{entityName ? (
|
||||
<div>
|
||||
This action will trigger an incident for{' '}
|
||||
<strong>"{entityName}"</strong>.
|
||||
</div>
|
||||
) : (
|
||||
'New incident'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="info">
|
||||
<Typography variant="body1" gutterBottom align="justify">
|
||||
Please describe the problem you want to report. Be as descriptive as
|
||||
possible. Your signed in user and a reference to the current page
|
||||
will automatically be amended to the alarm so that the receiver can
|
||||
reach out to you if necessary.
|
||||
</Typography>
|
||||
</Alert>
|
||||
{!initialAlertSource ? (
|
||||
<Autocomplete
|
||||
disabled={isLoading}
|
||||
options={alertSources}
|
||||
value={alertSource}
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
option: classes.option,
|
||||
}}
|
||||
onChange={(_event: any, newValue: any) => {
|
||||
setAlertSource(newValue);
|
||||
}}
|
||||
autoHighlight
|
||||
getOptionLabel={a => a.name}
|
||||
renderOption={a => (
|
||||
<div className={classes.optionWrapper}>
|
||||
<img
|
||||
src={prefersDarkMode ? a.lightIconUrl : a.iconUrl}
|
||||
alt={a.name}
|
||||
className={classes.sourceImage}
|
||||
/>
|
||||
<Typography noWrap>{a.name}</Typography>
|
||||
</div>
|
||||
)}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Alert Source"
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: 'new-password', // disable autocomplete and autofill
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<TextField
|
||||
disabled={isLoading}
|
||||
label="Summary"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
}}
|
||||
value={summary}
|
||||
onChange={event => {
|
||||
setSummary(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
disabled={isLoading}
|
||||
label="Details"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
}}
|
||||
value={details}
|
||||
onChange={event => {
|
||||
setDetails(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={!canCreate}
|
||||
onClick={handleCreate}
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { StatusError, StatusOK } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { ACCEPTED, Incident, PENDING, RESOLVED } from '../../types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
denseListIcon: {
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export const incidentStatusLabels = {
|
||||
[RESOLVED]: 'Resolved',
|
||||
[ACCEPTED]: 'Accepted',
|
||||
[PENDING]: 'Pending',
|
||||
} as Record<string, string>;
|
||||
|
||||
export const IncidentStatus = ({ incident }: { incident: Incident }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Tooltip title={incident.status} placement="top">
|
||||
<div className={classes.denseListIcon}>
|
||||
{incident.status === 'PENDING' ? <StatusError /> : <StatusOK />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 * from './IncidentActionsMenu';
|
||||
export * from './IncidentStatus';
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { Content, ContentHeader, SupportButton } from '@backstage/core';
|
||||
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';
|
||||
import { IncidentNewModal } from '../Incident/IncidentNewModal';
|
||||
|
||||
export const IncidentsPage = () => {
|
||||
const [
|
||||
{ tableState, states, incidents, incidentsCount, isLoading, error },
|
||||
{
|
||||
onIncidentStatesChange,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
onIncidentChanged,
|
||||
refetchIncidents,
|
||||
setIsLoading,
|
||||
},
|
||||
] = useIncidents(true);
|
||||
|
||||
const [isModalOpened, setIsModalOpened] = React.useState(false);
|
||||
|
||||
const handleCreateNewIncidentClick = () => {
|
||||
setIsModalOpened(true);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingAuthorizationHeaderError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert data-testid="error-message" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader title="Incidents">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleCreateNewIncidentClick}
|
||||
>
|
||||
Create Incident
|
||||
</Button>
|
||||
<IncidentNewModal
|
||||
isModalOpened={isModalOpened}
|
||||
setIsModalOpened={setIsModalOpened}
|
||||
refetchIncidents={refetchIncidents}
|
||||
/>
|
||||
<SupportButton>
|
||||
This helps you to bring iLert into your developer portal.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<IncidentsTable
|
||||
incidents={incidents}
|
||||
incidentsCount={incidentsCount}
|
||||
tableState={tableState}
|
||||
states={states}
|
||||
onIncidentChanged={onIncidentChanged}
|
||||
onIncidentStatesChange={onIncidentStatesChange}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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 { Table, TableColumn, useApi } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { ilertApiRef, TableState } from '../../api';
|
||||
import { Incident, IncidentStatus } from '../../types';
|
||||
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 { IncidentActionsMenu } from '../Incident/IncidentActionsMenu';
|
||||
import { IncidentLink } from '../Incident/IncidentLink';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
empty: {
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const IncidentsTable = ({
|
||||
incidents,
|
||||
incidentsCount,
|
||||
tableState,
|
||||
states,
|
||||
isLoading,
|
||||
onIncidentChanged,
|
||||
setIsLoading,
|
||||
onIncidentStatesChange,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
compact,
|
||||
}: {
|
||||
incidents: Incident[];
|
||||
incidentsCount: number;
|
||||
tableState: TableState;
|
||||
states: IncidentStatus[];
|
||||
isLoading: boolean;
|
||||
onIncidentChanged: (incident: Incident) => void;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
onIncidentStatesChange: (states: IncidentStatus[]) => void;
|
||||
onChangePage: (page: number) => void;
|
||||
onChangeRowsPerPage: (pageSize: number) => void;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const xsColumnStyle = {
|
||||
width: '5%',
|
||||
maxWidth: '5%',
|
||||
};
|
||||
const smColumnStyle = {
|
||||
width: '10%',
|
||||
maxWidth: '10%',
|
||||
};
|
||||
const mdColumnStyle = {
|
||||
width: '15%',
|
||||
maxWidth: '15%',
|
||||
};
|
||||
const lgColumnStyle = {
|
||||
width: '20%',
|
||||
maxWidth: '20%',
|
||||
};
|
||||
const xlColumnStyle = {
|
||||
width: '30%',
|
||||
maxWidth: '30%',
|
||||
};
|
||||
|
||||
const idColumn: TableColumn = {
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
highlight: true,
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => <IncidentLink incident={rowData as Incident} />,
|
||||
};
|
||||
const summaryColumn: TableColumn = {
|
||||
title: 'Summary',
|
||||
field: 'summary',
|
||||
cellStyle: !compact ? xlColumnStyle : undefined,
|
||||
headerStyle: !compact ? xlColumnStyle : undefined,
|
||||
render: rowData => <Typography>{(rowData as Incident).summary}</Typography>,
|
||||
};
|
||||
const sourceColumn: TableColumn = {
|
||||
title: 'Source',
|
||||
field: 'source',
|
||||
cellStyle: mdColumnStyle,
|
||||
headerStyle: mdColumnStyle,
|
||||
render: rowData => (
|
||||
<AlertSourceLink alertSource={(rowData as Incident).alertSource} />
|
||||
),
|
||||
};
|
||||
const durationColumn: TableColumn = {
|
||||
title: 'Duration',
|
||||
field: 'reportTime',
|
||||
type: 'datetime',
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
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),
|
||||
),
|
||||
)
|
||||
.humanize()}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
const assignedToColumn: TableColumn = {
|
||||
title: 'Assigned to',
|
||||
field: 'assignedTo',
|
||||
cellStyle: !compact ? mdColumnStyle : lgColumnStyle,
|
||||
headerStyle: !compact ? mdColumnStyle : lgColumnStyle,
|
||||
render: rowData => (
|
||||
<Typography noWrap>
|
||||
{ilertApi.getUserInitials((rowData as Incident).assignedTo)}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
const priorityColumn: TableColumn = {
|
||||
title: 'Priority',
|
||||
field: 'priority',
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => (
|
||||
<Typography noWrap>
|
||||
{(rowData as Incident).priority === 'HIGH' ? 'High' : 'Low'}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
const statusColumn: TableColumn = {
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
cellStyle: xsColumnStyle,
|
||||
headerStyle: xsColumnStyle,
|
||||
render: rowData => <StatusChip incident={rowData as Incident} />,
|
||||
};
|
||||
const actionsColumn: TableColumn = {
|
||||
title: '',
|
||||
field: '',
|
||||
cellStyle: xsColumnStyle,
|
||||
headerStyle: xsColumnStyle,
|
||||
render: rowData => (
|
||||
<IncidentActionsMenu
|
||||
incident={rowData as Incident}
|
||||
onIncidentChanged={onIncidentChanged}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = compact
|
||||
? [
|
||||
summaryColumn,
|
||||
durationColumn,
|
||||
assignedToColumn,
|
||||
statusColumn,
|
||||
actionsColumn,
|
||||
]
|
||||
: [
|
||||
idColumn,
|
||||
summaryColumn,
|
||||
sourceColumn,
|
||||
durationColumn,
|
||||
assignedToColumn,
|
||||
priorityColumn,
|
||||
statusColumn,
|
||||
actionsColumn,
|
||||
];
|
||||
let tableStyle: React.CSSProperties = {};
|
||||
if (compact) {
|
||||
tableStyle = {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
minWidth: '0',
|
||||
height: 'calc(100% - 10px)',
|
||||
boxShadow: 'none !important',
|
||||
borderRadius: 'none !important',
|
||||
};
|
||||
} else {
|
||||
tableStyle = {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
style={tableStyle}
|
||||
options={{
|
||||
sorting: false,
|
||||
search: !compact,
|
||||
paging: !compact,
|
||||
actionsColumnIndex: -1,
|
||||
pageSize: tableState.pageSize,
|
||||
pageSizeOptions: !compact ? [10, 20, 50, 100] : [3, 10, 20, 50, 100],
|
||||
padding: 'dense',
|
||||
loadingType: 'overlay',
|
||||
showEmptyDataSourceMessage: !isLoading,
|
||||
showTitle: true,
|
||||
toolbar: true,
|
||||
}}
|
||||
emptyContent={
|
||||
<Typography color="textSecondary" className={classes.empty}>
|
||||
No incidents right now
|
||||
</Typography>
|
||||
}
|
||||
title={
|
||||
!compact ? (
|
||||
<TableTitle
|
||||
incidentStates={states}
|
||||
onIncidentStatesChange={onIncidentStatesChange}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="button" color="textSecondary">
|
||||
INCIDENTS
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
page={tableState.page}
|
||||
totalCount={incidentsCount}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
// localization={{ header: { actions: undefined } }}
|
||||
columns={columns}
|
||||
data={incidents}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { Chip, withStyles } from '@material-ui/core';
|
||||
import { Incident, PENDING, ACCEPTED, RESOLVED } from '../../types';
|
||||
import { incidentStatusLabels } from '../Incident/IncidentStatus';
|
||||
|
||||
const ResolvedChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#4caf50',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
const AcceptedChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#ffb74d',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
const PendingChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#d32f2f',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
export const StatusChip = ({ incident }: { incident: Incident }) => {
|
||||
const label = `${incidentStatusLabels[incident.status]}`;
|
||||
|
||||
switch (incident.status) {
|
||||
case RESOLVED:
|
||||
return <ResolvedChip label={label} size="small" />;
|
||||
case ACCEPTED:
|
||||
return <AcceptedChip label={label} size="small" />;
|
||||
case PENDING:
|
||||
return <PendingChip label={label} size="small" />;
|
||||
default:
|
||||
return <Chip label={label} size="small" />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 { PENDING, ACCEPTED, RESOLVED, IncidentStatus } from '../../types';
|
||||
import { incidentStatusLabels } from '../Incident/IncidentStatus';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
label: {
|
||||
marginTop: 8,
|
||||
marginRight: 4,
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
maxWidth: 300,
|
||||
},
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export const TableTitle = ({
|
||||
incidentStates,
|
||||
onIncidentStatesChange,
|
||||
}: {
|
||||
incidentStates: IncidentStatus[];
|
||||
onIncidentStatesChange: (states: IncidentStatus[]) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const handleIncidentStatusSelectChange = (event: any) => {
|
||||
onIncidentStatesChange(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography noWrap className={classes.label}>
|
||||
Status:
|
||||
</Typography>
|
||||
<FormControl
|
||||
className={classes.formControl}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
<Select
|
||||
id="incidents-status-select"
|
||||
multiple
|
||||
value={incidentStates}
|
||||
onChange={handleIncidentStatusSelectChange}
|
||||
renderValue={(selected: any) => selected.join(', ')}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{[PENDING, ACCEPTED, RESOLVED].map(state => (
|
||||
<MenuItem key={state} value={state}>
|
||||
<Checkbox
|
||||
checked={incidentStates.indexOf(state as IncidentStatus) > -1}
|
||||
/>
|
||||
<ListItemText primary={incidentStatusLabels[state]} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 * from './IncidentsPage';
|
||||
export * from './IncidentsTable';
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 { useApi, ItemCardGrid, Progress } 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';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
margin: 16,
|
||||
width: 'calc(100% - 32px)',
|
||||
},
|
||||
|
||||
cardHeader: {
|
||||
maxWidth: '100%',
|
||||
},
|
||||
|
||||
cardContent: {
|
||||
marginLeft: 80,
|
||||
borderLeft: '1px #808289 solid',
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
indicatorNext: {
|
||||
position: 'absolute',
|
||||
top: 'calc(40% - 10px)',
|
||||
left: -8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
background: '#92949c !important',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
|
||||
indicatorCurrent: {
|
||||
position: 'absolute',
|
||||
top: 'calc(40% - 10px)',
|
||||
left: -8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
background: '#ffb74d !important',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
|
||||
beforeText: {
|
||||
position: 'absolute',
|
||||
top: 'calc(31% - 10px)',
|
||||
left: -78,
|
||||
width: 65,
|
||||
height: 20,
|
||||
textAlign: 'center',
|
||||
color: '#808289',
|
||||
},
|
||||
|
||||
marginBottom: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
|
||||
link: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 700,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'block',
|
||||
},
|
||||
}));
|
||||
|
||||
export const OnCallSchedulesGrid = ({
|
||||
onCallSchedules,
|
||||
isLoading,
|
||||
refetchOnCallSchedules,
|
||||
}: {
|
||||
onCallSchedules: Schedule[];
|
||||
isLoading: boolean;
|
||||
refetchOnCallSchedules: () => void;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (isLoading) {
|
||||
return <Progress />;
|
||||
}
|
||||
return (
|
||||
<ItemCardGrid data-testid="docs-explore">
|
||||
{!onCallSchedules?.length
|
||||
? null
|
||||
: onCallSchedules.map((schedule, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader
|
||||
classes={{ content: classes.cardHeader }}
|
||||
title={
|
||||
<Link
|
||||
href={ilertApi.getScheduleDetailsURL(schedule)}
|
||||
className={classes.link}
|
||||
>
|
||||
{schedule.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<CardContent className={classes.cardContent}>
|
||||
<div className={classes.indicatorCurrent} />
|
||||
<OnCallShiftItem
|
||||
shift={schedule.currentShift}
|
||||
scheduleId={schedule.id}
|
||||
refetchOnCallSchedules={refetchOnCallSchedules}
|
||||
/>
|
||||
<Typography className={classes.beforeText} variant="body2">
|
||||
On call now
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardContent
|
||||
className={`${classes.cardContent} ${classes.marginBottom}`}
|
||||
>
|
||||
<div className={classes.indicatorNext} />
|
||||
<OnCallShiftItem
|
||||
shift={schedule.nextShift}
|
||||
scheduleId={schedule.id}
|
||||
refetchOnCallSchedules={refetchOnCallSchedules}
|
||||
/>
|
||||
<Typography className={classes.beforeText} variant="body2">
|
||||
Next on call
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 { Content, ContentHeader, SupportButton } from '@backstage/core';
|
||||
import { UnauthorizedError } from '../../api';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { OnCallSchedulesGrid } from './OnCallSchedulesGrid';
|
||||
import { MissingAuthorizationHeaderError } from '../Errors';
|
||||
import { useOnCallSchedules } from '../../hooks/useOnCallSchedules';
|
||||
|
||||
export const OnCallSchedulesPage = () => {
|
||||
const [
|
||||
{ onCallSchedules, isLoading, error },
|
||||
{ refetchOnCallSchedules },
|
||||
] = useOnCallSchedules();
|
||||
|
||||
if (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingAuthorizationHeaderError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert data-testid="error-message" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader title="Who is on call?">
|
||||
<SupportButton>
|
||||
This helps you to bring iLert into your developer portal.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<OnCallSchedulesGrid
|
||||
onCallSchedules={onCallSchedules}
|
||||
isLoading={isLoading}
|
||||
refetchOnCallSchedules={refetchOnCallSchedules}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 Button from '@material-ui/core/Button';
|
||||
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 { makeStyles } from '@material-ui/core/styles';
|
||||
import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
button: {
|
||||
marginTop: 4,
|
||||
padding: 0,
|
||||
lineHeight: 1.8,
|
||||
'& span': {
|
||||
lineHeight: 1.8,
|
||||
fontSize: '0.65rem',
|
||||
},
|
||||
'& svg': {
|
||||
fontSize: '0.85rem !important',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const OnCallShiftItem = ({
|
||||
scheduleId,
|
||||
shift,
|
||||
refetchOnCallSchedules,
|
||||
}: {
|
||||
scheduleId: number;
|
||||
shift: Shift;
|
||||
refetchOnCallSchedules: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const [isModalOpened, setIsModalOpened] = React.useState(false);
|
||||
|
||||
const handleOverride = () => {
|
||||
setIsModalOpened(true);
|
||||
};
|
||||
|
||||
if (!shift || !shift.start) {
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
<Grid item sm={12}>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Nobody
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
{shift && shift.user ? (
|
||||
<Grid item sm={12}>
|
||||
<Typography variant="subtitle1" noWrap>
|
||||
{`${shift.user.firstName} ${shift.user.lastName} (${shift.user.username})`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : 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')}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item sm={12}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
startIcon={<RepeatIcon />}
|
||||
onClick={handleOverride}
|
||||
>
|
||||
<Typography variant="overline">Override shift</Typography>
|
||||
</Button>
|
||||
<ShiftOverrideModal
|
||||
scheduleId={scheduleId}
|
||||
shift={shift}
|
||||
refetchOnCallSchedules={refetchOnCallSchedules}
|
||||
isModalOpened={isModalOpened}
|
||||
setIsModalOpened={setIsModalOpened}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 * from './OnCallSchedulesPage';
|
||||
export * from './OnCallSchedulesGrid';
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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 { alertApiRef, useApi } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { Typography } from '@material-ui/core';
|
||||
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';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
width: '100%',
|
||||
},
|
||||
option: {
|
||||
fontSize: 15,
|
||||
'& > span': {
|
||||
marginRight: 10,
|
||||
fontSize: 18,
|
||||
},
|
||||
},
|
||||
optionWrapper: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
sourceImage: {
|
||||
height: 22,
|
||||
paddingRight: 4,
|
||||
},
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export const ShiftOverrideModal = ({
|
||||
scheduleId,
|
||||
shift,
|
||||
refetchOnCallSchedules,
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
}: {
|
||||
scheduleId: number;
|
||||
shift: Shift;
|
||||
refetchOnCallSchedules: () => void;
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (isModalOpened: boolean) => void;
|
||||
}) => {
|
||||
const [
|
||||
{ isLoading, users, user, start, end },
|
||||
{ setUser, setStart, setEnd, setIsLoading },
|
||||
] = useShiftOverride(shift, isModalOpened);
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsModalOpened(false);
|
||||
};
|
||||
|
||||
const handleOverride = () => {
|
||||
if (!shift || !shift.user) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const success = await ilertApi.overrideShift(
|
||||
scheduleId,
|
||||
user.id,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
if (success) {
|
||||
alertApi.post({ message: 'Shift overridden.' });
|
||||
refetchOnCallSchedules();
|
||||
}
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
setIsModalOpened(false);
|
||||
}, 250);
|
||||
};
|
||||
|
||||
if (!shift) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpened}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="override-shift-form-title"
|
||||
>
|
||||
<DialogTitle id="override-shift-form-title">Shift override</DialogTitle>
|
||||
<DialogContent>
|
||||
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||
<Autocomplete
|
||||
disabled={isLoading}
|
||||
options={users}
|
||||
value={user}
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
option: classes.option,
|
||||
}}
|
||||
onChange={(_event: any, newValue: any) => {
|
||||
setUser(newValue);
|
||||
}}
|
||||
autoHighlight
|
||||
getOptionLabel={a => ilertApi.getUserInitials(a)}
|
||||
renderOption={a => (
|
||||
<div className={classes.optionWrapper}>
|
||||
<Typography noWrap>{ilertApi.getUserInitials(a)}</Typography>
|
||||
</div>
|
||||
)}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="User"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: 'new-password', // disable autocomplete and autofill
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DateTimePicker
|
||||
label="Start"
|
||||
inputVariant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
ampm={false}
|
||||
value={start}
|
||||
className={classes.formControl}
|
||||
onChange={date => {
|
||||
setStart(date ? date.toISOString() : '');
|
||||
}}
|
||||
/>
|
||||
<DateTimePicker
|
||||
label="End"
|
||||
inputVariant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
ampm={false}
|
||||
value={end}
|
||||
className={classes.formControl}
|
||||
onChange={date => {
|
||||
setEnd(date ? date.toISOString() : '');
|
||||
}}
|
||||
/>
|
||||
</MuiPickersUtilsProvider>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
onClick={handleOverride}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
Override
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 { alertApiRef, 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 { UptimeMonitor } from '../../types';
|
||||
|
||||
export const UptimeMonitorActionsMenu = ({
|
||||
uptimeMonitor,
|
||||
onUptimeMonitorChanged,
|
||||
}: {
|
||||
uptimeMonitor: UptimeMonitor;
|
||||
onUptimeMonitorChanged?: (uptimeMonitor: UptimeMonitor) => void;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const callback = onUptimeMonitorChanged || ((_: UptimeMonitor): void => {});
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handlePause = async (): Promise<void> => {
|
||||
try {
|
||||
const newUptimeMonitor = await ilertApi.pauseUptimeMonitor(uptimeMonitor);
|
||||
handleCloseMenu();
|
||||
alertApi.post({ message: 'Uptime monitor paused.' });
|
||||
|
||||
callback(newUptimeMonitor);
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = async (): Promise<void> => {
|
||||
try {
|
||||
const newUptimeMonitor = await ilertApi.resumeUptimeMonitor(
|
||||
uptimeMonitor,
|
||||
);
|
||||
handleCloseMenu();
|
||||
alertApi.post({ message: 'Uptime monitor resumed.' });
|
||||
|
||||
callback(newUptimeMonitor);
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenReport = async (): Promise<void> => {
|
||||
try {
|
||||
const um = await ilertApi.fetchUptimeMonitor(uptimeMonitor.id);
|
||||
handleCloseMenu();
|
||||
window.open(um.shareUrl, '_blank');
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id={`uptime-monitor-actions-menu-${uptimeMonitor.id}`}
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
style: { maxHeight: 48 * 4.5 },
|
||||
}}
|
||||
>
|
||||
{uptimeMonitor.paused ? (
|
||||
<MenuItem key="ack" onClick={handleResume}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
Resume
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
{!uptimeMonitor.paused ? (
|
||||
<MenuItem key="close" onClick={handlePause}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
Pause
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
<MenuItem key="report" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link onClick={handleOpenReport}>View Report</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link href={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}>
|
||||
View in iLert
|
||||
</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { UptimeMonitor } from '../../types';
|
||||
import { ilertApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
link: {
|
||||
lineHeight: '22px',
|
||||
},
|
||||
});
|
||||
|
||||
export const UptimeMonitorLink = ({
|
||||
uptimeMonitor,
|
||||
}: {
|
||||
uptimeMonitor: UptimeMonitor | null;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!uptimeMonitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classes.link}
|
||||
href={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}
|
||||
>
|
||||
#{uptimeMonitor.id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 * from './UptimeMonitorActionsMenu';
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 Chip from '@material-ui/core/Chip';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { UptimeMonitor } from '../../types';
|
||||
|
||||
const UpChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#4caf50',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
const DownChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#d32f2f',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
const UnknownChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#92949c',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
export const uptimeMonitorStatusLabels = {
|
||||
['up']: 'Up',
|
||||
['down']: 'Down',
|
||||
['unknown']: 'Unknown',
|
||||
} as Record<string, string>;
|
||||
|
||||
export const StatusChip = ({
|
||||
uptimeMonitor,
|
||||
}: {
|
||||
uptimeMonitor: UptimeMonitor;
|
||||
}) => {
|
||||
let label = `${uptimeMonitorStatusLabels[uptimeMonitor.status]}`;
|
||||
|
||||
if (uptimeMonitor.paused) {
|
||||
label = 'Paused';
|
||||
return <UnknownChip label={label} size="small" />;
|
||||
}
|
||||
|
||||
switch (uptimeMonitor.status) {
|
||||
case 'up':
|
||||
return <UpChip label={label} size="small" />;
|
||||
case 'down':
|
||||
return <DownChip label={label} size="small" />;
|
||||
case 'unknown':
|
||||
return <UnknownChip label={label} size="small" />;
|
||||
default:
|
||||
return <Chip label={label} size="small" />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { UptimeMonitor } from '../../types';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
export const UptimeMonitorCheckType = ({
|
||||
uptimeMonitor,
|
||||
}: {
|
||||
uptimeMonitor: UptimeMonitor;
|
||||
}) => {
|
||||
switch (uptimeMonitor.region) {
|
||||
case 'EU':
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
>{`${uptimeMonitor.checkType.toUpperCase()} 🇩🇪`}</Typography>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
>{`${uptimeMonitor.checkType.toUpperCase()} 🇺🇸`}</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 { Content, ContentHeader, SupportButton } from '@backstage/core';
|
||||
import { UnauthorizedError } from '../../api';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { UptimeMonitorsTable } from './UptimeMonitorsTable';
|
||||
import { MissingAuthorizationHeaderError } from '../Errors';
|
||||
import { useUptimeMonitors } from '../../hooks/useUptimeMonitors';
|
||||
|
||||
export const UptimeMonitorsPage = () => {
|
||||
const [
|
||||
{ tableState, uptimeMonitors, isLoading, error },
|
||||
{ onChangePage, onChangeRowsPerPage, onUptimeMonitorChanged },
|
||||
] = useUptimeMonitors();
|
||||
|
||||
if (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingAuthorizationHeaderError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert data-testid="error-message" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader title="Uptime Monitors">
|
||||
<SupportButton>
|
||||
This helps you to bring iLert into your developer portal.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<UptimeMonitorsTable
|
||||
uptimeMonitors={uptimeMonitors}
|
||||
tableState={tableState}
|
||||
isLoading={isLoading}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
onUptimeMonitorChanged={onUptimeMonitorChanged}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 { makeStyles } from '@material-ui/core/styles';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { TableState } from '../../api';
|
||||
import { UptimeMonitor } from '../../types';
|
||||
import { StatusChip } from './StatusChip';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import moment from 'moment';
|
||||
import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink';
|
||||
import { UptimeMonitorCheckType } from './UptimeMonitorCheckType';
|
||||
import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu';
|
||||
import { UptimeMonitorLink } from '../UptimeMonitor/UptimeMonitorLink';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
empty: {
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const UptimeMonitorsTable = ({
|
||||
uptimeMonitors,
|
||||
tableState,
|
||||
isLoading,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
onUptimeMonitorChanged,
|
||||
}: {
|
||||
uptimeMonitors: UptimeMonitor[];
|
||||
tableState: TableState;
|
||||
isLoading: boolean;
|
||||
onChangePage: (page: number) => void;
|
||||
onChangeRowsPerPage: (pageSize: number) => void;
|
||||
onUptimeMonitorChanged: (uptimeMonitor: UptimeMonitor) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const smColumnStyle = {
|
||||
width: '5%',
|
||||
maxWidth: '5%',
|
||||
};
|
||||
const mdColumnStyle = {
|
||||
width: '10%',
|
||||
maxWidth: '10%',
|
||||
};
|
||||
const lgColumnStyle = {
|
||||
width: '15%',
|
||||
maxWidth: '15%',
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
highlight: true,
|
||||
cellStyle: mdColumnStyle,
|
||||
headerStyle: mdColumnStyle,
|
||||
render: rowData => (
|
||||
<UptimeMonitorLink uptimeMonitor={rowData as UptimeMonitor} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'name',
|
||||
render: rowData => (
|
||||
<Typography>{(rowData as UptimeMonitor).name}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Check Type',
|
||||
field: 'checkType',
|
||||
cellStyle: lgColumnStyle,
|
||||
headerStyle: lgColumnStyle,
|
||||
render: rowData => (
|
||||
<UptimeMonitorCheckType uptimeMonitor={rowData as UptimeMonitor} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Last state change',
|
||||
field: 'lastStatusChange',
|
||||
type: 'datetime',
|
||||
cellStyle: mdColumnStyle,
|
||||
headerStyle: mdColumnStyle,
|
||||
render: rowData => (
|
||||
<Typography noWrap>
|
||||
{moment
|
||||
.duration(
|
||||
moment((rowData as UptimeMonitor).lastStatusChange).diff(
|
||||
moment(),
|
||||
),
|
||||
)
|
||||
.humanize()}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Escalation policy',
|
||||
field: 'assignedTo',
|
||||
cellStyle: lgColumnStyle,
|
||||
headerStyle: lgColumnStyle,
|
||||
render: rowData => (
|
||||
<EscalationPolicyLink
|
||||
escalationPolicy={(rowData as UptimeMonitor).escalationPolicy}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => (
|
||||
<StatusChip uptimeMonitor={rowData as UptimeMonitor} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
field: '',
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => (
|
||||
<UptimeMonitorActionsMenu
|
||||
uptimeMonitor={rowData as UptimeMonitor}
|
||||
onUptimeMonitorChanged={onUptimeMonitorChanged}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
options={{
|
||||
sorting: true,
|
||||
search: true,
|
||||
paging: true,
|
||||
actionsColumnIndex: -1,
|
||||
pageSize: tableState.pageSize,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
padding: 'dense',
|
||||
loadingType: 'overlay',
|
||||
showEmptyDataSourceMessage: !isLoading,
|
||||
}}
|
||||
emptyContent={
|
||||
<Typography color="textSecondary" className={classes.empty}>
|
||||
No uptime monitor
|
||||
</Typography>
|
||||
}
|
||||
page={tableState.page}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
localization={{ header: { actions: undefined } }}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
data={uptimeMonitors}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 * from './UptimeMonitorsPage';
|
||||
export * from './UptimeMonitorsTable';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 * from './ILertPage';
|
||||
export * from './ILertCard';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 const ILERT_INTEGRATION_KEY = 'ilert.com/integration-key';
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { ILERT_INTEGRATION_KEY } from '../constants';
|
||||
|
||||
export function useILertEntity() {
|
||||
const { entity } = useEntity();
|
||||
const integrationKey =
|
||||
entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || '';
|
||||
const name = entity.metadata.name;
|
||||
|
||||
return { integrationKey, name };
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 { AlertSource, UptimeMonitor } from '../types';
|
||||
|
||||
export const useAlertSource = (integrationKey: string) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [alertSource, setAlertSource] = React.useState<AlertSource | null>(
|
||||
null,
|
||||
);
|
||||
const [isAlertSourceLoading, setIsAlertSourceLoading] = React.useState(false);
|
||||
const [
|
||||
uptimeMonitor,
|
||||
setUptimeMonitor,
|
||||
] = React.useState<UptimeMonitor | null>(null);
|
||||
const [isUptimeMonitorLoading, setIsUptimeMonitorLoading] = React.useState(
|
||||
false,
|
||||
);
|
||||
|
||||
const fetchAlertSourceCall = async () => {
|
||||
try {
|
||||
if (!integrationKey) {
|
||||
return;
|
||||
}
|
||||
setIsAlertSourceLoading(true);
|
||||
const data = await ilertApi.fetchAlertSource(integrationKey);
|
||||
setAlertSource(data || null);
|
||||
setIsAlertSourceLoading(false);
|
||||
} catch (e) {
|
||||
setIsAlertSourceLoading(false);
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
error: alertSourceError,
|
||||
retry: alertSourceRetry,
|
||||
} = useAsyncRetry(fetchAlertSourceCall, [integrationKey]);
|
||||
|
||||
const fetchUptimeMonitorCall = async () => {
|
||||
try {
|
||||
if (!alertSource || alertSource.integrationType !== 'MONITOR') {
|
||||
return;
|
||||
}
|
||||
setIsUptimeMonitorLoading(true);
|
||||
const data = await ilertApi.fetchUptimeMonitor(alertSource.id);
|
||||
setUptimeMonitor(data || null);
|
||||
setIsUptimeMonitorLoading(false);
|
||||
} catch (e) {
|
||||
setIsUptimeMonitorLoading(false);
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
error: uptimeMonitorError,
|
||||
retry: uptimeMonitorRetry,
|
||||
} = useAsyncRetry(fetchUptimeMonitorCall, [alertSource]);
|
||||
|
||||
const retry = () => {
|
||||
alertSourceRetry();
|
||||
uptimeMonitorRetry();
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
alertSource,
|
||||
uptimeMonitor,
|
||||
error: alertSourceError || uptimeMonitorError,
|
||||
isLoading: isAlertSourceLoading || isUptimeMonitorLoading,
|
||||
},
|
||||
{
|
||||
retry,
|
||||
setIsLoading: setIsAlertSourceLoading,
|
||||
refetchAlertSource: fetchAlertSourceCall,
|
||||
setAlertSource,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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, IncidentResponder } from '../types';
|
||||
|
||||
export const useAssignIncident = (incident: Incident | null, open: boolean) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [incidentRespondersList, setIncidentRespondersList] = React.useState<
|
||||
IncidentResponder[]
|
||||
>([]);
|
||||
const [
|
||||
incidentResponder,
|
||||
setIncidentResponder,
|
||||
] = React.useState<IncidentResponder | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
if (!incident || !open) {
|
||||
return;
|
||||
}
|
||||
const data = await ilertApi.fetchIncidentResponders(incident);
|
||||
if (data && Array.isArray(data)) {
|
||||
const groups = [
|
||||
'SUGGESTED',
|
||||
'USER',
|
||||
'ESCALATION_POLICY',
|
||||
'ON_CALL_SCHEDULE',
|
||||
];
|
||||
data.sort((a, b) => groups.indexOf(a.group) - groups.indexOf(b.group));
|
||||
setIncidentRespondersList(data);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [incident, open]);
|
||||
|
||||
return [
|
||||
{
|
||||
incidentRespondersList,
|
||||
incidentResponder,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setIncidentRespondersList,
|
||||
setIncidentResponder,
|
||||
setIsLoading,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 {
|
||||
GetIncidentsOpts,
|
||||
ilertApiRef,
|
||||
TableState,
|
||||
UnauthorizedError,
|
||||
} from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types';
|
||||
|
||||
export const useIncidents = (
|
||||
paging: boolean,
|
||||
alertSources?: number[] | string[],
|
||||
) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [tableState, setTableState] = React.useState<TableState>({
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [states, setStates] = React.useState<IncidentStatus[]>([
|
||||
ACCEPTED,
|
||||
PENDING,
|
||||
]);
|
||||
const [incidentsList, setIncidentsList] = React.useState<Incident[]>([]);
|
||||
const [incidentsCount, setIncidentsCount] = React.useState(0);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const fetchIncidentsCall = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const opts: GetIncidentsOpts = {
|
||||
states,
|
||||
alertSources,
|
||||
};
|
||||
if (paging) {
|
||||
opts.maxResults = tableState.pageSize;
|
||||
opts.startIndex = tableState.page * tableState.pageSize;
|
||||
}
|
||||
const data = await ilertApi.fetchIncidents(opts);
|
||||
setIncidentsList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchIncidentsCountCall = async () => {
|
||||
try {
|
||||
const count = await ilertApi.fetchIncidentsCount({ states });
|
||||
setIncidentsCount(count || 0);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [
|
||||
tableState,
|
||||
states,
|
||||
]);
|
||||
|
||||
const refetchIncidents = () => {
|
||||
setTableState({ ...tableState, page: 0 });
|
||||
Promise.all([fetchIncidentsCall(), fetchIncidentsCountCall()]);
|
||||
};
|
||||
|
||||
const fetchIncidentsCount = useAsyncRetry(fetchIncidentsCountCall, [states]);
|
||||
|
||||
const error = fetchIncidents.error || fetchIncidentsCount.error;
|
||||
const retry = () => {
|
||||
fetchIncidents.retry();
|
||||
fetchIncidentsCount.retry();
|
||||
};
|
||||
|
||||
const onIncidentChanged = (newIncident: Incident) => {
|
||||
let shouldRefetchIncidents = false;
|
||||
setIncidentsList(
|
||||
incidentsList.reduce((acc: Incident[], incident: Incident) => {
|
||||
if (newIncident.id === incident.id) {
|
||||
if (states.includes(newIncident.status)) {
|
||||
acc.push(newIncident);
|
||||
} else {
|
||||
shouldRefetchIncidents = true;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
acc.push(incident);
|
||||
return acc;
|
||||
}, []),
|
||||
);
|
||||
if (shouldRefetchIncidents) {
|
||||
refetchIncidents();
|
||||
}
|
||||
};
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
setTableState({ ...tableState, page });
|
||||
};
|
||||
const onChangeRowsPerPage = (p: number) => {
|
||||
setTableState({ ...tableState, pageSize: p });
|
||||
};
|
||||
const onIncidentStatesChange = (s: IncidentStatus[]) => {
|
||||
setStates(s);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
tableState,
|
||||
states,
|
||||
incidents: incidentsList,
|
||||
incidentsCount,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setTableState,
|
||||
setStates,
|
||||
setIncidentsList,
|
||||
setIsLoading,
|
||||
retry,
|
||||
onIncidentChanged,
|
||||
refetchIncidents,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
onIncidentStatesChange,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 { AlertSource } from '../types';
|
||||
|
||||
export const useNewIncident = (
|
||||
open: boolean,
|
||||
initialAlertSource?: AlertSource | null,
|
||||
) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [alertSourcesList, setAlertSourcesList] = React.useState<AlertSource[]>(
|
||||
[],
|
||||
);
|
||||
const [alertSource, setAlertSource] = React.useState<AlertSource | null>(
|
||||
null,
|
||||
);
|
||||
const [summary, setSummary] = React.useState('');
|
||||
const [details, setDetails] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const fetchAlertSources = useAsyncRetry(async () => {
|
||||
try {
|
||||
if (!open || initialAlertSource) {
|
||||
return;
|
||||
}
|
||||
const count = await ilertApi.fetchAlertSources();
|
||||
setAlertSourcesList(count || 0);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const error = fetchAlertSources.error;
|
||||
const retry = () => {
|
||||
fetchAlertSources.retry();
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
alertSources: alertSourcesList,
|
||||
alertSource: initialAlertSource ? initialAlertSource : alertSource,
|
||||
summary,
|
||||
details,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setAlertSourcesList,
|
||||
setAlertSource,
|
||||
setSummary,
|
||||
setDetails,
|
||||
setIsLoading,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { Schedule } from '../types';
|
||||
|
||||
export const useOnCallSchedules = () => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [onCallSchedulesList, setOnCallSchedulesList] = React.useState<
|
||||
Schedule[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const fetchOnCallSchedulesCall = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await ilertApi.fetchOnCallSchedules();
|
||||
setOnCallSchedulesList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const { error, retry } = useAsyncRetry(fetchOnCallSchedulesCall, []);
|
||||
|
||||
return [
|
||||
{
|
||||
onCallSchedules: onCallSchedulesList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
retry,
|
||||
setIsLoading,
|
||||
refetchOnCallSchedules: fetchOnCallSchedulesCall,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 { User, Shift } from '../types';
|
||||
|
||||
export const useShiftOverride = (s: Shift, isModalOpened: boolean) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [shift, setShift] = React.useState<Shift>(s);
|
||||
const [usersList, setUsersList] = React.useState<User[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
if (!isModalOpened) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const data = await ilertApi.fetchUsers();
|
||||
setUsersList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [isModalOpened]);
|
||||
|
||||
const setUser = (user: User) => {
|
||||
setShift({ ...shift, user });
|
||||
};
|
||||
|
||||
const setStart = (start: string) => {
|
||||
setShift({ ...shift, start });
|
||||
};
|
||||
|
||||
const setEnd = (end: string) => {
|
||||
setShift({ ...shift, end });
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
shift,
|
||||
users: usersList,
|
||||
user: shift.user,
|
||||
start: shift.start,
|
||||
end: shift.end,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
retry,
|
||||
setIsLoading,
|
||||
setUser,
|
||||
setStart,
|
||||
setEnd,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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, TableState, UnauthorizedError } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { UptimeMonitor } from '../types';
|
||||
|
||||
export const useUptimeMonitors = () => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [tableState, setTableState] = React.useState<TableState>({
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [uptimeMonitorsList, setUptimeMonitorsList] = React.useState<
|
||||
UptimeMonitor[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await ilertApi.fetchUptimeMonitors();
|
||||
setUptimeMonitorsList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (!(e instanceof UnauthorizedError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [tableState]);
|
||||
|
||||
const onUptimeMonitorChanged = (newUptimeMonitor: UptimeMonitor) => {
|
||||
setUptimeMonitorsList(
|
||||
uptimeMonitorsList.map(
|
||||
(uptimeMonitor: UptimeMonitor): UptimeMonitor => {
|
||||
if (newUptimeMonitor.id === uptimeMonitor.id) {
|
||||
return newUptimeMonitor;
|
||||
}
|
||||
|
||||
return uptimeMonitor;
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
setTableState({ ...tableState, page });
|
||||
};
|
||||
const onChangeRowsPerPage = (pageSize: number) => {
|
||||
setTableState({ ...tableState, pageSize });
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
tableState,
|
||||
uptimeMonitors: uptimeMonitorsList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setTableState,
|
||||
setUptimeMonitorsList,
|
||||
retry,
|
||||
onUptimeMonitorChanged,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
setIsLoading,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { IconComponent } from '@backstage/core';
|
||||
import ILertIconComponent from './assets/ilert.icon.svg';
|
||||
|
||||
export {
|
||||
ilertPlugin,
|
||||
ilertPlugin as plugin,
|
||||
ILertPage,
|
||||
EntityILertCard,
|
||||
} from './plugin';
|
||||
export {
|
||||
ILertPage as Router,
|
||||
isPluginApplicableToEntity,
|
||||
isPluginApplicableToEntity as isILertAvailable,
|
||||
ILertCard,
|
||||
} from './components';
|
||||
export * from './api';
|
||||
export * from './route-refs';
|
||||
export const ILertIcon: IconComponent = ILertIconComponent;
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { ilertPlugin } from './plugin';
|
||||
|
||||
describe('plugin-ilert', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(ilertPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
createRoutableExtension,
|
||||
createComponentExtension,
|
||||
} from '@backstage/core';
|
||||
import { ILertClient, ilertApiRef } from './api';
|
||||
import { iLertRouteRef } from './route-refs';
|
||||
|
||||
export const ilertPlugin = createPlugin({
|
||||
id: 'ilert',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: ilertApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, configApi }) =>
|
||||
ILertClient.fromConfig(configApi, discoveryApi),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
root: iLertRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const ILertPage = ilertPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () => import('./components').then(m => m.ILertPage),
|
||||
mountPoint: iLertRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityILertCard = ilertPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () => import('./components/ILertCard').then(m => m.ILertCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { createRouteRef } from '@backstage/core';
|
||||
import ILertIcon from './assets/ilert.icon.svg';
|
||||
|
||||
export const iLertRouteRef = createRouteRef({
|
||||
icon: ILertIcon,
|
||||
path: '/ilert',
|
||||
title: 'iLert',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* 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 Incident {
|
||||
id: number;
|
||||
summary: string;
|
||||
details: string;
|
||||
reportTime: string;
|
||||
resolvedOn: string;
|
||||
status: IncidentStatus;
|
||||
priority: IncidentPriority;
|
||||
incidentKey: string;
|
||||
alertSource: AlertSource | null;
|
||||
assignedTo: User | null;
|
||||
logEntries: LogEntry[];
|
||||
links: Link[];
|
||||
images: Image[];
|
||||
subscribers: Subscriber[];
|
||||
commentText: string;
|
||||
commentPublishToSubscribers: boolean;
|
||||
}
|
||||
|
||||
export const PENDING = 'PENDING';
|
||||
export const ACCEPTED = 'ACCEPTED';
|
||||
export const RESOLVED = 'RESOLVED';
|
||||
export type IncidentStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED;
|
||||
export type IncidentPriority = 'HIGH' | 'LOW';
|
||||
|
||||
export interface Link {
|
||||
href: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
src: string;
|
||||
href: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export type SubscriberType = 'TEAM' | 'USER';
|
||||
|
||||
export interface Subscriber {
|
||||
id: number;
|
||||
name: string;
|
||||
type: SubscriberType;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
logEntryType: string;
|
||||
text: string;
|
||||
incidentId?: number;
|
||||
iconName?: string;
|
||||
iconClass?: string;
|
||||
filterTypes?: string[];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
mobile: Phone;
|
||||
landline: Phone;
|
||||
timezone?: string;
|
||||
language?: Language;
|
||||
role?: UserRole;
|
||||
notificationPreferences?: any[];
|
||||
position: string;
|
||||
department: string;
|
||||
}
|
||||
|
||||
export type UserRole =
|
||||
| 'USER'
|
||||
| 'ADMIN'
|
||||
| 'STAKEHOLDER'
|
||||
| 'ACCOUNT_OWNER'
|
||||
| 'RESPONDER';
|
||||
export type Language = 'de' | 'en';
|
||||
export interface Phone {
|
||||
regionCode: string;
|
||||
number: string;
|
||||
}
|
||||
|
||||
export interface AlertSource {
|
||||
id: number;
|
||||
name: string;
|
||||
status: AlertSourceStatus;
|
||||
escalationPolicy: EscalationPolicy;
|
||||
integrationType: AlertSourceIntegrationType;
|
||||
integrationKey?: string;
|
||||
iconUrl?: string;
|
||||
lightIconUrl?: string;
|
||||
darkIconUrl?: string;
|
||||
incidentCreation?: AlertSourceIncidentCreation;
|
||||
incidentPriorityRule?: AlertSourceIncidentPriorityRule;
|
||||
emailFiltered?: boolean;
|
||||
emailResolveFiltered?: boolean;
|
||||
active?: boolean;
|
||||
emailPredicates?: AlertSourceEmailPredicate[];
|
||||
emailResolvePredicates?: AlertSourceEmailPredicate[];
|
||||
filterOperator?: AlertSourceFilterOperator;
|
||||
resolveFilterOperator?: AlertSourceFilterOperator;
|
||||
supportHours?: AlertSourceSupportHours;
|
||||
heartbeat?: AlertSourceHeartbeat;
|
||||
autotaskMetadata?: AlertSourceAutotaskMetadata;
|
||||
autoResolutionTimeout?: string;
|
||||
teams: TeamShort[];
|
||||
}
|
||||
|
||||
export interface TeamShort {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
user: User;
|
||||
role: 'STAKEHOLDER' | 'RESPONDER' | 'USER' | 'ADMIN';
|
||||
}
|
||||
|
||||
export type AlertSourceStatus =
|
||||
| 'PENDING'
|
||||
| 'ALL_ACCEPTED'
|
||||
| 'ALL_RESOLVED'
|
||||
| 'IN_MAINTENANCE'
|
||||
| 'DISABLED';
|
||||
export type AlertSourceIntegrationType =
|
||||
| 'NAGIOS'
|
||||
| 'ICINGA'
|
||||
| 'EMAIL'
|
||||
| 'SMS'
|
||||
| 'API'
|
||||
| 'CRN'
|
||||
| 'HEARTBEAT'
|
||||
| 'PRTG'
|
||||
| 'PINGDOM'
|
||||
| 'CLOUDWATCH'
|
||||
| 'AWSPHD'
|
||||
| 'STACKDRIVER'
|
||||
| 'INSTANA'
|
||||
| 'ZABBIX'
|
||||
| 'SOLARWINDS'
|
||||
| 'PROMETHEUS'
|
||||
| 'NEWRELIC'
|
||||
| 'GRAFANA'
|
||||
| 'GITHUB'
|
||||
| 'DATADOG'
|
||||
| 'UPTIMEROBOT'
|
||||
| 'APPDYNAMICS'
|
||||
| 'DYNATRACE'
|
||||
| 'TOPDESK'
|
||||
| 'STATUSCAKE'
|
||||
| 'MONITOR'
|
||||
| 'TOOL'
|
||||
| 'CHECKMK'
|
||||
| 'AUTOTASK'
|
||||
| 'AWSBUDGET'
|
||||
| 'KENTIXAM'
|
||||
| 'CONSUL'
|
||||
| 'ZAMMAD'
|
||||
| 'SIGNALFX'
|
||||
| 'SPLUNK'
|
||||
| 'KUBERNETES'
|
||||
| 'SEMATEXT'
|
||||
| 'SENTRY'
|
||||
| 'SUMOLOGIC'
|
||||
| 'RAYGUN'
|
||||
| 'MXTOOLBOX'
|
||||
| 'ESWATCHER'
|
||||
| 'AMAZONSNS'
|
||||
| 'KAPACITOR'
|
||||
| 'CORTEXXSOAR'
|
||||
| string;
|
||||
export type AlertSourceIncidentCreation =
|
||||
| 'ONE_INCIDENT_PER_EMAIL'
|
||||
| 'ONE_INCIDENT_PER_EMAIL_SUBJECT'
|
||||
| 'ONE_PENDING_INCIDENT_ALLOWED'
|
||||
| 'ONE_OPEN_INCIDENT_ALLOWED'
|
||||
| 'OPEN_RESOLVE_ON_EXTRACTION';
|
||||
export type AlertSourceFilterOperator = 'AND' | 'OR';
|
||||
export type AlertSourceIncidentPriorityRule =
|
||||
| 'HIGH'
|
||||
| 'LOW'
|
||||
| 'HIGH_DURING_SUPPORT_HOURS'
|
||||
| 'LOW_DURING_SUPPORT_HOURS';
|
||||
export interface AlertSourceEmailPredicate {
|
||||
field: 'EMAIL_FROM' | 'EMAIL_SUBJECT' | 'EMAIL_BODY';
|
||||
criteria:
|
||||
| 'CONTAINS_ANY_WORDS'
|
||||
| 'CONTAINS_NOT_WORDS'
|
||||
| 'CONTAINS_STRING'
|
||||
| 'CONTAINS_NOT_STRING'
|
||||
| 'IS_STRING'
|
||||
| 'IS_NOT_STRING'
|
||||
| 'MATCHES_REGEX'
|
||||
| 'MATCHES_NOT_REGEX';
|
||||
value: string;
|
||||
}
|
||||
export type AlertSourceTimeZone =
|
||||
| 'Europe/Berlin'
|
||||
| 'America/New_York'
|
||||
| 'America/Los_Angeles'
|
||||
| 'Asia/Istanbul';
|
||||
export interface AlertSourceSupportDay {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
export interface AlertSourceSupportHours {
|
||||
timezone: AlertSourceTimeZone;
|
||||
autoRaiseIncidents: boolean;
|
||||
supportDays: {
|
||||
MONDAY: AlertSourceSupportDay;
|
||||
TUESDAY: AlertSourceSupportDay;
|
||||
WEDNESDAY: AlertSourceSupportDay;
|
||||
THURSDAY: AlertSourceSupportDay;
|
||||
FRIDAY: AlertSourceSupportDay;
|
||||
SATURDAY: AlertSourceSupportDay;
|
||||
SUNDAY: AlertSourceSupportDay;
|
||||
};
|
||||
}
|
||||
export interface AlertSourceHeartbeat {
|
||||
summary: string;
|
||||
intervalSec: number;
|
||||
status: 'OVERDUE' | 'ON_TIME' | 'NEVER_RECEIVED';
|
||||
}
|
||||
|
||||
export interface AlertSourceAutotaskMetadata {
|
||||
userName: string;
|
||||
secret: string;
|
||||
apiIntegrationCode: string;
|
||||
webServer: string;
|
||||
}
|
||||
|
||||
export interface EscalationPolicy {
|
||||
id: number;
|
||||
name: string;
|
||||
escalationRules: EscalationRule[];
|
||||
newEscalationRule: EscalationRule;
|
||||
repeating?: boolean;
|
||||
frequency?: number;
|
||||
teams: TeamShort[];
|
||||
}
|
||||
|
||||
export interface EscalationRule {
|
||||
user: User | null;
|
||||
schedule: Schedule | null;
|
||||
escalationTimeout: number;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
id: number;
|
||||
name: string;
|
||||
timezone: string;
|
||||
startsOn: string;
|
||||
currentShift: Shift;
|
||||
nextShift: Shift;
|
||||
shifts: Shift[];
|
||||
overrides: Shift[];
|
||||
teams: TeamShort[];
|
||||
}
|
||||
|
||||
export interface Shift {
|
||||
user: User;
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export interface UptimeMonitor {
|
||||
id: number;
|
||||
name: string;
|
||||
region: 'EU' | 'US';
|
||||
checkType: 'http' | 'tcp' | 'udp' | 'ping';
|
||||
checkParams: UptimeMonitorCheckParams;
|
||||
intervalSec: number;
|
||||
timeoutMs: number;
|
||||
createIncidentAfterFailedChecks: number;
|
||||
paused: boolean;
|
||||
embedUrl: string;
|
||||
shareUrl: string;
|
||||
status: string;
|
||||
lastStatusChange: string;
|
||||
escalationPolicy: EscalationPolicy;
|
||||
teams: TeamShort[];
|
||||
}
|
||||
|
||||
export interface UptimeMonitorCheckParams {
|
||||
host?: string;
|
||||
port?: number;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface IncidentResponder {
|
||||
group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE';
|
||||
id: number;
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user