@@ -16,6 +16,7 @@
|
||||
/plugins/search-* @backstage/techdocs-core
|
||||
/plugins/techdocs @backstage/techdocs-core
|
||||
/plugins/techdocs-backend @backstage/techdocs-core
|
||||
/plugins/ilert @yacut
|
||||
/packages/search-common @backstage/techdocs-core
|
||||
/packages/techdocs-common @backstage/techdocs-core
|
||||
/.changeset/cost-insights-* @backstage/silver-lining
|
||||
|
||||
@@ -81,6 +81,13 @@ proxy:
|
||||
headers:
|
||||
Authorization: ${SENTRY_TOKEN}
|
||||
|
||||
'/ilert':
|
||||
target: https://api.ilert.com
|
||||
allowedMethods: ['GET', 'POST', 'PUT']
|
||||
allowedHeaders: ['Authorization']
|
||||
headers:
|
||||
Authorization: ${ILERT_AUTH_HEADER}
|
||||
|
||||
organization:
|
||||
name: My Company
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: iLert
|
||||
author: iLert
|
||||
authorUrl: https://www.ilert.com
|
||||
category: Monitoring
|
||||
description: iLert is a platform for alerting, on-call management and uptime monitoring targeted at DevOps and IT teams.
|
||||
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
|
||||
- uptime
|
||||
- on-call
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
rules: {
|
||||
quotes: ['error', 'single'],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
# @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 or creating tickets in external incident management tools.
|
||||
|
||||
## Overview
|
||||
|
||||
This plugin gives an overview about ongoing iLert incidents, on-call and uptime monitor status.
|
||||
See who is on-call, which incidents are active and trigger incidents directly from backstage for the configured alert sources.
|
||||
|
||||
In detail this plugin provides:
|
||||
|
||||
- Information details about the person on-call (all escalation levels of the current time)
|
||||
- A way to override the current on-call person
|
||||
- A list of active incidents
|
||||
- A way to trigger a new incident
|
||||
- A way to reassign/acknowledge/resolve an incident
|
||||
- A way to trigger an incident action
|
||||
- A way to trigger an immediate maintenance
|
||||
- A way to disable/enable an alert source
|
||||
- A list of uptime monitors
|
||||
|
||||
## Setup instructions
|
||||
|
||||
Install the plugin:
|
||||
|
||||
```bash
|
||||
# From the Backstage repository root
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-ilert
|
||||
```
|
||||
|
||||
Add it to the `EntityPage.tsx`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
isPluginApplicableToEntity as isILertAvailable,
|
||||
EntityILertCard,
|
||||
} from '@backstage/plugin-ilert';
|
||||
|
||||
// ...
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isILertAvailable}>
|
||||
<Grid item sm={6}>
|
||||
<EntityILertCard />
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>;
|
||||
// ...
|
||||
```
|
||||
|
||||
> To force an iLert card for each entity just add the `<EntityILertCard />` component. An instruction card will appear if no integration key is set.
|
||||
|
||||
## 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 by the plugin - for example:
|
||||
|
||||
```tsx
|
||||
import { ILertPage } from '@backstage/plugin-ilert';
|
||||
<FlatRoutes>
|
||||
// ...
|
||||
<Route path="/ilert" element={<ILertPage />} />
|
||||
// ...
|
||||
</FlatRoutes>;
|
||||
```
|
||||
|
||||
Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported by the plugin - for example:
|
||||
|
||||
```tsx
|
||||
import { ILertIcon } from '@backstage/plugin-ilert';
|
||||
<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:
|
||||
baseUrl: 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: ${ILERT_AUTH_HEADER}
|
||||
```
|
||||
|
||||
Then start the backend, passing the authorization header (bearer token or basic auth) as environment variable:
|
||||
|
||||
```bash
|
||||
$ ILERT_AUTH_HEADER='<ILERT_AUTH>' yarn start
|
||||
```
|
||||
|
||||
## Integration Key
|
||||
|
||||
The information displayed for each entity is based on the alert source integration key.
|
||||
|
||||
### 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
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: example
|
||||
annotations:
|
||||
ilert.com/integration-key: [INTEGRATION_KEY]
|
||||
```
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface Config {
|
||||
ilert?: {
|
||||
/**
|
||||
* Domain used by users to access iLert web UI.
|
||||
* Example: https://my-app.ilert.com/
|
||||
* @visibility frontend
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
/**
|
||||
* Path to use for requests via the proxy, defaults to /ilert/api
|
||||
* @visibility frontend
|
||||
*/
|
||||
proxyPath?: string;
|
||||
};
|
||||
}
|
||||
@@ -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,56 @@
|
||||
{
|
||||
"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.9",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/plugin-catalog-react": "^0.1.4",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@date-io/luxon": "1.x",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@material-ui/pickers": "^3.3.10",
|
||||
"humanize-duration": "^3.26.0",
|
||||
"luxon": "^1.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* 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 { AuthenticationError, ResponseError } from '@backstage/errors';
|
||||
import {
|
||||
AlertSource,
|
||||
EscalationPolicy,
|
||||
Incident,
|
||||
IncidentAction,
|
||||
IncidentResponder,
|
||||
OnCall,
|
||||
Schedule,
|
||||
UptimeMonitor,
|
||||
User,
|
||||
} from '../types';
|
||||
import {
|
||||
ILertApi,
|
||||
GetIncidentsOpts,
|
||||
GetIncidentsCountOpts,
|
||||
Options,
|
||||
EventRequest,
|
||||
} from './types';
|
||||
import { DateTime as dt } from 'luxon';
|
||||
|
||||
export const ilertApiRef = createApiRef<ILertApi>({
|
||||
id: 'plugin.ilert.service',
|
||||
description: 'Used to make requests towards iLert API',
|
||||
});
|
||||
|
||||
const DEFAULT_PROXY_PATH = '/ilert';
|
||||
const JSON_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
export class ILertClient implements ILertApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly proxyPath: string;
|
||||
private readonly baseUrl: string;
|
||||
|
||||
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
|
||||
const baseUrl: string =
|
||||
configApi.getOptionalString('ilert.baseUrl') ?? 'https://app.ilert.com';
|
||||
|
||||
return new ILertClient({
|
||||
discoveryApi: discoveryApi,
|
||||
baseUrl,
|
||||
proxyPath:
|
||||
configApi.getOptionalString('ilert.proxyPath') ?? DEFAULT_PROXY_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
constructor(opts: Options) {
|
||||
this.discoveryApi = opts.discoveryApi;
|
||||
this.baseUrl = opts.baseUrl;
|
||||
this.proxyPath = opts.proxyPath;
|
||||
}
|
||||
|
||||
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 AuthenticationError(
|
||||
'This request requires HTTP authentication.',
|
||||
);
|
||||
}
|
||||
if (!response.ok || response.status >= 400) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
const query = new URLSearchParams();
|
||||
if (opts?.maxResults !== undefined) {
|
||||
query.append('max-results', String(opts.maxResults));
|
||||
}
|
||||
if (opts?.startIndex !== undefined) {
|
||||
query.append('start-index', String(opts.startIndex));
|
||||
}
|
||||
if (opts?.alertSources !== undefined && Array.isArray(opts.alertSources)) {
|
||||
opts.alertSources.forEach((a: number) => {
|
||||
if (a) {
|
||||
query.append('alert-source', String(a));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (opts?.states !== undefined && 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: JSON_HEADERS,
|
||||
};
|
||||
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 fetchIncident(id: number): Promise<Incident> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${encodeURIComponent(id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchIncidentResponders(
|
||||
incident: Incident,
|
||||
): Promise<IncidentResponder[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${encodeURIComponent(incident.id)}/responders`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchIncidentActions(incident: Incident): Promise<IncidentAction[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async acceptIncident(
|
||||
incident: Incident,
|
||||
userName: string,
|
||||
): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
apiKey: incident.alertSource?.integrationKey || '',
|
||||
incidentKey: incident.incidentKey,
|
||||
summary: `from ${userName} via Backstage plugin`,
|
||||
eventType: 'ACCEPT',
|
||||
}),
|
||||
};
|
||||
|
||||
await this.fetch('/api/v1/events', init);
|
||||
return this.fetchIncident(incident.id);
|
||||
}
|
||||
|
||||
async resolveIncident(
|
||||
incident: Incident,
|
||||
userName: string,
|
||||
): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
apiKey: incident.alertSource?.integrationKey || '',
|
||||
incidentKey: incident.incidentKey,
|
||||
summary: `from ${userName} via Backstage plugin`,
|
||||
eventType: 'RESOLVE',
|
||||
}),
|
||||
};
|
||||
|
||||
await this.fetch('/api/v1/events', init);
|
||||
return this.fetchIncident(incident.id);
|
||||
}
|
||||
|
||||
async assignIncident(
|
||||
incident: Incident,
|
||||
responder: IncidentResponder,
|
||||
): Promise<Incident> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const query = new URLSearchParams();
|
||||
switch (responder.group) {
|
||||
case 'ESCALATION_POLICY':
|
||||
query.append('policy-id', String(responder.id));
|
||||
break;
|
||||
case 'ON_CALL_SCHEDULE':
|
||||
query.append('schedule-id', String(responder.id));
|
||||
break;
|
||||
default:
|
||||
query.append('user-id', String(responder.id));
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/incidents/${encodeURIComponent(
|
||||
incident.id,
|
||||
)}/assign?${query.toString()}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async triggerIncidentAction(
|
||||
incident: Incident,
|
||||
action: IncidentAction,
|
||||
): Promise<void> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
webhookId: action.webhookId,
|
||||
extensionId: action.extensionId,
|
||||
type: action.type,
|
||||
name: action.name,
|
||||
}),
|
||||
};
|
||||
|
||||
await this.fetch(
|
||||
`/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`,
|
||||
init,
|
||||
);
|
||||
}
|
||||
|
||||
async createIncident(eventRequest: EventRequest): Promise<boolean> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
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;
|
||||
}
|
||||
|
||||
async fetchUptimeMonitors(): Promise<UptimeMonitor[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/uptime-monitors', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchUptimeMonitor(id: number): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response: UptimeMonitor = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${encodeURIComponent(id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async pauseUptimeMonitor(
|
||||
uptimeMonitor: UptimeMonitor,
|
||||
): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ ...uptimeMonitor, paused: true }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async resumeUptimeMonitor(
|
||||
uptimeMonitor: UptimeMonitor,
|
||||
): Promise<UptimeMonitor> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ ...uptimeMonitor, paused: false }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchAlertSources(): Promise<AlertSource[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/alert-sources', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchAlertSource(
|
||||
idOrIntegrationKey: number | string,
|
||||
): Promise<AlertSource> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${encodeURIComponent(idOrIntegrationKey)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/on-calls?policies=${encodeURIComponent(
|
||||
alertSource.escalationPolicy.id,
|
||||
)}&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async enableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ ...alertSource, active: true }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async disableAlertSource(alertSource: AlertSource): Promise<AlertSource> {
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ ...alertSource, active: false }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async addImmediateMaintenance(
|
||||
alertSourceId: number,
|
||||
minutes: number,
|
||||
): Promise<void> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
start: dt.utc().toISO(),
|
||||
end: dt.utc().plus({ minutes }).toISO(),
|
||||
description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`,
|
||||
createdBy: 'Backstage',
|
||||
timezone: dt.local().zoneName,
|
||||
alertSources: [{ id: alertSourceId }],
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/maintenance-windows', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchOnCallSchedules(): Promise<Schedule[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/v1/schedules', init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchUsers(): Promise<User[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
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: JSON_HEADERS,
|
||||
body: JSON.stringify({ user: { id: userId }, start, end }),
|
||||
};
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/v1/schedules/${encodeURIComponent(scheduleId)}/overrides`,
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
getIncidentDetailsURL(incident: Incident): string {
|
||||
return `${this.baseUrl}/incident/view.jsf?id=${encodeURIComponent(
|
||||
incident.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getAlertSourceDetailsURL(alertSource: AlertSource | null): string {
|
||||
if (!alertSource) {
|
||||
return '';
|
||||
}
|
||||
return `${this.baseUrl}/source/view.jsf?id=${encodeURIComponent(
|
||||
alertSource.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string {
|
||||
return `${this.baseUrl}/policy/view.jsf?id=${encodeURIComponent(
|
||||
escalationPolicy.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string {
|
||||
return `${this.baseUrl}/uptime/view.jsf?id=${encodeURIComponent(
|
||||
uptimeMonitor.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getScheduleDetailsURL(schedule: Schedule): string {
|
||||
return `${this.baseUrl}/schedule/view.jsf?id=${encodeURIComponent(
|
||||
schedule.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getUserPhoneNumber(user: User | null) {
|
||||
return user?.mobile?.number || user?.landline?.number || '';
|
||||
}
|
||||
|
||||
getUserInitials(user: User | null) {
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (!user.firstName && !user.lastName) {
|
||||
return user.username;
|
||||
}
|
||||
return `${user.firstName} ${user.lastName} (${user.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 } from './client';
|
||||
export type {
|
||||
ILertApi,
|
||||
GetIncidentsCountOpts,
|
||||
GetIncidentsOpts,
|
||||
TableState,
|
||||
} from './types';
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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,
|
||||
IncidentAction,
|
||||
OnCall,
|
||||
} from '../types';
|
||||
|
||||
export type TableState = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
export type GetIncidentsOpts = {
|
||||
maxResults?: number;
|
||||
startIndex?: number;
|
||||
states?: IncidentStatus[];
|
||||
alertSources?: number[];
|
||||
};
|
||||
|
||||
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>;
|
||||
fetchIncident(id: number): Promise<Incident>;
|
||||
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
|
||||
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
|
||||
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
|
||||
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
|
||||
assignIncident(
|
||||
incident: Incident,
|
||||
responder: IncidentResponder,
|
||||
): Promise<Incident>;
|
||||
createIncident(eventRequest: EventRequest): Promise<boolean>;
|
||||
triggerIncidentAction(
|
||||
incident: Incident,
|
||||
action: IncidentAction,
|
||||
): Promise<void>;
|
||||
|
||||
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>;
|
||||
fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]>;
|
||||
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;
|
||||
getUserPhoneNumber(user: User | null): string;
|
||||
getUserInitials(user: User | null): string;
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
|
||||
/**
|
||||
* URL used by users to access iLert web UI.
|
||||
* Example: https://my-org.ilert.com/
|
||||
*/
|
||||
baseUrl: 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,70 @@
|
||||
/*
|
||||
* 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, Link } from '@backstage/core';
|
||||
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}
|
||||
to={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,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 { useApi, Link } from '@backstage/core';
|
||||
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}
|
||||
to={ilertApi.getEscalationPolicyDetailsURL(escalationPolicy)}
|
||||
>
|
||||
{escalationPolicy.name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 { AuthenticationError } from '@backstage/errors';
|
||||
import { ResponseErrorPanel } from '@backstage/core';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { ILERT_INTEGRATION_KEY_ANNOTATION } 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';
|
||||
import { ILertCardOnCall } from './ILertCardOnCall';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION]);
|
||||
|
||||
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 [
|
||||
{ tableState, states, incidents, incidentsCount, isLoading, error },
|
||||
{
|
||||
onIncidentStatesChange,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
onIncidentChanged,
|
||||
refetchIncidents,
|
||||
setIsLoading,
|
||||
},
|
||||
] = useIncidents(false, true, alertSource);
|
||||
|
||||
const [
|
||||
isNewIncidentModalOpened,
|
||||
setIsNewIncidentModalOpened,
|
||||
] = React.useState(false);
|
||||
const [
|
||||
isMaintenanceModalOpened,
|
||||
setIsMaintenanceModalOpened,
|
||||
] = React.useState(false);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return <MissingAuthorizationHeaderError />;
|
||||
}
|
||||
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
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}>
|
||||
<ILertCardOnCall alertSource={alertSource} />
|
||||
<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,84 @@
|
||||
/*
|
||||
* 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, 0),
|
||||
background: theme.palette.type === 'dark' ? '#444' : '#fff',
|
||||
},
|
||||
header: {
|
||||
display: 'inline-block',
|
||||
padding: theme.spacing(2, 2, 2, 2.5),
|
||||
},
|
||||
}));
|
||||
|
||||
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,117 @@
|
||||
/*
|
||||
* 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 List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListSubheader from '@material-ui/core/ListSubheader';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ReplayIcon from '@material-ui/icons/Replay';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { AlertSource } from '../../types';
|
||||
import { useAlertSourceOnCalls } from '../../hooks/useAlertSourceOnCalls';
|
||||
import { Progress } from '@backstage/core';
|
||||
import { ILertCardOnCallEmptyState } from './ILertCardOnCallEmptyState';
|
||||
import { ILertCardOnCallItem } from './ILertCardOnCallItem';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
repeatText: {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
repeatIcon: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const ILertCardOnCall = ({
|
||||
alertSource,
|
||||
}: {
|
||||
alertSource: AlertSource | null;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const [{ onCalls, isLoading }, {}] = useAlertSourceOnCalls(alertSource);
|
||||
|
||||
if (isLoading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (!alertSource || !onCalls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repeatInfo = () => {
|
||||
if (
|
||||
!alertSource ||
|
||||
!alertSource.escalationPolicy ||
|
||||
!alertSource.escalationPolicy.repeating ||
|
||||
!alertSource.escalationPolicy.frequency
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key="repeat">
|
||||
<ListItemIcon>
|
||||
<ReplayIcon className={classes.repeatIcon} fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
className={classes.repeatText}
|
||||
>
|
||||
{`Repeat ${alertSource.escalationPolicy.frequency} times`}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
if (!onCalls.length) {
|
||||
return (
|
||||
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
|
||||
<ILertCardOnCallEmptyState />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
if (onCalls.length === 1) {
|
||||
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
|
||||
<ILertCardOnCallItem
|
||||
onCall={onCalls[0]}
|
||||
fistItem={false}
|
||||
lastItem={false}
|
||||
/>
|
||||
</List>;
|
||||
}
|
||||
|
||||
return (
|
||||
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
|
||||
{onCalls.map((onCall, index) => (
|
||||
<ILertCardOnCallItem
|
||||
key={index}
|
||||
onCall={onCall}
|
||||
fistItem={index === 0}
|
||||
lastItem={index === onCalls.length - 1}
|
||||
/>
|
||||
))}
|
||||
{repeatInfo()}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 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 ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
text: {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
|
||||
export const ILertCardOnCallEmptyState = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="textSecondary"
|
||||
className={classes.text}
|
||||
>
|
||||
Nobody
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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 Divider from '@material-ui/core/Divider';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import EmailIcon from '@material-ui/icons/Email';
|
||||
import PhoneIcon from '@material-ui/icons/Phone';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { OnCall } from '../../types';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { DateTime as dt } from 'luxon';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
listItemPrimary: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
fistItemLine: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
height: '50%',
|
||||
left: 36,
|
||||
},
|
||||
lastItemLine: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '50%',
|
||||
left: 36,
|
||||
},
|
||||
itemLine: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: '100%',
|
||||
left: 36,
|
||||
},
|
||||
});
|
||||
|
||||
export const ILertCardOnCallItem = ({
|
||||
onCall,
|
||||
fistItem = false,
|
||||
lastItem = false,
|
||||
}: {
|
||||
onCall: OnCall;
|
||||
fistItem?: boolean;
|
||||
lastItem?: boolean;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!onCall || !onCall.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const phoneNumber = ilertApi.getUserPhoneNumber(onCall.user);
|
||||
const escalationRepeating = onCall.escalationPolicy.repeating;
|
||||
const escalationSeconds =
|
||||
onCall.escalationPolicy.escalationRules[onCall.escalationLevel - 1]
|
||||
.escalationTimeout;
|
||||
const escalationHoursOnly = Math.floor(escalationSeconds / 60);
|
||||
const escalationMinutesOnly = escalationSeconds % 60;
|
||||
|
||||
let escalationText = '';
|
||||
if (!lastItem || (lastItem && escalationRepeating)) {
|
||||
escalationText = 'escalate';
|
||||
if (escalationSeconds === 0) {
|
||||
escalationText += ' immediately';
|
||||
} else {
|
||||
escalationText += ' after';
|
||||
if (escalationHoursOnly > 0) {
|
||||
escalationText += ` ${escalationHoursOnly} ${
|
||||
escalationHoursOnly === 1 ? 'hour' : 'hours'
|
||||
}`;
|
||||
}
|
||||
if (escalationMinutesOnly > 0 || escalationSeconds === 0) {
|
||||
escalationText += ` ${escalationMinutesOnly} ${
|
||||
escalationMinutesOnly === 1 ? 'minute' : 'minutes'
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
{fistItem ? (
|
||||
<Divider orientation="vertical" className={classes.fistItemLine} />
|
||||
) : null}
|
||||
{lastItem ? (
|
||||
<Divider orientation="vertical" className={classes.lastItemLine} />
|
||||
) : null}
|
||||
{!fistItem && !lastItem ? (
|
||||
<Divider orientation="vertical" className={classes.itemLine} />
|
||||
) : null}
|
||||
<ListItemIcon>
|
||||
<Tooltip
|
||||
title={`Escalation level #${onCall.escalationLevel}`}
|
||||
placement="top"
|
||||
>
|
||||
<Avatar>{onCall.escalationLevel}</Avatar>
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
{onCall.schedule ? (
|
||||
<Tooltip
|
||||
title={
|
||||
'On call shift ' +
|
||||
`${dt.fromISO(onCall.start).toFormat('D MMM, HH:mm')} - ` +
|
||||
`${dt.fromISO(onCall.end).toFormat('D MMM, HH:mm')}`
|
||||
}
|
||||
placement="top-start"
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography className={classes.listItemPrimary}>
|
||||
{ilertApi.getUserInitials(onCall.user)}
|
||||
</Typography>
|
||||
}
|
||||
secondary={escalationText}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography className={classes.listItemPrimary}>
|
||||
{ilertApi.getUserInitials(onCall.user)}
|
||||
</Typography>
|
||||
}
|
||||
secondary={escalationText}
|
||||
/>
|
||||
)}
|
||||
<ListItemSecondaryAction>
|
||||
{phoneNumber ? (
|
||||
<Tooltip title="Call to user" placement="top">
|
||||
<IconButton href={`tel:${phoneNumber}`}>
|
||||
<PhoneIcon color="secondary" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip
|
||||
title={`Send e-mail to user ${onCall.user.email}`}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton href={`mailto:${onCall.user.email}`}>
|
||||
<EmailIcon color="primary" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -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,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 {
|
||||
alertApiRef,
|
||||
Progress,
|
||||
useApi,
|
||||
identityApiRef,
|
||||
Link,
|
||||
} from '@backstage/core';
|
||||
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { Incident, IncidentAction } from '../../types';
|
||||
import { IncidentAssignModal } from './IncidentAssignModal';
|
||||
import { useIncidentActions } from '../../hooks/useIncidentActions';
|
||||
|
||||
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 identityApi = useApi(identityApiRef);
|
||||
const userName = identityApi.getUserId();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const callback = onIncidentChanged || ((_: Incident): void => {});
|
||||
const setProcessing = setIsLoading || ((_: boolean): void => {});
|
||||
const [
|
||||
isAssignIncidentModalOpened,
|
||||
setIsAssignIncidentModalOpened,
|
||||
] = React.useState(false);
|
||||
|
||||
const [{ incidentActions, isLoading }] = useIncidentActions(
|
||||
incident,
|
||||
Boolean(anchorEl),
|
||||
);
|
||||
|
||||
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, userName);
|
||||
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, userName);
|
||||
alertApi.post({ message: 'Incident resolved.' });
|
||||
|
||||
callback(newIncident);
|
||||
setProcessing(false);
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
handleCloseMenu();
|
||||
setIsAssignIncidentModalOpened(true);
|
||||
};
|
||||
|
||||
const handleTriggerAction = (action: IncidentAction) => async () => {
|
||||
try {
|
||||
handleCloseMenu();
|
||||
setProcessing(true);
|
||||
await ilertApi.triggerIncidentAction(incident, action);
|
||||
alertApi.post({ message: 'Incident action triggered.' });
|
||||
setProcessing(false);
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const actions: React.ReactNode[] = incidentActions.map(a => {
|
||||
const successTrigger = a.history
|
||||
? a.history.find(h => h.success)
|
||||
: undefined;
|
||||
const triggeredBy =
|
||||
successTrigger && successTrigger.actor
|
||||
? `${successTrigger.actor.firstName} ${successTrigger.actor.lastName}`
|
||||
: '';
|
||||
return (
|
||||
<MenuItem
|
||||
key={a.webhookId}
|
||||
onClick={handleTriggerAction(a)}
|
||||
disabled={!!successTrigger}
|
||||
>
|
||||
<Typography variant="inherit" noWrap>
|
||||
{triggeredBy ? `${a.name} (by ${triggeredBy})` : a.name}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
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 * 5.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}
|
||||
|
||||
{isLoading ? (
|
||||
<MenuItem key="loading">
|
||||
<Progress style={{ width: '100%' }} />
|
||||
</MenuItem>
|
||||
) : (
|
||||
actions
|
||||
)}
|
||||
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link to={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,44 @@
|
||||
/*
|
||||
* 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, Link } from '@backstage/core';
|
||||
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}
|
||||
to={ilertApi.getIncidentDetailsURL(incident)}
|
||||
>
|
||||
#{incident.id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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 {
|
||||
await ilertApi.createIncident({
|
||||
integrationKey,
|
||||
summary,
|
||||
details,
|
||||
userName,
|
||||
source,
|
||||
});
|
||||
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,101 @@
|
||||
/*
|
||||
* 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,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
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 AuthenticationError) {
|
||||
return (
|
||||
<Content>
|
||||
<MissingAuthorizationHeaderError />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
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,262 @@
|
||||
/*
|
||||
* 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 { DateTime as dt, Interval } from 'luxon';
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
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'
|
||||
? humanizeDuration(
|
||||
Interval.fromDateTimes(
|
||||
dt.fromISO((rowData as Incident).reportTime),
|
||||
dt.now(),
|
||||
)
|
||||
.toDuration()
|
||||
.valueOf(),
|
||||
{ units: ['h', 'm', 's'], largest: 2, round: true },
|
||||
)
|
||||
: humanizeDuration(
|
||||
Interval.fromDateTimes(
|
||||
dt.fromISO((rowData as Incident).reportTime),
|
||||
dt.fromISO((rowData as Incident).resolvedOn),
|
||||
)
|
||||
.toDuration()
|
||||
.valueOf(),
|
||||
{ units: ['h', 'm', 's'], largest: 2, round: true },
|
||||
)}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
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,171 @@
|
||||
/*
|
||||
* 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, Link } from '@backstage/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { 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: -6,
|
||||
width: 12,
|
||||
height: 12,
|
||||
background: '#92949c !important',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
|
||||
indicatorCurrent: {
|
||||
position: 'absolute',
|
||||
top: 'calc(40% - 10px)',
|
||||
left: -6,
|
||||
width: 12,
|
||||
height: 12,
|
||||
background: '#ffb74d !important',
|
||||
color: '#ffb74d !important',
|
||||
borderRadius: '50%',
|
||||
'&::after': {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
animation: '$ripple 1.2s infinite ease-in-out',
|
||||
border: '1px solid currentColor',
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
'@keyframes ripple': {
|
||||
'0%': {
|
||||
transform: 'scale(.8)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'scale(2.4)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
|
||||
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
|
||||
to={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,64 @@
|
||||
/*
|
||||
* 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,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError) {
|
||||
return (
|
||||
<Content>
|
||||
<MissingAuthorizationHeaderError />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
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 { DateTime as dt } from 'luxon';
|
||||
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">
|
||||
{`${dt.fromISO(shift.start).toFormat('D MMM, HH:mm')} - ${dt
|
||||
.fromISO(shift.end)
|
||||
.toFormat('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 LuxonUtils from '@date-io/luxon';
|
||||
|
||||
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={LuxonUtils}>
|
||||
<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.toISO() : '');
|
||||
}}
|
||||
/>
|
||||
<DateTimePicker
|
||||
label="End"
|
||||
inputVariant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
ampm={false}
|
||||
value={end}
|
||||
className={classes.formControl}
|
||||
onChange={date => {
|
||||
setEnd(date ? date.toISO() : '');
|
||||
}}
|
||||
/>
|
||||
</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,135 @@
|
||||
/*
|
||||
* 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, Link } from '@backstage/core';
|
||||
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
|
||||
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 to="#" onClick={handleOpenReport}>
|
||||
View Report
|
||||
</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link to={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}>
|
||||
View in iLert
|
||||
</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 { useApi, Link } from '@backstage/core';
|
||||
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}
|
||||
to={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,67 @@
|
||||
/*
|
||||
* 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,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError) {
|
||||
return (
|
||||
<Content>
|
||||
<MissingAuthorizationHeaderError />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
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,176 @@
|
||||
/*
|
||||
* 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 { DateTime as dt, Interval } from 'luxon';
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink';
|
||||
import { UptimeMonitorCheckType } from './UptimeMonitorCheckType';
|
||||
import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu';
|
||||
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>
|
||||
{humanizeDuration(
|
||||
Interval.fromDateTimes(
|
||||
dt.fromISO((rowData as UptimeMonitor).lastStatusChange),
|
||||
dt.now(),
|
||||
)
|
||||
.toDuration()
|
||||
.valueOf(),
|
||||
{ units: ['h', 'm', 's'], largest: 2, round: true },
|
||||
)}
|
||||
</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_ANNOTATION = 'ilert.com/integration-key';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../constants';
|
||||
|
||||
export function useILertEntity() {
|
||||
const { entity } = useEntity();
|
||||
const integrationKey =
|
||||
entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION] || '';
|
||||
const name = entity.metadata.name;
|
||||
const identifier = `${entity.kind}:${
|
||||
entity.metadata.namespace || 'default'
|
||||
}/${entity.metadata.name}`;
|
||||
|
||||
return { integrationKey, name, identifier };
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
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 AuthenticationError)) {
|
||||
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,64 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { AlertSource, OnCall } from '../types';
|
||||
|
||||
export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [onCallsList, setOnCallsList] = React.useState<OnCall[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const fetchAlertSourceOnCallsCall = async () => {
|
||||
try {
|
||||
if (!alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const data = await ilertApi.fetchAlertSourceOnCalls(alertSource);
|
||||
setOnCallsList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (!(e instanceof AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const { error, retry } = useAsyncRetry(fetchAlertSourceOnCallsCall, [
|
||||
alertSource,
|
||||
]);
|
||||
|
||||
return [
|
||||
{
|
||||
onCalls: onCallsList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
retry,
|
||||
setIsLoading,
|
||||
refetchAlertSourceOnCalls: fetchAlertSourceOnCallsCall,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [incident, open]);
|
||||
|
||||
return [
|
||||
{
|
||||
incidentRespondersList,
|
||||
incidentResponder,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setIncidentRespondersList,
|
||||
setIncidentResponder,
|
||||
setIsLoading,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { Incident, IncidentAction } from '../types';
|
||||
|
||||
export const useIncidentActions = (
|
||||
incident: Incident | null,
|
||||
open: boolean,
|
||||
) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [incidentActionsList, setIncidentActionsList] = React.useState<
|
||||
IncidentAction[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
if (!incident || !open) {
|
||||
return;
|
||||
}
|
||||
const data = await ilertApi.fetchIncidentActions(incident);
|
||||
setIncidentActionsList(data);
|
||||
} catch (e) {
|
||||
if (!(e instanceof AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, [incident, open]);
|
||||
|
||||
return [
|
||||
{
|
||||
incidentActions: incidentActionsList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setIncidentActionsList,
|
||||
setIsLoading,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import {
|
||||
ACCEPTED,
|
||||
PENDING,
|
||||
Incident,
|
||||
IncidentStatus,
|
||||
AlertSource,
|
||||
} from '../types';
|
||||
|
||||
export const useIncidents = (
|
||||
paging: boolean,
|
||||
singleSource?: boolean,
|
||||
alertSource?: AlertSource | null,
|
||||
) => {
|
||||
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 {
|
||||
if (singleSource && !alertSource) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const opts: GetIncidentsOpts = {
|
||||
states,
|
||||
alertSources: alertSource ? [alertSource.id] : [],
|
||||
};
|
||||
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 AuthenticationError)) {
|
||||
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 AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [
|
||||
tableState,
|
||||
states,
|
||||
singleSource,
|
||||
alertSource,
|
||||
]);
|
||||
|
||||
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,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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
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,61 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ilertApiRef } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const { error, retry } = useAsyncRetry(fetchOnCallSchedulesCall, []);
|
||||
|
||||
return [
|
||||
{
|
||||
onCallSchedules: onCallSchedulesList,
|
||||
error,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
retry,
|
||||
setIsLoading,
|
||||
refetchOnCallSchedules: fetchOnCallSchedulesCall,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
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,89 @@
|
||||
/*
|
||||
* 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 } from '../api';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
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 AuthenticationError)) {
|
||||
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,337 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
export interface IncidentAction {
|
||||
name: string;
|
||||
type: string;
|
||||
webhookId: string;
|
||||
extensionId?: string;
|
||||
history?: IncidentActionHistory[];
|
||||
}
|
||||
|
||||
export interface IncidentActionHistory {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
incidentId: number;
|
||||
actor: User;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface OnCall {
|
||||
user: User;
|
||||
escalationPolicy: EscalationPolicy;
|
||||
schedule?: Schedule;
|
||||
start: string;
|
||||
end: string;
|
||||
escalationLevel: number;
|
||||
}
|
||||
@@ -1951,6 +1951,13 @@
|
||||
dependencies:
|
||||
"@date-io/core" "^1.3.13"
|
||||
|
||||
"@date-io/luxon@1.x":
|
||||
version "1.3.13"
|
||||
resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a"
|
||||
integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw==
|
||||
dependencies:
|
||||
"@date-io/core" "^1.3.13"
|
||||
|
||||
"@emotion/cache@^10.0.27":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
@@ -3729,6 +3736,18 @@
|
||||
react-transition-group "^4.0.0"
|
||||
rifm "^0.7.0"
|
||||
|
||||
"@material-ui/pickers@^3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485"
|
||||
integrity sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.0"
|
||||
"@date-io/core" "1.x"
|
||||
"@types/styled-jsx" "^2.2.8"
|
||||
clsx "^1.0.2"
|
||||
react-transition-group "^4.0.0"
|
||||
rifm "^0.7.0"
|
||||
|
||||
"@material-ui/styles@^4.10.0", "@material-ui/styles@^4.11.0", "@material-ui/styles@^4.11.3", "@material-ui/styles@^4.9.6":
|
||||
version "4.11.3"
|
||||
resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2"
|
||||
@@ -15119,6 +15138,11 @@ humanize-duration@^3.25.1:
|
||||
resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f"
|
||||
integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q==
|
||||
|
||||
humanize-duration@^3.26.0:
|
||||
version "3.26.0"
|
||||
resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.26.0.tgz#4d77f6b3d2fe0ca1ff14623ccc2b2f8b48ab1aaf"
|
||||
integrity sha512-SddekX3p5ApvPY6bbAYppGKe874jP6iFZXYtrQToDV4R0j2UpTYPqwTFM2QpXpuw9DhS/eXTUnKYTF9TbXAJ6A==
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
|
||||
|
||||
Reference in New Issue
Block a user