From 2d3ee7572eb6f7816dd725c1b6230ad9c4171c9d Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 14 Apr 2021 17:29:23 +0200 Subject: [PATCH 01/17] add iLert plugin Signed-off-by: yacut --- .github/CODEOWNERS | 1 + microsite/data/plugins/ilert.yaml | 13 + plugins/ilert/.eslintrc.js | 6 + plugins/ilert/README.md | 132 +++++ plugins/ilert/dev/index.tsx | 27 + plugins/ilert/package.json | 54 ++ plugins/ilert/src/api/client.ts | 500 ++++++++++++++++++ plugins/ilert/src/api/index.ts | 23 + plugins/ilert/src/api/types.ts | 110 ++++ plugins/ilert/src/assets/ilert.icon.svg | 11 + .../AlertSource/AlertSourceLink.tsx | 71 +++ .../MissingAuthorizationHeaderError.tsx | 35 ++ plugins/ilert/src/components/Errors/index.ts | 17 + .../EscalationPolicy/EscalationPolicyLink.tsx | 49 ++ .../src/components/ILertCard/ILertCard.tsx | 145 +++++ .../ILertCard/ILertCardActionsHeader.tsx | 203 +++++++ .../ILertCard/ILertCardEmptyState.tsx | 86 +++ .../ILertCard/ILertCardHeaderStatus.tsx | 46 ++ .../ILertCard/ILertCardMaintenanceModal.tsx | 138 +++++ .../ilert/src/components/ILertCard/index.ts | 16 + .../src/components/ILertPage/ILertPage.tsx | 68 +++ .../ilert/src/components/ILertPage/index.ts | 16 + .../Incident/IncidentActionsMenu.tsx | 148 ++++++ .../Incident/IncidentAssignModal.tsx | 183 +++++++ .../src/components/Incident/IncidentLink.tsx | 45 ++ .../components/Incident/IncidentNewModal.tsx | 230 ++++++++ .../components/Incident/IncidentStatus.tsx | 48 ++ .../ilert/src/components/Incident/index.ts | 17 + .../IncidentsPage/IncidentsPage.tsx | 93 ++++ .../IncidentsPage/IncidentsTable.tsx | 253 +++++++++ .../components/IncidentsPage/StatusChip.tsx | 57 ++ .../components/IncidentsPage/TableTitle.tsx | 97 ++++ .../src/components/IncidentsPage/index.ts | 17 + .../OnCallSchedulesGrid.tsx | 150 ++++++ .../OnCallSchedulesPage.tsx | 56 ++ .../OnCallSchedulesPage/OnCallShiftItem.tsx | 105 ++++ .../components/OnCallSchedulesPage/index.ts | 17 + .../components/Shift/ShiftOverrideModal.tsx | 196 +++++++ .../UptimeMonitorActionsMenu.tsx | 134 +++++ .../UptimeMonitor/UptimeMonitorLink.tsx | 49 ++ .../src/components/UptimeMonitor/index.ts | 16 + .../UptimeMonitorsPage/StatusChip.tsx | 73 +++ .../UptimeMonitorCheckType.tsx | 39 ++ .../UptimeMonitorsPage/UptimeMonitorsPage.tsx | 59 +++ .../UptimeMonitorsTable.tsx | 173 ++++++ .../components/UptimeMonitorsPage/index.ts | 17 + plugins/ilert/src/components/index.ts | 18 + plugins/ilert/src/constants.ts | 16 + plugins/ilert/src/hooks/index.ts | 28 + plugins/ilert/src/hooks/useAlertSource.ts | 103 ++++ plugins/ilert/src/hooks/useAssignIncident.ts | 73 +++ plugins/ilert/src/hooks/useIncidents.ts | 151 ++++++ plugins/ilert/src/hooks/useNewIncident.ts | 77 +++ plugins/ilert/src/hooks/useOnCallSchedules.ts | 60 +++ plugins/ilert/src/hooks/useShiftOverride.ts | 78 +++ plugins/ilert/src/hooks/useUptimeMonitors.ts | 88 +++ plugins/ilert/src/index.ts | 33 ++ plugins/ilert/src/plugin.test.ts | 22 + plugins/ilert/src/plugin.ts | 60 +++ plugins/ilert/src/route-refs.tsx | 24 + plugins/ilert/src/setupTests.ts | 17 + plugins/ilert/src/types.ts | 312 +++++++++++ 62 files changed, 5199 insertions(+) create mode 100644 microsite/data/plugins/ilert.yaml create mode 100644 plugins/ilert/.eslintrc.js create mode 100644 plugins/ilert/README.md create mode 100644 plugins/ilert/dev/index.tsx create mode 100644 plugins/ilert/package.json create mode 100644 plugins/ilert/src/api/client.ts create mode 100644 plugins/ilert/src/api/index.ts create mode 100644 plugins/ilert/src/api/types.ts create mode 100644 plugins/ilert/src/assets/ilert.icon.svg create mode 100644 plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx create mode 100644 plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx create mode 100644 plugins/ilert/src/components/Errors/index.ts create mode 100644 plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCard.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx create mode 100644 plugins/ilert/src/components/ILertCard/index.ts create mode 100644 plugins/ilert/src/components/ILertPage/ILertPage.tsx create mode 100644 plugins/ilert/src/components/ILertPage/index.ts create mode 100644 plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentAssignModal.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentLink.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentNewModal.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentStatus.tsx create mode 100644 plugins/ilert/src/components/Incident/index.ts create mode 100644 plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/StatusChip.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/TableTitle.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/index.ts create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/index.ts create mode 100644 plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/index.ts create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/index.ts create mode 100644 plugins/ilert/src/components/index.ts create mode 100644 plugins/ilert/src/constants.ts create mode 100644 plugins/ilert/src/hooks/index.ts create mode 100644 plugins/ilert/src/hooks/useAlertSource.ts create mode 100644 plugins/ilert/src/hooks/useAssignIncident.ts create mode 100644 plugins/ilert/src/hooks/useIncidents.ts create mode 100644 plugins/ilert/src/hooks/useNewIncident.ts create mode 100644 plugins/ilert/src/hooks/useOnCallSchedules.ts create mode 100644 plugins/ilert/src/hooks/useShiftOverride.ts create mode 100644 plugins/ilert/src/hooks/useUptimeMonitors.ts create mode 100644 plugins/ilert/src/index.ts create mode 100644 plugins/ilert/src/plugin.test.ts create mode 100644 plugins/ilert/src/plugin.ts create mode 100644 plugins/ilert/src/route-refs.tsx create mode 100644 plugins/ilert/src/setupTests.ts create mode 100644 plugins/ilert/src/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e21603c63..d939d529dc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ /plugins/search-* @backstage/techdocs-core /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core +/plugins/ilert @yacut /packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining /.changeset/techdocs-* @backstage/techdocs-core diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml new file mode 100644 index 0000000000..99cfca01bc --- /dev/null +++ b/microsite/data/plugins/ilert.yaml @@ -0,0 +1,13 @@ +--- +title: iLert +author: iLert +authorUrl: https://github.com/iLert +category: Monitoring +description: iLert is a platform for alerting, on-call management and uptime monitoring. +documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert +iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4 +npmPackageName: '@backstage/plugin-ilert' +tags: + - monitoring + - errors + - alerting diff --git a/plugins/ilert/.eslintrc.js b/plugins/ilert/.eslintrc.js new file mode 100644 index 0000000000..4d51616e98 --- /dev/null +++ b/plugins/ilert/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + quotes: ['error', 'single'], + }, +}; diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md new file mode 100644 index 0000000000..55e41ba5bd --- /dev/null +++ b/plugins/ilert/README.md @@ -0,0 +1,132 @@ +# @backstage/plugin-ilert + +## Introduction + +[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as [informing stakeholders](https://docs.ilert.com/getting-started/stakeholder-engagement) or creating tickets in external incident management tools. + +## Overview + +This plugin displays iLert information about an entity such as if there are any active incidents, wo is on-call now and uptime monitor status. + +There is also an easy way to trigger an incident directly to the person who is currently on-call. + +This plugin provides: + +- Information details about the persons on-call +- A way to override current person on-call +- A list of incidents +- A way to trigger a new incident +- A way to reassign/acknowledge/resolve an incident +- A list of uptime monitors + +## Setup instructions + +Install the plugin: + +```bash +yarn add @backstage/plugin-ilert +``` + +Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) +to enable the plugin: + +```js +export { plugin as ILert } from '@backstage/plugin-ilert'; +``` + +Add it to the `EntityPage.tsx`: + +```ts +import { + isPluginApplicableToEntity as isILertAvailable, + EntityILertCard, +} from '@backstage/plugin-ilert'; +// add to code +{ + isILertAvailable(entity) && ( + + + + ); +} +``` + +> To force iLert card for each entity just add the `` component, so an instruction card will appears if no integration key is set. + +## Add iLert explorer to the App sidebar + +Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: + +```tsx +// At the top imports +import { ILertPage } from '@backstage/plugin-ilert'; + +// Inside App component + + // ... + } /> + // ... +; +``` + +Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: + +```tsx +// At the top imports +import { ILertIcon } from '@backstage/plugin-ilert'; + +// Inside Sidebar component + + // ... + + // ... +; +``` + +## Client configuration + +If you want to override the default URL for api calls and detail pages, you can add it to `app-config.yaml`. + +In `app-config.yaml`: + +```yaml +ilert: + domain: https://my-org.ilert.com/ +``` + +## Providing the Authorization Header + +In order to make the API calls, you need to provide a new proxy config which will redirect to the [iLert API](https://api.ilert.com/api-docs/) endpoint it needs an [Authorization Header](https://api.ilert.com/api-docs/#section/Authentication). + +Add the proxy configuration in `app-config.yaml` + +```yaml +proxy: + ... + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: + $env: ILERT_AUTH_HEADER +``` + +Then start the backend passing the token as an environment variable: + +```bash +$ ILERT_AUTH_HEADER='Basic ' yarn start +``` + +## Integration Key + +The information displayed for each entity is based on the [alert source integration key](https://docs.ilert.com/integrations/backstage). + +### Adding the integration key to the entity annotation + +If you want to use this plugin for an entity, you need to label it with the below annotation: + +```yml +annotations: + ilert.com/integration-key: [INTEGRATION_KEY] +``` diff --git a/plugins/ilert/dev/index.tsx b/plugins/ilert/dev/index.tsx new file mode 100644 index 0000000000..c05e1da3d9 --- /dev/null +++ b/plugins/ilert/dev/index.tsx @@ -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: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json new file mode 100644 index 0000000000..b31af34753 --- /dev/null +++ b/plugins/ilert/package.json @@ -0,0 +1,54 @@ +{ + "name": "@backstage/plugin-ilert", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.6", + "@backstage/core": "^0.7.4", + "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/theme": "^0.2.5", + "@date-io/date-fns": "1.x", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@material-ui/pickers": "^3.3.10", + "date-fns": "^2.20.2", + "moment": "^2.29.1", + "moment-timezone": "^0.5.33", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.6.7", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.10", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts new file mode 100644 index 0000000000..259502eff0 --- /dev/null +++ b/plugins/ilert/src/api/client.ts @@ -0,0 +1,500 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core'; +import { + AlertSource, + EscalationPolicy, + Incident, + IncidentResponder, + Schedule, + UptimeMonitor, + User, +} from '../types'; +import { + ILertApi, + GetIncidentsOpts, + GetIncidentsCountOpts, + Options, + EventRequest, +} from './types'; +import moment from 'moment'; +import momentTimezone from 'moment-timezone'; + +export const ilertApiRef = createApiRef({ + id: 'plugin.ilert.service', + description: 'Used to make requests towards iLert API', +}); + +const DEFAULT_PROXY_PATH = '/ilert'; +export class UnauthorizedError extends Error {} + +export class ILertClient implements ILertApi { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPath: string; + private readonly domain: string; + + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { + const domainUrl: string = + configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com'; + + return new ILertClient({ + discoveryApi: discoveryApi, + domain: domainUrl, + proxyPath: configApi.getOptionalString('ilert.proxyPath'), + }); + } + + constructor(opts: Options) { + this.discoveryApi = opts.discoveryApi; + this.domain = opts.domain; + this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; + } + + private async fetch(input: string, init?: RequestInit): Promise { + const apiUrl = await this.apiUrl(); + + const response = await fetch(`${apiUrl}${input}`, init); + if (response.status === 401) { + throw new UnauthorizedError(''); + } + if (!response.ok) { + throw new Error( + `Request failed with ${response.status} ${response.statusText}`, + ); + } + + return await response.json(); + } + + async fetchIncidents(opts?: GetIncidentsOpts): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + const query = new URLSearchParams(); + if (opts && opts.maxResults) { + query.append('max-results', `${opts.maxResults}`); + } + if (opts && opts.startIndex) { + query.append('start-index', `${opts.startIndex}`); + } + if (opts && opts.alertSources) { + opts.alertSources.forEach((a: string | number) => { + if (a) { + query.append('alert-source', `${a}`); + } + }); + } + + if (opts && opts.states && Array.isArray(opts.states)) { + opts.states.forEach(state => { + query.append('state', state); + }); + } + const response = await this.fetch( + `/api/v1/incidents?${query.toString()}`, + init, + ); + + return response; + } + + async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + const query = new URLSearchParams(); + if (opts && opts.states && Array.isArray(opts.states)) { + opts.states.forEach(state => { + query.append('state', state); + }); + } + const response = await this.fetch( + `/api/v1/incidents/count?${query.toString()}`, + init, + ); + + return response && response.count ? response.count : 0; + } + + async fetchIncidentResponders( + incident: Incident, + ): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/responders`, + init, + ); + + return response; + } + + async acceptIncident(incident: Incident): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/accept`, + init, + ); + + return response; + } + + async resolveIncident(incident: Incident): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/resolve`, + init, + ); + + return response; + } + + async assignIncident( + incident: Incident, + responder: IncidentResponder, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const query = new URLSearchParams(); + switch (responder.group) { + case 'ESCALATION_POLICY': + query.append('policy-id', `${responder.id}`); + break; + case 'ON_CALL_SCHEDULE': + query.append('schedule-id', `${responder.id}`); + break; + default: + query.append('user-id', `${responder.id}`); + break; + } + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/assign?${query.toString()}`, + init, + ); + + return response; + } + + async createIncident(eventRequest: EventRequest): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + apiKey: eventRequest.integrationKey, + summary: eventRequest.summary, + details: eventRequest.details, + eventType: 'ALERT', + links: [ + { + href: eventRequest.source, + text: 'Backstage Url', + }, + ], + customDetails: { + userName: eventRequest.userName, + }, + }), + }; + + const response = await this.fetch('/api/v1/events', init); + return response.responseCode === 'NEW_INCIDENT_CREATED'; + } + + async fetchUptimeMonitors(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/uptime-monitors', init); + + return response; + } + + async fetchUptimeMonitor(id: number): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response: UptimeMonitor = await this.fetch( + `/api/v1/uptime-monitors/${id}`, + init, + ); + + return response; + } + + async pauseUptimeMonitor( + uptimeMonitor: UptimeMonitor, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...uptimeMonitor, paused: true }), + }; + + const response = await this.fetch( + `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + init, + ); + + return response; + } + + async resumeUptimeMonitor( + uptimeMonitor: UptimeMonitor, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...uptimeMonitor, paused: false }), + }; + + const response = await this.fetch( + `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + init, + ); + + return response; + } + + async fetchAlertSources(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/alert-sources', init); + + return response; + } + + async fetchAlertSource( + idOrIntegrationKey: number | string, + ): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${idOrIntegrationKey}`, + init, + ); + + return response; + } + + async enableAlertSource(alertSource: AlertSource): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...alertSource, active: true }), + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${alertSource.id}`, + init, + ); + + return response; + } + + async disableAlertSource(alertSource: AlertSource): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...alertSource, active: false }), + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${alertSource.id}`, + init, + ); + + return response; + } + + async addImmediateMaintenance( + alertSourceId: number, + minutes: number, + ): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + start: moment().utc().toISOString(), + end: moment().add(minutes, 'minutes').utc().toISOString(), + description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`, + createdBy: 'Backstage', + timezone: momentTimezone.tz.guess(), + alertSources: [{ id: alertSourceId }], + }), + }; + + const response = await this.fetch('/api/v1/maintenance-windows', init); + + return response; + } + + async fetchOnCallSchedules(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/schedules', init); + + return response; + } + + async fetchUsers(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/users', init); + + return response; + } + + async overrideShift( + scheduleId: number, + userId: number, + start: string, + end: string, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ user: { id: userId }, start, end }), + }; + + const response = await this.fetch( + `/api/v1/schedules/${scheduleId}/overrides`, + init, + ); + + return response; + } + + getIncidentDetailsURL(incident: Incident): string { + return `${this.domain}/incident/view.jsf?id=${incident.id}`; + } + + getAlertSourceDetailsURL(alertSource: AlertSource | null): string { + if (!alertSource) { + return ''; + } + return `${this.domain}/source/view.jsf?id=${alertSource.id}`; + } + + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { + return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`; + } + + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { + return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`; + } + + getScheduleDetailsURL(schedule: Schedule): string { + return `${this.domain}/schedule/view.jsf?id=${schedule.id}`; + } + + getUserInitials(assignedTo: User | null) { + if (!assignedTo) { + return ''; + } + if (!assignedTo.firstName && !assignedTo.lastName) { + return assignedTo.username; + } + return `${assignedTo.firstName} ${assignedTo.lastName} (${assignedTo.username})`; + } + + private async apiUrl() { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return proxyUrl + this.proxyPath; + } +} diff --git a/plugins/ilert/src/api/index.ts b/plugins/ilert/src/api/index.ts new file mode 100644 index 0000000000..128856ebdf --- /dev/null +++ b/plugins/ilert/src/api/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ILertClient, ilertApiRef, UnauthorizedError } from './client'; +export type { + ILertApi, + GetIncidentsCountOpts, + GetIncidentsOpts, + TableState, +} from './types'; diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts new file mode 100644 index 0000000000..4c33455d86 --- /dev/null +++ b/plugins/ilert/src/api/types.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DiscoveryApi } from '@backstage/core'; +import { + AlertSource, + Incident, + User, + IncidentStatus, + UptimeMonitor, + EscalationPolicy, + Schedule, + IncidentResponder, +} from '../types'; + +export type TableState = { + page: number; + pageSize: number; +}; + +export type GetIncidentsOpts = { + maxResults?: number; + startIndex?: number; + states?: IncidentStatus[]; + alertSources?: number[] | string[]; +}; + +export type GetIncidentsCountOpts = { + states?: IncidentStatus[]; +}; + +export type EventRequest = { + integrationKey: string; + summary: string; + details: string; + userName: string; + source: string; +}; + +export interface ILertApi { + fetchIncidents(opts?: GetIncidentsOpts): Promise; + fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + fetchIncidentResponders(incident: Incident): Promise; + acceptIncident(incident: Incident): Promise; + resolveIncident(incident: Incident): Promise; + assignIncident( + incident: Incident, + responder: IncidentResponder, + ): Promise; + createIncident(eventRequest: EventRequest): Promise; + + fetchUptimeMonitors(): Promise; + pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + fetchUptimeMonitor(id: number): Promise; + + fetchAlertSources(): Promise; + fetchAlertSource(idOrIntegrationKey: number | string): Promise; + enableAlertSource(alertSource: AlertSource): Promise; + disableAlertSource(alertSource: AlertSource): Promise; + + addImmediateMaintenance( + alertSourceId: number, + minutes: number, + ): Promise; + + fetchOnCallSchedules(): Promise; + fetchUsers(): Promise; + + overrideShift( + scheduleId: number, + userId: number, + start: string, + end: string, + ): Promise; + + getIncidentDetailsURL(incident: Incident): string; + getAlertSourceDetailsURL(alertSource: AlertSource | null): string; + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; + getScheduleDetailsURL(schedule: Schedule): string; + getUserInitials(assignedTo: User | null): string; +} + +export type Options = { + discoveryApi: DiscoveryApi; + + /** + * Domain used by users to access iLert web UI. + * Example: https://my-org.ilert.com/ + */ + domain: string; + + /** + * Path to use for requests via the proxy, defaults to /ilert/api + */ + proxyPath?: string; +}; diff --git a/plugins/ilert/src/assets/ilert.icon.svg b/plugins/ilert/src/assets/ilert.icon.svg new file mode 100644 index 0000000000..e93f1e80ca --- /dev/null +++ b/plugins/ilert/src/assets/ilert.icon.svg @@ -0,0 +1,11 @@ + + + + diff --git a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx new file mode 100644 index 0000000000..d94a9b0039 --- /dev/null +++ b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import Grid from '@material-ui/core/Grid'; +import { AlertSource } from '../../types'; +import { ilertApiRef } from '../../api'; +import { makeStyles } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + maxWidth: '100%', + }, + image: { + height: 22, + paddingRight: 4, + }, + link: { + lineHeight: '22px', + }, +}); + +export const AlertSourceLink = ({ + alertSource, +}: { + alertSource: AlertSource | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + if (!alertSource) { + return null; + } + + return ( + + + {alertSource.name} + + + + {alertSource.name} + + + + ); +}; diff --git a/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx b/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx new file mode 100644 index 0000000000..6b3d463d2a --- /dev/null +++ b/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx @@ -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 = () => ( + + Read More + + } + /> +); diff --git a/plugins/ilert/src/components/Errors/index.ts b/plugins/ilert/src/components/Errors/index.ts new file mode 100644 index 0000000000..f20de48b90 --- /dev/null +++ b/plugins/ilert/src/components/Errors/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx new file mode 100644 index 0000000000..7f1e3524ff --- /dev/null +++ b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { EscalationPolicy } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const EscalationPolicyLink = ({ + escalationPolicy, +}: { + escalationPolicy: EscalationPolicy | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!escalationPolicy) { + return null; + } + + return ( + + {escalationPolicy.name} + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx new file mode 100644 index 0000000000..b376b319b5 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Divider from '@material-ui/core/Divider'; +import { makeStyles } from '@material-ui/core/styles'; +import { ILERT_INTEGRATION_KEY } from '../../constants'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useIncidents } from '../../hooks/useIncidents'; +import { IncidentsTable } from '../IncidentsPage'; +import { IncidentNewModal } from '../Incident/IncidentNewModal'; +import { ILertCardActionsHeader } from './ILertCardActionsHeader'; +import { useAlertSource } from '../../hooks/useAlertSource'; +import { useILertEntity } from '../../hooks'; +import { ILertCardHeaderStatus } from './ILertCardHeaderStatus'; +import { ILertCardMaintenanceModal } from './ILertCardMaintenanceModal'; +import { ILertCardEmptyState } from './ILertCardEmptyState'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]); + +const useStyles = makeStyles({ + content: { + paddingLeft: '0 !important', + paddingRight: '0 !important', + paddingBottom: '0 !important', + paddingTop: '0 !important', + '& div div': { + boxShadow: 'none !important', + }, + }, +}); + +export const ILertCard = () => { + const classes = useStyles(); + const { integrationKey, name } = useILertEntity(); + const [ + { alertSource, uptimeMonitor }, + { setAlertSource, refetchAlertSource }, + ] = useAlertSource(integrationKey); + const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey]; + const [ + { tableState, states, incidents, incidentsCount, isLoading, error }, + { + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + onIncidentChanged, + refetchIncidents, + setIsLoading, + }, + ] = useIncidents(false, alertSourcesFilter); + + const [ + isNewIncidentModalOpened, + setIsNewIncidentModalOpened, + ] = React.useState(false); + const [ + isMaintenanceModalOpened, + setIsMaintenanceModalOpened, + ] = React.useState(false); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + if (!integrationKey) { + return ; + } + + return ( + <> + + + } + action={} + /> + + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx b/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx new file mode 100644 index 0000000000..c19c31839a --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx @@ -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: , + }; + + const createIncidentLink: IconLinkVerticalProps = { + label: 'Create Incident', + onClick: handleCreateNewIncident, + icon: , + color: 'secondary', + disabled: + !alertSource || + alertSource.status === 'DISABLED' || + alertSource.status === 'IN_MAINTENANCE', + }; + + const enableAlertSourceLink: IconLinkVerticalProps = { + label: 'Enable', + onClick: handleEnableAlertSource, + icon: , + disabled: !alertSource || isLoading, + }; + + const disableAlertSourceLink: IconLinkVerticalProps = { + label: 'Disable', + onClick: handleDisableAlertSourceWarningOpen, + icon: , + disabled: !alertSource || isLoading, + }; + + const maintenanceAlertSourceLink: IconLinkVerticalProps = { + label: 'Immediate maintenance', + onClick: handleMaintenanceAlertSource, + icon: , + disabled: !alertSource || isLoading, + }; + + const uptimeMonitorReportLink: IconLinkVerticalProps = { + label: 'Uptime Report', + href: uptimeMonitor ? uptimeMonitor.shareUrl : '', + icon: , + 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 ( + <> + + + + Disable alert source + + + + + Do you really want to disable this alert source? A disabled alert + source cannot create new incidents. + + + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx new file mode 100644 index 0000000000..42dc140696 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CodeSnippet } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const ENTITY_YAML = `apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example + description: example.com + annotations: + ilert.com/integration-key: [INTEGRATION_KEY] +spec: + type: website + lifecycle: production + owner: guest`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, + header: { + display: 'inline-block', + padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing( + 2, + )}px ${theme.spacing(2.5)}px`, + }, +})); + +export const ILertCardEmptyState = () => { + const classes = useStyles(); + + return ( + + + + + + No integration key defined for this entity. You can add integration + key to your entity YAML as shown in the highlighted example below: + +
+ +
+ +
+
+ ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx b/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx new file mode 100644 index 0000000000..2c1b44551e --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx @@ -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 ; + case 'DISABLED': + return ; + default: + return null; + } +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx b/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx new file mode 100644 index 0000000000..54d465a83c --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx @@ -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 ( + + + New maintenance window + + + + Keep your alert sources quiet, when your systems are under + maintenance. + + + {minuteOptions.map(option => ( + + {option.label} + + ))} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/index.ts b/plugins/ilert/src/components/ILertCard/index.ts new file mode 100644 index 0000000000..4ae259bfc5 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/ILertPage/ILertPage.tsx b/plugins/ilert/src/components/ILertPage/ILertPage.tsx new file mode 100644 index 0000000000..40934bfdfb --- /dev/null +++ b/plugins/ilert/src/components/ILertPage/ILertPage.tsx @@ -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(0); + const tabs = [ + { label: 'Who is on call?' }, + { label: 'Incidents' }, + { label: 'Uptime Monitors' }, + ]; + const renderTab = () => { + switch (selectedTab) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + default: + return null; + } + }; + + return ( + +
+ + +
+ setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + {renderTab()} +
+ ); +}; + +export default ILertPage; diff --git a/plugins/ilert/src/components/ILertPage/index.ts b/plugins/ilert/src/components/ILertPage/index.ts new file mode 100644 index 0000000000..5de46b63d4 --- /dev/null +++ b/plugins/ilert/src/components/ILertPage/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx new file mode 100644 index 0000000000..576f037c8c --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; +import Link from '@material-ui/core/Link'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { ilertApiRef } from '../../api'; +import { Incident } from '../../types'; +import { IncidentAssignModal } from './IncidentAssignModal'; + +export const IncidentActionsMenu = ({ + incident, + onIncidentChanged, + setIsLoading, +}: { + incident: Incident; + onIncidentChanged?: (incident: Incident) => void; + setIsLoading?: (isLoading: boolean) => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [anchorEl, setAnchorEl] = React.useState(null); + const callback = onIncidentChanged || ((_: Incident): void => {}); + const setProcessing = setIsLoading || ((_: boolean): void => {}); + const [ + isAssignIncidentModalOpened, + setIsAssignIncidentModalOpened, + ] = React.useState(false); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handleAccept = async (): Promise => { + try { + handleCloseMenu(); + setProcessing(true); + const newIncident = await ilertApi.acceptIncident(incident); + alertApi.post({ message: 'Incident accepted.' }); + + callback(newIncident); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleResolve = async (): Promise => { + try { + handleCloseMenu(); + setProcessing(true); + const newIncident = await ilertApi.resolveIncident(incident); + alertApi.post({ message: 'Incident resolved.' }); + + callback(newIncident); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleAssign = () => { + handleCloseMenu(); + setIsAssignIncidentModalOpened(true); + }; + + return ( + <> + + + + + {incident.status === 'PENDING' ? ( + + + Accept + + + ) : null} + + {incident.status !== 'RESOLVED' ? ( + + + Resolve + + + ) : null} + + {incident.status !== 'RESOLVED' ? ( + + + Assign + + + ) : null} + + + + + View in iLert + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx b/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx new file mode 100644 index 0000000000..edac5e4e0e --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx @@ -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 ( + + + Select responder to assign + + + + + This action will assign the incident to the selected responder. + + + { + 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 => ( +
+ {a.name} +
+ )} + renderInput={params => ( + + )} + /> +
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentLink.tsx b/plugins/ilert/src/components/Incident/IncidentLink.tsx new file mode 100644 index 0000000000..87818f363a --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentLink.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { Incident } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const IncidentLink = ({ incident }: { incident: Incident | null }) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!incident) { + return null; + } + + return ( + + #{incident.id} + + ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx new file mode 100644 index 0000000000..3fae62bf0c --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -0,0 +1,230 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, identityApiRef, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { useNewIncident } from '../../hooks/useNewIncident'; +import { Typography } from '@material-ui/core'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { ilertApiRef } from '../../api'; +import { AlertSource } from '../../types'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + minWidth: 120, + width: '100%', + }, + option: { + fontSize: 15, + '& > span': { + marginRight: 10, + fontSize: 18, + }, + }, + optionWrapper: { + display: 'flex', + width: '100%', + }, + sourceImage: { + height: 22, + paddingRight: 4, + }, +})); + +export const IncidentNewModal = ({ + isModalOpened, + setIsModalOpened, + refetchIncidents, + initialAlertSource, + entityName, +}: { + isModalOpened: boolean; + setIsModalOpened: (open: boolean) => void; + refetchIncidents: () => void; + initialAlertSource?: AlertSource | null; + entityName?: string; +}) => { + const [ + { alertSources, alertSource, summary, details, isLoading }, + { setAlertSource, setSummary, setDetails, setIsLoading }, + ] = useNewIncident(isModalOpened, initialAlertSource); + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const identityApi = useApi(identityApiRef); + const userName = identityApi.getUserId(); + const source = window.location.toString(); + const classes = useStyles(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + const handleClose = () => { + setIsModalOpened(false); + }; + + let integrationKey = ''; + if (initialAlertSource && initialAlertSource.integrationKey) { + integrationKey = initialAlertSource.integrationKey; + } else if (alertSource && alertSource.integrationKey) { + integrationKey = alertSource.integrationKey; + } + const handleCreate = () => { + if (!integrationKey) { + return; + } + setIsLoading(true); + setTimeout(async () => { + try { + const success = await ilertApi.createIncident({ + integrationKey, + summary, + details, + userName, + source, + }); + if (success) { + alertApi.post({ message: 'Incident created.' }); + refetchIncidents(); + } + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + setIsModalOpened(false); + }, 250); + }; + + const canCreate = !!integrationKey && !!summary; + + return ( + + + {entityName ? ( +
+ This action will trigger an incident for{' '} + "{entityName}". +
+ ) : ( + 'New incident' + )} +
+ + + + 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. + + + {!initialAlertSource ? ( + { + setAlertSource(newValue); + }} + autoHighlight + getOptionLabel={a => a.name} + renderOption={a => ( +
+ {a.name} + {a.name} +
+ )} + renderInput={params => ( + + )} + /> + ) : null} + { + setSummary(event.target.value); + }} + /> + { + setDetails(event.target.value); + }} + /> +
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentStatus.tsx b/plugins/ilert/src/components/Incident/IncidentStatus.tsx new file mode 100644 index 0000000000..931a472ec6 --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentStatus.tsx @@ -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; + +export const IncidentStatus = ({ incident }: { incident: Incident }) => { + const classes = useStyles(); + + return ( + +
+ {incident.status === 'PENDING' ? : } +
+
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/index.ts b/plugins/ilert/src/components/Incident/index.ts new file mode 100644 index 0000000000..df8bab5d95 --- /dev/null +++ b/plugins/ilert/src/components/Incident/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx new file mode 100644 index 0000000000..e552ce7d26 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import Button from '@material-ui/core/Button'; +import AddIcon from '@material-ui/icons/Add'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { IncidentsTable } from './IncidentsTable'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useIncidents } from '../../hooks/useIncidents'; +import { IncidentNewModal } from '../Incident/IncidentNewModal'; + +export const IncidentsPage = () => { + const [ + { tableState, states, incidents, incidentsCount, isLoading, error }, + { + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + onIncidentChanged, + refetchIncidents, + setIsLoading, + }, + ] = useIncidents(true); + + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const handleCreateNewIncidentClick = () => { + setIsModalOpened(true); + }; + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx new file mode 100644 index 0000000000..b7ee298062 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx @@ -0,0 +1,253 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Table, TableColumn, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ilertApiRef, TableState } from '../../api'; +import { Incident, IncidentStatus } from '../../types'; +import { StatusChip } from './StatusChip'; +import { AlertSourceLink } from '../AlertSource/AlertSourceLink'; +import { TableTitle } from './TableTitle'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment'; +import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu'; +import { IncidentLink } from '../Incident/IncidentLink'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export const IncidentsTable = ({ + incidents, + incidentsCount, + tableState, + states, + isLoading, + onIncidentChanged, + setIsLoading, + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + compact, +}: { + incidents: Incident[]; + incidentsCount: number; + tableState: TableState; + states: IncidentStatus[]; + isLoading: boolean; + onIncidentChanged: (incident: Incident) => void; + setIsLoading: (isLoading: boolean) => void; + onIncidentStatesChange: (states: IncidentStatus[]) => void; + onChangePage: (page: number) => void; + onChangeRowsPerPage: (pageSize: number) => void; + compact?: boolean; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + const xsColumnStyle = { + width: '5%', + maxWidth: '5%', + }; + const smColumnStyle = { + width: '10%', + maxWidth: '10%', + }; + const mdColumnStyle = { + width: '15%', + maxWidth: '15%', + }; + const lgColumnStyle = { + width: '20%', + maxWidth: '20%', + }; + const xlColumnStyle = { + width: '30%', + maxWidth: '30%', + }; + + const idColumn: TableColumn = { + title: 'ID', + field: 'id', + highlight: true, + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => , + }; + const summaryColumn: TableColumn = { + title: 'Summary', + field: 'summary', + cellStyle: !compact ? xlColumnStyle : undefined, + headerStyle: !compact ? xlColumnStyle : undefined, + render: rowData => {(rowData as Incident).summary}, + }; + const sourceColumn: TableColumn = { + title: 'Source', + field: 'source', + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + ), + }; + const durationColumn: TableColumn = { + title: 'Duration', + field: 'reportTime', + type: 'datetime', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + {(rowData as Incident).status !== 'RESOLVED' + ? moment + .duration(moment((rowData as Incident).reportTime).diff(moment())) + .humanize() + : moment + .duration( + moment((rowData as Incident).reportTime).diff( + moment((rowData as Incident).resolvedOn), + ), + ) + .humanize()} + + ), + }; + const assignedToColumn: TableColumn = { + title: 'Assigned to', + field: 'assignedTo', + cellStyle: !compact ? mdColumnStyle : lgColumnStyle, + headerStyle: !compact ? mdColumnStyle : lgColumnStyle, + render: rowData => ( + + {ilertApi.getUserInitials((rowData as Incident).assignedTo)} + + ), + }; + const priorityColumn: TableColumn = { + title: 'Priority', + field: 'priority', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + {(rowData as Incident).priority === 'HIGH' ? 'High' : 'Low'} + + ), + }; + const statusColumn: TableColumn = { + title: 'Status', + field: 'status', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => , + }; + const actionsColumn: TableColumn = { + title: '', + field: '', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => ( + + ), + }; + + 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 ( + + No incidents right now + + } + title={ + !compact ? ( + + ) : ( + + INCIDENTS + + ) + } + page={tableState.page} + totalCount={incidentsCount} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangeRowsPerPage} + // localization={{ header: { actions: undefined } }} + columns={columns} + data={incidents} + isLoading={isLoading} + /> + ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx b/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx new file mode 100644 index 0000000000..9713b16a55 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx @@ -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 ; + case ACCEPTED: + return ; + case PENDING: + return ; + default: + return ; + } +}; diff --git a/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx b/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx new file mode 100644 index 0000000000..f35981dec2 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx @@ -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 ( +
+ + Status: + + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/index.ts b/plugins/ilert/src/components/IncidentsPage/index.ts new file mode 100644 index 0000000000..7159247c68 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx new file mode 100644 index 0000000000..34601009ff --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -0,0 +1,150 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi, ItemCardGrid, Progress } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Typography from '@material-ui/core/Typography'; +import Link from '@material-ui/core/Link'; +import { Schedule } from '../../types'; +import { ilertApiRef } from '../../api'; +import { OnCallShiftItem } from './OnCallShiftItem'; + +const useStyles = makeStyles(() => ({ + card: { + margin: 16, + width: 'calc(100% - 32px)', + }, + + cardHeader: { + maxWidth: '100%', + }, + + cardContent: { + marginLeft: 80, + borderLeft: '1px #808289 solid', + position: 'relative', + }, + + indicatorNext: { + position: 'absolute', + top: 'calc(40% - 10px)', + left: -8, + width: 16, + height: 16, + background: '#92949c !important', + borderRadius: '50%', + }, + + indicatorCurrent: { + position: 'absolute', + top: 'calc(40% - 10px)', + left: -8, + width: 16, + height: 16, + background: '#ffb74d !important', + borderRadius: '50%', + }, + + beforeText: { + position: 'absolute', + top: 'calc(31% - 10px)', + left: -78, + width: 65, + height: 20, + textAlign: 'center', + color: '#808289', + }, + + marginBottom: { + marginBottom: 16, + }, + + link: { + fontSize: '1.5rem', + fontWeight: 700, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + display: 'block', + }, +})); + +export const OnCallSchedulesGrid = ({ + onCallSchedules, + isLoading, + refetchOnCallSchedules, +}: { + onCallSchedules: Schedule[]; + isLoading: boolean; + refetchOnCallSchedules: () => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (isLoading) { + return ; + } + return ( + + {!onCallSchedules?.length + ? null + : onCallSchedules.map((schedule, index) => ( + + + {schedule.name} + + } + /> + + +
+ + + On call now + + + + +
+ + + Next on call + + + + ))} + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx new file mode 100644 index 0000000000..09c2f2fc63 --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { OnCallSchedulesGrid } from './OnCallSchedulesGrid'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useOnCallSchedules } from '../../hooks/useOnCallSchedules'; + +export const OnCallSchedulesPage = () => { + const [ + { onCallSchedules, isLoading, error }, + { refetchOnCallSchedules }, + ] = useOnCallSchedules(); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx new file mode 100644 index 0000000000..c636609781 --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import RepeatIcon from '@material-ui/icons/Repeat'; +import { Shift } from '../../types'; +import moment from 'moment'; +import { makeStyles } from '@material-ui/core/styles'; +import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal'; + +const useStyles = makeStyles({ + button: { + marginTop: 4, + padding: 0, + lineHeight: 1.8, + '& span': { + lineHeight: 1.8, + fontSize: '0.65rem', + }, + '& svg': { + fontSize: '0.85rem !important', + }, + }, +}); + +export const OnCallShiftItem = ({ + scheduleId, + shift, + refetchOnCallSchedules, +}: { + scheduleId: number; + shift: Shift; + refetchOnCallSchedules: () => void; +}) => { + const classes = useStyles(); + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const handleOverride = () => { + setIsModalOpened(true); + }; + + if (!shift || !shift.start) { + return ( + + + + Nobody + + + + ); + } + + return ( + + {shift && shift.user ? ( + + + {`${shift.user.firstName} ${shift.user.lastName} (${shift.user.username})`} + + + ) : null} + + + {`${moment(shift.start).format('D MMM, HH:mm')} - ${moment( + shift.end, + ).format('D MMM, HH:mm')}`} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/index.ts b/plugins/ilert/src/components/OnCallSchedulesPage/index.ts new file mode 100644 index 0000000000..a52accc2bb --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx new file mode 100644 index 0000000000..1fdd05cb4c --- /dev/null +++ b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { Typography } from '@material-ui/core'; +import { ilertApiRef } from '../../api'; +import { useShiftOverride } from '../../hooks/useShiftOverride'; +import { Shift } from '../../types'; +import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + minWidth: 120, + width: '100%', + }, + option: { + fontSize: 15, + '& > span': { + marginRight: 10, + fontSize: 18, + }, + }, + optionWrapper: { + display: 'flex', + width: '100%', + }, + sourceImage: { + height: 22, + paddingRight: 4, + }, + grow: { + flexGrow: 1, + }, +})); + +export const ShiftOverrideModal = ({ + scheduleId, + shift, + refetchOnCallSchedules, + isModalOpened, + setIsModalOpened, +}: { + scheduleId: number; + shift: Shift; + refetchOnCallSchedules: () => void; + isModalOpened: boolean; + setIsModalOpened: (isModalOpened: boolean) => void; +}) => { + const [ + { isLoading, users, user, start, end }, + { setUser, setStart, setEnd, setIsLoading }, + ] = useShiftOverride(shift, isModalOpened); + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const classes = useStyles(); + + const handleClose = () => { + setIsModalOpened(false); + }; + + const handleOverride = () => { + if (!shift || !shift.user) { + return; + } + setIsLoading(true); + setTimeout(async () => { + try { + const success = await ilertApi.overrideShift( + scheduleId, + user.id, + start, + end, + ); + if (success) { + alertApi.post({ message: 'Shift overridden.' }); + refetchOnCallSchedules(); + } + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + setIsModalOpened(false); + }, 250); + }; + + if (!shift) { + return null; + } + + return ( + + Shift override + + + { + setUser(newValue); + }} + autoHighlight + getOptionLabel={a => ilertApi.getUserInitials(a)} + renderOption={a => ( +
+ {ilertApi.getUserInitials(a)} +
+ )} + renderInput={params => ( + + )} + /> + { + setStart(date ? date.toISOString() : ''); + }} + /> + { + setEnd(date ? date.toISOString() : ''); + }} + /> +
+
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx new file mode 100644 index 0000000000..efd69a4acf --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; +import Link from '@material-ui/core/Link'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +import { ilertApiRef } from '../../api'; +import { UptimeMonitor } from '../../types'; + +export const UptimeMonitorActionsMenu = ({ + uptimeMonitor, + onUptimeMonitorChanged, +}: { + uptimeMonitor: UptimeMonitor; + onUptimeMonitorChanged?: (uptimeMonitor: UptimeMonitor) => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [anchorEl, setAnchorEl] = React.useState(null); + const callback = onUptimeMonitorChanged || ((_: UptimeMonitor): void => {}); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handlePause = async (): Promise => { + 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 => { + 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 => { + try { + const um = await ilertApi.fetchUptimeMonitor(uptimeMonitor.id); + handleCloseMenu(); + window.open(um.shareUrl, '_blank'); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + }; + + return ( + <> + + + + + {uptimeMonitor.paused ? ( + + + Resume + + + ) : null} + + {!uptimeMonitor.paused ? ( + + + Pause + + + ) : null} + + + + View Report + + + + + + + View in iLert + + + + + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx new file mode 100644 index 0000000000..0af5cd89df --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { UptimeMonitor } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const UptimeMonitorLink = ({ + uptimeMonitor, +}: { + uptimeMonitor: UptimeMonitor | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!uptimeMonitor) { + return null; + } + + return ( + + #{uptimeMonitor.id} + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/index.ts b/plugins/ilert/src/components/UptimeMonitor/index.ts new file mode 100644 index 0000000000..f30d1ef15a --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx new file mode 100644 index 0000000000..dd17b1758b --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx @@ -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; + +export const StatusChip = ({ + uptimeMonitor, +}: { + uptimeMonitor: UptimeMonitor; +}) => { + let label = `${uptimeMonitorStatusLabels[uptimeMonitor.status]}`; + + if (uptimeMonitor.paused) { + label = 'Paused'; + return ; + } + + switch (uptimeMonitor.status) { + case 'up': + return ; + case 'down': + return ; + case 'unknown': + return ; + default: + return ; + } +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx new file mode 100644 index 0000000000..3fdecd65e9 --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx @@ -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 ( + {`${uptimeMonitor.checkType.toUpperCase()} 🇩🇪`} + ); + default: + return ( + {`${uptimeMonitor.checkType.toUpperCase()} 🇺🇸`} + ); + } +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx new file mode 100644 index 0000000000..dc7ea77a6c --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { UptimeMonitorsTable } from './UptimeMonitorsTable'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useUptimeMonitors } from '../../hooks/useUptimeMonitors'; + +export const UptimeMonitorsPage = () => { + const [ + { tableState, uptimeMonitors, isLoading, error }, + { onChangePage, onChangeRowsPerPage, onUptimeMonitorChanged }, + ] = useUptimeMonitors(); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx new file mode 100644 index 0000000000..bd55310ad7 --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx @@ -0,0 +1,173 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn } from '@backstage/core'; +import { TableState } from '../../api'; +import { UptimeMonitor } from '../../types'; +import { StatusChip } from './StatusChip'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment'; +import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink'; +import { UptimeMonitorCheckType } from './UptimeMonitorCheckType'; +import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu'; +import { UptimeMonitorLink } from '../UptimeMonitor/UptimeMonitorLink'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export const UptimeMonitorsTable = ({ + uptimeMonitors, + tableState, + isLoading, + onChangePage, + onChangeRowsPerPage, + onUptimeMonitorChanged, +}: { + uptimeMonitors: UptimeMonitor[]; + tableState: TableState; + isLoading: boolean; + onChangePage: (page: number) => void; + onChangeRowsPerPage: (pageSize: number) => void; + onUptimeMonitorChanged: (uptimeMonitor: UptimeMonitor) => void; +}) => { + const classes = useStyles(); + + const smColumnStyle = { + width: '5%', + maxWidth: '5%', + }; + const mdColumnStyle = { + width: '10%', + maxWidth: '10%', + }; + const lgColumnStyle = { + width: '15%', + maxWidth: '15%', + }; + + const columns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + highlight: true, + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Name', + field: 'name', + render: rowData => ( + {(rowData as UptimeMonitor).name} + ), + }, + { + title: 'Check Type', + field: 'checkType', + cellStyle: lgColumnStyle, + headerStyle: lgColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Last state change', + field: 'lastStatusChange', + type: 'datetime', + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + {moment + .duration( + moment((rowData as UptimeMonitor).lastStatusChange).diff( + moment(), + ), + ) + .humanize()} + + ), + }, + { + title: 'Escalation policy', + field: 'assignedTo', + cellStyle: lgColumnStyle, + headerStyle: lgColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Status', + field: 'status', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + ), + }, + { + title: '', + field: '', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + ), + }, + ]; + + return ( +
+ No uptime monitor + + } + page={tableState.page} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangeRowsPerPage} + localization={{ header: { actions: undefined } }} + isLoading={isLoading} + columns={columns} + data={uptimeMonitors} + /> + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/index.ts b/plugins/ilert/src/components/UptimeMonitorsPage/index.ts new file mode 100644 index 0000000000..d1a8809a4e --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/index.ts b/plugins/ilert/src/components/index.ts new file mode 100644 index 0000000000..81f7e02e23 --- /dev/null +++ b/plugins/ilert/src/components/index.ts @@ -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'; diff --git a/plugins/ilert/src/constants.ts b/plugins/ilert/src/constants.ts new file mode 100644 index 0000000000..edaa06b293 --- /dev/null +++ b/plugins/ilert/src/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const ILERT_INTEGRATION_KEY = 'ilert.com/integration-key'; diff --git a/plugins/ilert/src/hooks/index.ts b/plugins/ilert/src/hooks/index.ts new file mode 100644 index 0000000000..e21c9584d2 --- /dev/null +++ b/plugins/ilert/src/hooks/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { ILERT_INTEGRATION_KEY } from '../constants'; + +export function useILertEntity() { + const { entity } = useEntity(); + const integrationKey = + entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || ''; + const name = entity.metadata.name; + + return { integrationKey, name }; +} diff --git a/plugins/ilert/src/hooks/useAlertSource.ts b/plugins/ilert/src/hooks/useAlertSource.ts new file mode 100644 index 0000000000..950f8f41c6 --- /dev/null +++ b/plugins/ilert/src/hooks/useAlertSource.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource, UptimeMonitor } from '../types'; + +export const useAlertSource = (integrationKey: string) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [alertSource, setAlertSource] = React.useState( + null, + ); + const [isAlertSourceLoading, setIsAlertSourceLoading] = React.useState(false); + const [ + uptimeMonitor, + setUptimeMonitor, + ] = React.useState(null); + const [isUptimeMonitorLoading, setIsUptimeMonitorLoading] = React.useState( + false, + ); + + const fetchAlertSourceCall = async () => { + try { + if (!integrationKey) { + return; + } + setIsAlertSourceLoading(true); + const data = await ilertApi.fetchAlertSource(integrationKey); + setAlertSource(data || null); + setIsAlertSourceLoading(false); + } catch (e) { + setIsAlertSourceLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { + error: alertSourceError, + retry: alertSourceRetry, + } = useAsyncRetry(fetchAlertSourceCall, [integrationKey]); + + const fetchUptimeMonitorCall = async () => { + try { + if (!alertSource || alertSource.integrationType !== 'MONITOR') { + return; + } + setIsUptimeMonitorLoading(true); + const data = await ilertApi.fetchUptimeMonitor(alertSource.id); + setUptimeMonitor(data || null); + setIsUptimeMonitorLoading(false); + } catch (e) { + setIsUptimeMonitorLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { + error: uptimeMonitorError, + retry: uptimeMonitorRetry, + } = useAsyncRetry(fetchUptimeMonitorCall, [alertSource]); + + const retry = () => { + alertSourceRetry(); + uptimeMonitorRetry(); + }; + + return [ + { + alertSource, + uptimeMonitor, + error: alertSourceError || uptimeMonitorError, + isLoading: isAlertSourceLoading || isUptimeMonitorLoading, + }, + { + retry, + setIsLoading: setIsAlertSourceLoading, + refetchAlertSource: fetchAlertSourceCall, + setAlertSource, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useAssignIncident.ts b/plugins/ilert/src/hooks/useAssignIncident.ts new file mode 100644 index 0000000000..1671acdc51 --- /dev/null +++ b/plugins/ilert/src/hooks/useAssignIncident.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Incident, IncidentResponder } from '../types'; + +export const useAssignIncident = (incident: Incident | null, open: boolean) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [incidentRespondersList, setIncidentRespondersList] = React.useState< + IncidentResponder[] + >([]); + const [ + incidentResponder, + setIncidentResponder, + ] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!incident || !open) { + return; + } + const data = await ilertApi.fetchIncidentResponders(incident); + if (data && Array.isArray(data)) { + const groups = [ + 'SUGGESTED', + 'USER', + 'ESCALATION_POLICY', + 'ON_CALL_SCHEDULE', + ]; + data.sort((a, b) => groups.indexOf(a.group) - groups.indexOf(b.group)); + setIncidentRespondersList(data); + } + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [incident, open]); + + return [ + { + incidentRespondersList, + incidentResponder, + error, + isLoading, + }, + { + setIncidentRespondersList, + setIncidentResponder, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts new file mode 100644 index 0000000000..a252dfb14f --- /dev/null +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + GetIncidentsOpts, + ilertApiRef, + TableState, + UnauthorizedError, +} from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types'; + +export const useIncidents = ( + paging: boolean, + alertSources?: number[] | string[], +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [tableState, setTableState] = React.useState({ + page: 0, + pageSize: 10, + }); + const [states, setStates] = React.useState([ + ACCEPTED, + PENDING, + ]); + const [incidentsList, setIncidentsList] = React.useState([]); + const [incidentsCount, setIncidentsCount] = React.useState(0); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchIncidentsCall = async () => { + try { + setIsLoading(true); + const opts: GetIncidentsOpts = { + states, + alertSources, + }; + if (paging) { + opts.maxResults = tableState.pageSize; + opts.startIndex = tableState.page * tableState.pageSize; + } + const data = await ilertApi.fetchIncidents(opts); + setIncidentsList(data || []); + setIsLoading(false); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + setIsLoading(false); + throw e; + } + }; + + const fetchIncidentsCountCall = async () => { + try { + const count = await ilertApi.fetchIncidentsCount({ states }); + setIncidentsCount(count || 0); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [ + tableState, + states, + ]); + + const refetchIncidents = () => { + setTableState({ ...tableState, page: 0 }); + Promise.all([fetchIncidentsCall(), fetchIncidentsCountCall()]); + }; + + const fetchIncidentsCount = useAsyncRetry(fetchIncidentsCountCall, [states]); + + const error = fetchIncidents.error || fetchIncidentsCount.error; + const retry = () => { + fetchIncidents.retry(); + fetchIncidentsCount.retry(); + }; + + const onIncidentChanged = (newIncident: Incident) => { + let shouldRefetchIncidents = false; + setIncidentsList( + incidentsList.reduce((acc: Incident[], incident: Incident) => { + if (newIncident.id === incident.id) { + if (states.includes(newIncident.status)) { + acc.push(newIncident); + } else { + shouldRefetchIncidents = true; + } + return acc; + } + acc.push(incident); + return acc; + }, []), + ); + if (shouldRefetchIncidents) { + refetchIncidents(); + } + }; + + const onChangePage = (page: number) => { + setTableState({ ...tableState, page }); + }; + const onChangeRowsPerPage = (p: number) => { + setTableState({ ...tableState, pageSize: p }); + }; + const onIncidentStatesChange = (s: IncidentStatus[]) => { + setStates(s); + }; + + return [ + { + tableState, + states, + incidents: incidentsList, + incidentsCount, + error, + isLoading, + }, + { + setTableState, + setStates, + setIncidentsList, + setIsLoading, + retry, + onIncidentChanged, + refetchIncidents, + onChangePage, + onChangeRowsPerPage, + onIncidentStatesChange, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useNewIncident.ts b/plugins/ilert/src/hooks/useNewIncident.ts new file mode 100644 index 0000000000..669fa74b63 --- /dev/null +++ b/plugins/ilert/src/hooks/useNewIncident.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource } from '../types'; + +export const useNewIncident = ( + open: boolean, + initialAlertSource?: AlertSource | null, +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [alertSourcesList, setAlertSourcesList] = React.useState( + [], + ); + const [alertSource, setAlertSource] = React.useState( + null, + ); + const [summary, setSummary] = React.useState(''); + const [details, setDetails] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchAlertSources = useAsyncRetry(async () => { + try { + if (!open || initialAlertSource) { + return; + } + const count = await ilertApi.fetchAlertSources(); + setAlertSourcesList(count || 0); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [open]); + + const error = fetchAlertSources.error; + const retry = () => { + fetchAlertSources.retry(); + }; + + return [ + { + alertSources: alertSourcesList, + alertSource: initialAlertSource ? initialAlertSource : alertSource, + summary, + details, + error, + isLoading, + }, + { + setAlertSourcesList, + setAlertSource, + setSummary, + setDetails, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useOnCallSchedules.ts b/plugins/ilert/src/hooks/useOnCallSchedules.ts new file mode 100644 index 0000000000..5e0538a370 --- /dev/null +++ b/plugins/ilert/src/hooks/useOnCallSchedules.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Schedule } from '../types'; + +export const useOnCallSchedules = () => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [onCallSchedulesList, setOnCallSchedulesList] = React.useState< + Schedule[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchOnCallSchedulesCall = async () => { + try { + setIsLoading(true); + const data = await ilertApi.fetchOnCallSchedules(); + setOnCallSchedulesList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { error, retry } = useAsyncRetry(fetchOnCallSchedulesCall, []); + + return [ + { + onCallSchedules: onCallSchedulesList, + error, + isLoading, + }, + { + retry, + setIsLoading, + refetchOnCallSchedules: fetchOnCallSchedulesCall, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useShiftOverride.ts b/plugins/ilert/src/hooks/useShiftOverride.ts new file mode 100644 index 0000000000..fd1da7423b --- /dev/null +++ b/plugins/ilert/src/hooks/useShiftOverride.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { User, Shift } from '../types'; + +export const useShiftOverride = (s: Shift, isModalOpened: boolean) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [shift, setShift] = React.useState(s); + const [usersList, setUsersList] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!isModalOpened) { + return; + } + setIsLoading(true); + const data = await ilertApi.fetchUsers(); + setUsersList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [isModalOpened]); + + const setUser = (user: User) => { + setShift({ ...shift, user }); + }; + + const setStart = (start: string) => { + setShift({ ...shift, start }); + }; + + const setEnd = (end: string) => { + setShift({ ...shift, end }); + }; + + return [ + { + shift, + users: usersList, + user: shift.user, + start: shift.start, + end: shift.end, + error, + isLoading, + }, + { + retry, + setIsLoading, + setUser, + setStart, + setEnd, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useUptimeMonitors.ts b/plugins/ilert/src/hooks/useUptimeMonitors.ts new file mode 100644 index 0000000000..02cd5aaa2f --- /dev/null +++ b/plugins/ilert/src/hooks/useUptimeMonitors.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, TableState, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { UptimeMonitor } from '../types'; + +export const useUptimeMonitors = () => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [tableState, setTableState] = React.useState({ + page: 0, + pageSize: 10, + }); + const [uptimeMonitorsList, setUptimeMonitorsList] = React.useState< + UptimeMonitor[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + setIsLoading(true); + const data = await ilertApi.fetchUptimeMonitors(); + setUptimeMonitorsList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [tableState]); + + const onUptimeMonitorChanged = (newUptimeMonitor: UptimeMonitor) => { + setUptimeMonitorsList( + uptimeMonitorsList.map( + (uptimeMonitor: UptimeMonitor): UptimeMonitor => { + if (newUptimeMonitor.id === uptimeMonitor.id) { + return newUptimeMonitor; + } + + return uptimeMonitor; + }, + ), + ); + }; + + const onChangePage = (page: number) => { + setTableState({ ...tableState, page }); + }; + const onChangeRowsPerPage = (pageSize: number) => { + setTableState({ ...tableState, pageSize }); + }; + + return [ + { + tableState, + uptimeMonitors: uptimeMonitorsList, + error, + isLoading, + }, + { + setTableState, + setUptimeMonitorsList, + retry, + onUptimeMonitorChanged, + onChangePage, + onChangeRowsPerPage, + setIsLoading, + }, + ] as const; +}; diff --git a/plugins/ilert/src/index.ts b/plugins/ilert/src/index.ts new file mode 100644 index 0000000000..2e5301f151 --- /dev/null +++ b/plugins/ilert/src/index.ts @@ -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; diff --git a/plugins/ilert/src/plugin.test.ts b/plugins/ilert/src/plugin.test.ts new file mode 100644 index 0000000000..89a0cb231f --- /dev/null +++ b/plugins/ilert/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/ilert/src/plugin.ts b/plugins/ilert/src/plugin.ts new file mode 100644 index 0000000000..546b77911d --- /dev/null +++ b/plugins/ilert/src/plugin.ts @@ -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), + }, + }), +); diff --git a/plugins/ilert/src/route-refs.tsx b/plugins/ilert/src/route-refs.tsx new file mode 100644 index 0000000000..b63ba15315 --- /dev/null +++ b/plugins/ilert/src/route-refs.tsx @@ -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', +}); diff --git a/plugins/ilert/src/setupTests.ts b/plugins/ilert/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/ilert/src/setupTests.ts @@ -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'; diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts new file mode 100644 index 0000000000..dd9aab42d7 --- /dev/null +++ b/plugins/ilert/src/types.ts @@ -0,0 +1,312 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Incident { + id: number; + summary: string; + details: string; + reportTime: string; + resolvedOn: string; + status: IncidentStatus; + priority: IncidentPriority; + incidentKey: string; + alertSource: AlertSource | null; + assignedTo: User | null; + logEntries: LogEntry[]; + links: Link[]; + images: Image[]; + subscribers: Subscriber[]; + commentText: string; + commentPublishToSubscribers: boolean; +} + +export const PENDING = 'PENDING'; +export const ACCEPTED = 'ACCEPTED'; +export const RESOLVED = 'RESOLVED'; +export type IncidentStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED; +export type IncidentPriority = 'HIGH' | 'LOW'; + +export interface Link { + href: string; + text: string; +} + +export interface Image { + src: string; + href: string; + alt: string; +} + +export type SubscriberType = 'TEAM' | 'USER'; + +export interface Subscriber { + id: number; + name: string; + type: SubscriberType; +} + +export interface LogEntry { + id: number; + timestamp: string; + logEntryType: string; + text: string; + incidentId?: number; + iconName?: string; + iconClass?: string; + filterTypes?: string[]; +} + +export interface User { + id: number; + username: string; + firstName: string; + lastName: string; + email: string; + mobile: Phone; + landline: Phone; + timezone?: string; + language?: Language; + role?: UserRole; + notificationPreferences?: any[]; + position: string; + department: string; +} + +export type UserRole = + | 'USER' + | 'ADMIN' + | 'STAKEHOLDER' + | 'ACCOUNT_OWNER' + | 'RESPONDER'; +export type Language = 'de' | 'en'; +export interface Phone { + regionCode: string; + number: string; +} + +export interface AlertSource { + id: number; + name: string; + status: AlertSourceStatus; + escalationPolicy: EscalationPolicy; + integrationType: AlertSourceIntegrationType; + integrationKey?: string; + iconUrl?: string; + lightIconUrl?: string; + darkIconUrl?: string; + incidentCreation?: AlertSourceIncidentCreation; + incidentPriorityRule?: AlertSourceIncidentPriorityRule; + emailFiltered?: boolean; + emailResolveFiltered?: boolean; + active?: boolean; + emailPredicates?: AlertSourceEmailPredicate[]; + emailResolvePredicates?: AlertSourceEmailPredicate[]; + filterOperator?: AlertSourceFilterOperator; + resolveFilterOperator?: AlertSourceFilterOperator; + supportHours?: AlertSourceSupportHours; + heartbeat?: AlertSourceHeartbeat; + autotaskMetadata?: AlertSourceAutotaskMetadata; + autoResolutionTimeout?: string; + teams: TeamShort[]; +} + +export interface TeamShort { + id: number; + name: string; +} + +export interface TeamMember { + user: User; + role: 'STAKEHOLDER' | 'RESPONDER' | 'USER' | 'ADMIN'; +} + +export type AlertSourceStatus = + | 'PENDING' + | 'ALL_ACCEPTED' + | 'ALL_RESOLVED' + | 'IN_MAINTENANCE' + | 'DISABLED'; +export type AlertSourceIntegrationType = + | 'NAGIOS' + | 'ICINGA' + | 'EMAIL' + | 'SMS' + | 'API' + | 'CRN' + | 'HEARTBEAT' + | 'PRTG' + | 'PINGDOM' + | 'CLOUDWATCH' + | 'AWSPHD' + | 'STACKDRIVER' + | 'INSTANA' + | 'ZABBIX' + | 'SOLARWINDS' + | 'PROMETHEUS' + | 'NEWRELIC' + | 'GRAFANA' + | 'GITHUB' + | 'DATADOG' + | 'UPTIMEROBOT' + | 'APPDYNAMICS' + | 'DYNATRACE' + | 'TOPDESK' + | 'STATUSCAKE' + | 'MONITOR' + | 'TOOL' + | 'CHECKMK' + | 'AUTOTASK' + | 'AWSBUDGET' + | 'KENTIXAM' + | 'CONSUL' + | 'ZAMMAD' + | 'SIGNALFX' + | 'SPLUNK' + | 'KUBERNETES' + | 'SEMATEXT' + | 'SENTRY' + | 'SUMOLOGIC' + | 'RAYGUN' + | 'MXTOOLBOX' + | 'ESWATCHER' + | 'AMAZONSNS' + | 'KAPACITOR' + | 'CORTEXXSOAR' + | string; +export type AlertSourceIncidentCreation = + | 'ONE_INCIDENT_PER_EMAIL' + | 'ONE_INCIDENT_PER_EMAIL_SUBJECT' + | 'ONE_PENDING_INCIDENT_ALLOWED' + | 'ONE_OPEN_INCIDENT_ALLOWED' + | 'OPEN_RESOLVE_ON_EXTRACTION'; +export type AlertSourceFilterOperator = 'AND' | 'OR'; +export type AlertSourceIncidentPriorityRule = + | 'HIGH' + | 'LOW' + | 'HIGH_DURING_SUPPORT_HOURS' + | 'LOW_DURING_SUPPORT_HOURS'; +export interface AlertSourceEmailPredicate { + field: 'EMAIL_FROM' | 'EMAIL_SUBJECT' | 'EMAIL_BODY'; + criteria: + | 'CONTAINS_ANY_WORDS' + | 'CONTAINS_NOT_WORDS' + | 'CONTAINS_STRING' + | 'CONTAINS_NOT_STRING' + | 'IS_STRING' + | 'IS_NOT_STRING' + | 'MATCHES_REGEX' + | 'MATCHES_NOT_REGEX'; + value: string; +} +export type AlertSourceTimeZone = + | 'Europe/Berlin' + | 'America/New_York' + | 'America/Los_Angeles' + | 'Asia/Istanbul'; +export interface AlertSourceSupportDay { + start: string; + end: string; +} +export interface AlertSourceSupportHours { + timezone: AlertSourceTimeZone; + autoRaiseIncidents: boolean; + supportDays: { + MONDAY: AlertSourceSupportDay; + TUESDAY: AlertSourceSupportDay; + WEDNESDAY: AlertSourceSupportDay; + THURSDAY: AlertSourceSupportDay; + FRIDAY: AlertSourceSupportDay; + SATURDAY: AlertSourceSupportDay; + SUNDAY: AlertSourceSupportDay; + }; +} +export interface AlertSourceHeartbeat { + summary: string; + intervalSec: number; + status: 'OVERDUE' | 'ON_TIME' | 'NEVER_RECEIVED'; +} + +export interface AlertSourceAutotaskMetadata { + userName: string; + secret: string; + apiIntegrationCode: string; + webServer: string; +} + +export interface EscalationPolicy { + id: number; + name: string; + escalationRules: EscalationRule[]; + newEscalationRule: EscalationRule; + repeating?: boolean; + frequency?: number; + teams: TeamShort[]; +} + +export interface EscalationRule { + user: User | null; + schedule: Schedule | null; + escalationTimeout: number; +} + +export interface Schedule { + id: number; + name: string; + timezone: string; + startsOn: string; + currentShift: Shift; + nextShift: Shift; + shifts: Shift[]; + overrides: Shift[]; + teams: TeamShort[]; +} + +export interface Shift { + user: User; + start: string; + end: string; +} + +export interface UptimeMonitor { + id: number; + name: string; + region: 'EU' | 'US'; + checkType: 'http' | 'tcp' | 'udp' | 'ping'; + checkParams: UptimeMonitorCheckParams; + intervalSec: number; + timeoutMs: number; + createIncidentAfterFailedChecks: number; + paused: boolean; + embedUrl: string; + shareUrl: string; + status: string; + lastStatusChange: string; + escalationPolicy: EscalationPolicy; + teams: TeamShort[]; +} + +export interface UptimeMonitorCheckParams { + host?: string; + port?: number; + url?: string; +} + +export interface IncidentResponder { + group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE'; + id: number; + name: string; + disabled: boolean; +} From 687d973c6be3c5511743a64d73cf19e3a61a867d Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 14 Apr 2021 20:31:18 +0200 Subject: [PATCH 02/17] add incident actions to ilert plugin Signed-off-by: yacut --- plugins/ilert/README.md | 10 ++- plugins/ilert/src/api/client.ts | 60 ++++++++++++++---- plugins/ilert/src/api/types.ts | 12 +++- .../src/components/ILertCard/ILertCard.tsx | 3 +- .../Incident/IncidentActionsMenu.tsx | 54 +++++++++++++++- plugins/ilert/src/hooks/useIncidentActions.ts | 61 +++++++++++++++++++ plugins/ilert/src/hooks/useIncidents.ts | 18 +++++- plugins/ilert/src/types.ts | 16 +++++ 8 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 plugins/ilert/src/hooks/useIncidentActions.ts diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 55e41ba5bd..0265f4172b 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -17,6 +17,9 @@ This plugin provides: - A list of incidents - A way to trigger a new incident - A way to reassign/acknowledge/resolve an incident +- A way to trigger an incident action +- A way to trigger an immediate maintenance +- A way to disable/enable an alert source - A list of uptime monitors ## Setup instructions @@ -41,7 +44,6 @@ import { isPluginApplicableToEntity as isILertAvailable, EntityILertCard, } from '@backstage/plugin-ilert'; -// add to code { isILertAvailable(entity) && ( @@ -58,10 +60,8 @@ import { Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: ```tsx -// At the top imports import { ILertPage } from '@backstage/plugin-ilert'; -// Inside App component // ... } /> @@ -72,10 +72,8 @@ import { ILertPage } from '@backstage/plugin-ilert'; Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: ```tsx -// At the top imports import { ILertIcon } from '@backstage/plugin-ilert'; -// Inside Sidebar component // ... @@ -91,7 +89,7 @@ In `app-config.yaml`: ```yaml ilert: - domain: https://my-org.ilert.com/ + baseUrl: https://my-org.ilert.com/ ``` ## Providing the Authorization Header diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index 259502eff0..ed79681320 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -18,6 +18,7 @@ import { AlertSource, EscalationPolicy, Incident, + IncidentAction, IncidentResponder, Schedule, UptimeMonitor, @@ -44,22 +45,22 @@ export class UnauthorizedError extends Error {} export class ILertClient implements ILertApi { private readonly discoveryApi: DiscoveryApi; private readonly proxyPath: string; - private readonly domain: string; + private readonly baseUrl: string; static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { - const domainUrl: string = - configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com'; + const baseUrl: string = + configApi.getOptionalString('ilert.baseUrl') ?? 'https://app.ilert.com'; return new ILertClient({ discoveryApi: discoveryApi, - domain: domainUrl, + baseUrl, proxyPath: configApi.getOptionalString('ilert.proxyPath'), }); } constructor(opts: Options) { this.discoveryApi = opts.discoveryApi; - this.domain = opts.domain; + this.baseUrl = opts.baseUrl; this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; } @@ -94,7 +95,7 @@ export class ILertClient implements ILertApi { query.append('start-index', `${opts.startIndex}`); } if (opts && opts.alertSources) { - opts.alertSources.forEach((a: string | number) => { + opts.alertSources.forEach((a: number) => { if (a) { query.append('alert-source', `${a}`); } @@ -153,6 +154,22 @@ export class ILertClient implements ILertApi { return response; } + async fetchIncidentActions(incident: Incident): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/actions`, + init, + ); + + return response; + } + async acceptIncident(incident: Incident): Promise { const init = { method: 'PUT', @@ -222,6 +239,27 @@ export class ILertClient implements ILertApi { return response; } + async triggerIncidentAction( + incident: Incident, + action: IncidentAction, + ): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + webhookId: action.webhookId, + extensionId: action.extensionId, + type: action.type, + name: action.name, + }), + }; + + await this.fetch(`/api/v1/incidents/${incident.id}/actions`, init); + } + async createIncident(eventRequest: EventRequest): Promise { const init = { method: 'POST', @@ -461,26 +499,26 @@ export class ILertClient implements ILertApi { } getIncidentDetailsURL(incident: Incident): string { - return `${this.domain}/incident/view.jsf?id=${incident.id}`; + return `${this.baseUrl}/incident/view.jsf?id=${incident.id}`; } getAlertSourceDetailsURL(alertSource: AlertSource | null): string { if (!alertSource) { return ''; } - return `${this.domain}/source/view.jsf?id=${alertSource.id}`; + return `${this.baseUrl}/source/view.jsf?id=${alertSource.id}`; } getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { - return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`; + return `${this.baseUrl}/policy/view.jsf?id=${escalationPolicy.id}`; } getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { - return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`; + return `${this.baseUrl}/uptime/view.jsf?id=${uptimeMonitor.id}`; } getScheduleDetailsURL(schedule: Schedule): string { - return `${this.domain}/schedule/view.jsf?id=${schedule.id}`; + return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; } getUserInitials(assignedTo: User | null) { diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index 4c33455d86..b37ae73c93 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -23,6 +23,7 @@ import { EscalationPolicy, Schedule, IncidentResponder, + IncidentAction, } from '../types'; export type TableState = { @@ -34,7 +35,7 @@ export type GetIncidentsOpts = { maxResults?: number; startIndex?: number; states?: IncidentStatus[]; - alertSources?: number[] | string[]; + alertSources?: number[]; }; export type GetIncidentsCountOpts = { @@ -53,6 +54,7 @@ export interface ILertApi { fetchIncidents(opts?: GetIncidentsOpts): Promise; fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; fetchIncidentResponders(incident: Incident): Promise; + fetchIncidentActions(incident: Incident): Promise; acceptIncident(incident: Incident): Promise; resolveIncident(incident: Incident): Promise; assignIncident( @@ -60,6 +62,10 @@ export interface ILertApi { responder: IncidentResponder, ): Promise; createIncident(eventRequest: EventRequest): Promise; + triggerIncidentAction( + incident: Incident, + action: IncidentAction, + ): Promise; fetchUptimeMonitors(): Promise; pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; @@ -98,10 +104,10 @@ export type Options = { discoveryApi: DiscoveryApi; /** - * Domain used by users to access iLert web UI. + * URL used by users to access iLert web UI. * Example: https://my-org.ilert.com/ */ - domain: string; + baseUrl: string; /** * Path to use for requests via the proxy, defaults to /ilert/api diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index b376b319b5..9e2c93d3da 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -56,7 +56,6 @@ export const ILertCard = () => { { alertSource, uptimeMonitor }, { setAlertSource, refetchAlertSource }, ] = useAlertSource(integrationKey); - const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey]; const [ { tableState, states, incidents, incidentsCount, isLoading, error }, { @@ -67,7 +66,7 @@ export const ILertCard = () => { refetchIncidents, setIsLoading, }, - ] = useIncidents(false, alertSourcesFilter); + ] = useIncidents(false, true, alertSource); const [ isNewIncidentModalOpened, diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index 576f037c8c..0a1ae95ded 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, useApi } from '@backstage/core'; +import { alertApiRef, Progress, useApi } from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; -import { Incident } from '../../types'; +import { Incident, IncidentAction } from '../../types'; import { IncidentAssignModal } from './IncidentAssignModal'; +import { useIncidentActions } from '../../hooks/useIncidentActions'; export const IncidentActionsMenu = ({ incident, @@ -41,6 +42,11 @@ export const IncidentActionsMenu = ({ setIsAssignIncidentModalOpened, ] = React.useState(false); + const [{ incidentActions, isLoading }] = useIncidentActions( + incident, + Boolean(anchorEl), + ); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -84,6 +90,40 @@ export const IncidentActionsMenu = ({ setIsAssignIncidentModalOpened(true); }; + const handleTriggerAction = (action: IncidentAction) => async () => { + try { + handleCloseMenu(); + setProcessing(true); + await ilertApi.triggerIncidentAction(incident, action); + alertApi.post({ message: 'Incident action triggered.' }); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const actions: React.ReactNode[] = incidentActions.map(a => { + const successTrigger = a.history + ? a.history.find(h => h.success) + : undefined; + const triggeredBy = + successTrigger && successTrigger.actor + ? `${successTrigger.actor.firstName} ${successTrigger.actor.lastName}` + : ''; + return ( + + + {triggeredBy ? `${a.name} (by ${triggeredBy})` : a.name} + + + ); + }); + return ( <> {incident.status === 'PENDING' ? ( @@ -129,6 +169,14 @@ export const IncidentActionsMenu = ({ ) : null} + {isLoading ? ( + + + + ) : ( + actions + )} + diff --git a/plugins/ilert/src/hooks/useIncidentActions.ts b/plugins/ilert/src/hooks/useIncidentActions.ts new file mode 100644 index 0000000000..d8dd08e17a --- /dev/null +++ b/plugins/ilert/src/hooks/useIncidentActions.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Incident, IncidentAction } from '../types'; + +export const useIncidentActions = ( + incident: Incident | null, + open: boolean, +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [incidentActionsList, setIncidentActionsList] = React.useState< + IncidentAction[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!incident || !open) { + return; + } + const data = await ilertApi.fetchIncidentActions(incident); + setIncidentActionsList(data); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [incident, open]); + + return [ + { + incidentActions: incidentActionsList, + error, + isLoading, + }, + { + setIncidentActionsList, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts index a252dfb14f..2f3d12cf17 100644 --- a/plugins/ilert/src/hooks/useIncidents.ts +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -22,11 +22,18 @@ import { } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; -import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types'; +import { + ACCEPTED, + PENDING, + Incident, + IncidentStatus, + AlertSource, +} from '../types'; export const useIncidents = ( paging: boolean, - alertSources?: number[] | string[], + singleSource?: boolean, + alertSource?: AlertSource | null, ) => { const ilertApi = useApi(ilertApiRef); const errorApi = useApi(errorApiRef); @@ -45,10 +52,13 @@ export const useIncidents = ( const fetchIncidentsCall = async () => { try { + if (singleSource && !alertSource) { + return; + } setIsLoading(true); const opts: GetIncidentsOpts = { states, - alertSources, + alertSources: alertSource ? [alertSource.id] : [], }; if (paging) { opts.maxResults = tableState.pageSize; @@ -80,6 +90,8 @@ export const useIncidents = ( const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [ tableState, states, + singleSource, + alertSource, ]); const refetchIncidents = () => { diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts index dd9aab42d7..a209b99533 100644 --- a/plugins/ilert/src/types.ts +++ b/plugins/ilert/src/types.ts @@ -310,3 +310,19 @@ export interface IncidentResponder { name: string; disabled: boolean; } + +export interface IncidentAction { + name: string; + type: string; + webhookId: string; + extensionId?: string; + history?: IncidentActionHistory[]; +} + +export interface IncidentActionHistory { + id: string; + webhookId: string; + incidentId: number; + actor: User; + success: boolean; +} From 78589718b823c40aeddab15adb9d18e172abbd81 Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 16 Apr 2021 22:10:34 +0200 Subject: [PATCH 03/17] add on-call to entity card Signed-off-by: yacut --- plugins/ilert/src/api/client.ts | 43 ++++- plugins/ilert/src/api/types.ts | 5 +- .../src/components/ILertCard/ILertCard.tsx | 2 + .../components/ILertCard/ILertCardOnCall.tsx | 120 +++++++++++++ .../ILertCard/ILertCardOnCallEmptyState.tsx | 45 +++++ .../ILertCard/ILertCardOnCallItem.tsx | 169 ++++++++++++++++++ .../OnCallSchedulesGrid.tsx | 34 +++- .../ilert/src/hooks/useAlertSourceOnCalls.ts | 63 +++++++ plugins/ilert/src/types.ts | 9 + 9 files changed, 478 insertions(+), 12 deletions(-) create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx create mode 100644 plugins/ilert/src/hooks/useAlertSourceOnCalls.ts diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index ed79681320..f3c263e9e7 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -20,6 +20,7 @@ import { Incident, IncidentAction, IncidentResponder, + OnCall, Schedule, UptimeMonitor, User, @@ -388,6 +389,24 @@ export class ILertClient implements ILertApi { return response; } + async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/on-calls?policies=${ + alertSource.escalationPolicy.id + }&expand=user&expand=escalationPolicy&timezone=${momentTimezone.tz.guess()}`, + init, + ); + + return response; + } + async enableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', @@ -521,14 +540,28 @@ export class ILertClient implements ILertApi { return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; } - getUserInitials(assignedTo: User | null) { - if (!assignedTo) { + getUserPhoneNumber(user: User | null) { + if (!user) { return ''; } - if (!assignedTo.firstName && !assignedTo.lastName) { - return assignedTo.username; + if (user.mobile) { + return user.mobile.number; } - return `${assignedTo.firstName} ${assignedTo.lastName} (${assignedTo.username})`; + + if (user.landline) { + return user.landline.number; + } + return ''; + } + + 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() { diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index b37ae73c93..84b4ecf654 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -24,6 +24,7 @@ import { Schedule, IncidentResponder, IncidentAction, + OnCall, } from '../types'; export type TableState = { @@ -74,6 +75,7 @@ export interface ILertApi { fetchAlertSources(): Promise; fetchAlertSource(idOrIntegrationKey: number | string): Promise; + fetchAlertSourceOnCalls(alertSource: AlertSource): Promise; enableAlertSource(alertSource: AlertSource): Promise; disableAlertSource(alertSource: AlertSource): Promise; @@ -97,7 +99,8 @@ export interface ILertApi { getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; getScheduleDetailsURL(schedule: Schedule): string; - getUserInitials(assignedTo: User | null): string; + getUserPhoneNumber(user: User | null): string; + getUserInitials(user: User | null): string; } export type Options = { diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index 9e2c93d3da..dd4bdde3cf 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -33,6 +33,7 @@ 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]); @@ -111,6 +112,7 @@ export const ILertCard = () => { /> + ({ + 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 ; + } + + if (!alertSource || !onCalls) { + return null; + } + + const repeatInfo = () => { + if ( + !onCalls || + !onCalls.length || + !onCalls[onCalls.length - 1].escalationPolicy || + !onCalls[onCalls.length - 1].escalationPolicy.repeating || + !onCalls[onCalls.length - 1].escalationPolicy.frequency + ) { + return null; + } + + return ( + + + + + + {`Repeat ${ + onCalls[onCalls.length - 1].escalationPolicy.frequency + } times`} + + } + /> + + ); + }; + + if (!onCalls.length) { + return ( + ON CALL}> + + + ); + } + + if (onCalls.length === 1) { + ON CALL}> + + ; + } + + return ( + ON CALL}> + {onCalls.map((onCall, index) => ( + + ))} + {repeatInfo()} + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx new file mode 100644 index 0000000000..2d43aeccb8 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx @@ -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 ( + + + + Nobody + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx new file mode 100644 index 0000000000..69ceb0ad55 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx @@ -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 moment from 'moment'; + +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 ( + + {fistItem ? ( + + ) : null} + {lastItem ? ( + + ) : null} + {!fistItem && !lastItem ? ( + + ) : null} + + + {onCall.escalationLevel} + + + {onCall.schedule ? ( + + + {ilertApi.getUserInitials(onCall.user)} + + } + secondary={escalationText} + /> + + ) : ( + + {ilertApi.getUserInitials(onCall.user)} + + } + secondary={escalationText} + /> + )} + + {phoneNumber ? ( + + + + + + ) : null} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx index 34601009ff..c84e0a7b8d 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -44,9 +44,9 @@ const useStyles = makeStyles(() => ({ indicatorNext: { position: 'absolute', top: 'calc(40% - 10px)', - left: -8, - width: 16, - height: 16, + left: -6, + width: 12, + height: 12, background: '#92949c !important', borderRadius: '50%', }, @@ -54,11 +54,33 @@ const useStyles = makeStyles(() => ({ indicatorCurrent: { position: 'absolute', top: 'calc(40% - 10px)', - left: -8, - width: 16, - height: 16, + 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: { diff --git a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts new file mode 100644 index 0000000000..ac36c70097 --- /dev/null +++ b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource, OnCall } from '../types'; + +export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [onCallsList, setOnCallsList] = React.useState([]); + 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 UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { error, retry } = useAsyncRetry(fetchAlertSourceOnCallsCall, [ + alertSource, + ]); + + return [ + { + onCalls: onCallsList, + error, + isLoading, + }, + { + retry, + setIsLoading, + refetchAlertSourceOnCalls: fetchAlertSourceOnCallsCall, + }, + ] as const; +}; diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts index a209b99533..d7746c9d9b 100644 --- a/plugins/ilert/src/types.ts +++ b/plugins/ilert/src/types.ts @@ -326,3 +326,12 @@ export interface IncidentActionHistory { actor: User; success: boolean; } + +export interface OnCall { + user: User; + escalationPolicy: EscalationPolicy; + schedule?: Schedule; + start: string; + end: string; + escalationLevel: number; +} From 7ddf3ef3b4f5c2d40c84f58081ae7a78c1460c39 Mon Sep 17 00:00:00 2001 From: yacut Date: Sun, 18 Apr 2021 05:03:39 +0200 Subject: [PATCH 04/17] add app config and refactor card for iLert Signed-off-by: yacut --- app-config.yaml | 8 ++++++++ .../src/components/ILertCard/ILertCardOnCall.tsx | 13 +++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 309fa28234..03097069e6 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -82,6 +82,14 @@ proxy: Authorization: $env: SENTRY_TOKEN + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: + $env: ILERT_AUTH_HEADER + organization: name: My Company diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx index 9c347eeddc..8f28790375 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx @@ -55,11 +55,10 @@ export const ILertCardOnCall = ({ const repeatInfo = () => { if ( - !onCalls || - !onCalls.length || - !onCalls[onCalls.length - 1].escalationPolicy || - !onCalls[onCalls.length - 1].escalationPolicy.repeating || - !onCalls[onCalls.length - 1].escalationPolicy.frequency + !alertSource || + !alertSource.escalationPolicy || + !alertSource.escalationPolicy.repeating || + !alertSource.escalationPolicy.frequency ) { return null; } @@ -76,9 +75,7 @@ export const ILertCardOnCall = ({ color="textSecondary" className={classes.repeatText} > - {`Repeat ${ - onCalls[onCalls.length - 1].escalationPolicy.frequency - } times`} + {`Repeat ${alertSource.escalationPolicy.frequency} times`} } /> From 7d9739fe708a3e9169dfe23d4e0a159b5863d0d3 Mon Sep 17 00:00:00 2001 From: yacut Date: Tue, 20 Apr 2021 19:46:39 +0100 Subject: [PATCH 05/17] refactor ilert readme Signed-off-by: yacut --- microsite/data/plugins/ilert.yaml | 6 ++++-- plugins/ilert/README.md | 32 ++++++++++++++----------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml index 99cfca01bc..172f005ed1 100644 --- a/microsite/data/plugins/ilert.yaml +++ b/microsite/data/plugins/ilert.yaml @@ -1,9 +1,9 @@ --- title: iLert author: iLert -authorUrl: https://github.com/iLert +authorUrl: https://ilert.com category: Monitoring -description: iLert is a platform for alerting, on-call management and uptime 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' @@ -11,3 +11,5 @@ tags: - monitoring - errors - alerting + - uptime + - on-call diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 0265f4172b..130883d6e0 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -2,19 +2,18 @@ ## Introduction -[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as [informing stakeholders](https://docs.ilert.com/getting-started/stakeholder-engagement) or creating tickets in external incident management tools. +[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 displays iLert information about an entity such as if there are any active incidents, wo is on-call now and uptime monitor status. +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. -There is also an easy way to trigger an incident directly to the person who is currently on-call. +In detail this plugin provides: -This plugin provides: - -- Information details about the persons on-call -- A way to override current person on-call -- A list of incidents +- 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 @@ -30,8 +29,7 @@ Install the plugin: yarn add @backstage/plugin-ilert ``` -Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) -to enable the plugin: +Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: ```js export { plugin as ILert } from '@backstage/plugin-ilert'; @@ -53,15 +51,14 @@ import { } ``` -> To force iLert card for each entity just add the `` component, so an instruction card will appears if no integration key is set. +> To force an iLert card for each entity just add the `` component. An instruction card will appear if no integration key is set. -## Add iLert explorer to the App sidebar +## Add iLert explorer to the app sidebar -Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: +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'; - // ... } /> @@ -69,11 +66,10 @@ import { ILertPage } from '@backstage/plugin-ilert'; ; ``` -Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: +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'; - // ... @@ -94,7 +90,7 @@ ilert: ## 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). +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` @@ -110,7 +106,7 @@ proxy: $env: ILERT_AUTH_HEADER ``` -Then start the backend passing the token as an environment variable: +Then start the backend, passing the token as environment variable: ```bash $ ILERT_AUTH_HEADER='Basic ' yarn start From 3896a88351a268e0c97e64334445d7f2e48ebafe Mon Sep 17 00:00:00 2001 From: yacut Date: Tue, 20 Apr 2021 20:02:00 +0100 Subject: [PATCH 06/17] fix ilert plugin link Signed-off-by: yacut --- microsite/data/plugins/ilert.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml index 172f005ed1..29cc4f6aa5 100644 --- a/microsite/data/plugins/ilert.yaml +++ b/microsite/data/plugins/ilert.yaml @@ -1,7 +1,7 @@ --- title: iLert author: iLert -authorUrl: https://ilert.com +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 From d5c0b6b6ef946cafc9b8fe5b00b1a717bad722be Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 28 Apr 2021 03:51:05 +0100 Subject: [PATCH 07/17] adjust ilert readme Signed-off-by: yacut --- plugins/ilert/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 130883d6e0..87b67a2052 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -114,7 +114,7 @@ $ ILERT_AUTH_HEADER='Basic ' yarn start ## Integration Key -The information displayed for each entity is based on the [alert source integration key](https://docs.ilert.com/integrations/backstage). +The information displayed for each entity is based on the alert source integration key. ### Adding the integration key to the entity annotation From eedbe7452fbea9d28a839c0f670cd7fc911a9e5b Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 28 Apr 2021 03:58:56 +0100 Subject: [PATCH 08/17] Add iLert plugin Signed-off-by: Roman Frey roman@ilert.com Signed-off-by: yacut --- .changeset/blue-sloths-camp.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-sloths-camp.md diff --git a/.changeset/blue-sloths-camp.md b/.changeset/blue-sloths-camp.md new file mode 100644 index 0000000000..f05326831f --- /dev/null +++ b/.changeset/blue-sloths-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-ilert': minor +--- + +Add iLert plugin From 078e4457a215b76ea5da78c415d98b0951834aaa Mon Sep 17 00:00:00 2001 From: yacut Date: Mon, 3 May 2021 16:51:44 +0200 Subject: [PATCH 09/17] fix dev page for iLert plugin Signed-off-by: yacut --- plugins/ilert/dev/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/ilert/dev/index.tsx b/plugins/ilert/dev/index.tsx index c05e1da3d9..737666e650 100644 --- a/plugins/ilert/dev/index.tsx +++ b/plugins/ilert/dev/index.tsx @@ -16,12 +16,12 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { ilertPlugin } from '../src/plugin'; -import { IlertPage } from '../src'; +import { ILertPage } from '../src'; createDevApp() .registerPlugin(ilertPlugin) .addPage({ - element: , + element: , title: 'Root Page', }) .render(); From b4ea71e2193b1811246eaa79e147d3b7212fda46 Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 5 May 2021 19:51:56 +0200 Subject: [PATCH 10/17] upgrade deps for iLert plugin Signed-off-by: yacut Signed-off-by: yacut --- plugins/ilert/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index b31af34753..9a16960233 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.4", + "@backstage/core": "^0.7.7", "@backstage/plugin-catalog-react": "^0.1.4", - "@backstage/theme": "^0.2.5", + "@backstage/theme": "^0.2.6", "@date-io/date-fns": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,7 +37,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.7", + "@backstage/cli": "^0.6.9", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", From d27ec62d82ab8470c651090e873e41e9ffdd4906 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 21:59:57 +0100 Subject: [PATCH 11/17] Update app-config.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- app-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 3187feee63..b1c915f85a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -86,8 +86,7 @@ proxy: allowedMethods: ['GET', 'POST', 'PUT'] allowedHeaders: ['Authorization'] headers: - Authorization: - $env: ILERT_AUTH_HEADER + Authorization: ${ILERT_AUTH_HEADER} organization: name: My Company From 3b73a5ce9c1fab4bf441dc69a2bc30c22b319109 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 22:00:37 +0100 Subject: [PATCH 12/17] Update plugins/ilert/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- plugins/ilert/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 87b67a2052..c6b7a94731 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -26,6 +26,8 @@ In detail this plugin provides: Install the plugin: ```bash +# From the Backstage repository root +cd packages/app yarn add @backstage/plugin-ilert ``` From 40ce1954f12b05da3b747e224bb1034d3817c521 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 22:04:34 +0100 Subject: [PATCH 13/17] Update plugins/ilert/src/constants.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- plugins/ilert/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/src/constants.ts b/plugins/ilert/src/constants.ts index edaa06b293..bba644785f 100644 --- a/plugins/ilert/src/constants.ts +++ b/plugins/ilert/src/constants.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const ILERT_INTEGRATION_KEY = 'ilert.com/integration-key'; +export const ILERT_INTEGRATION_KEY_ANNOTATION = 'ilert.com/integration-key'; From 78a71f46ae91187c9ac25d1c31120264cb962c1b Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 7 May 2021 11:05:20 +0200 Subject: [PATCH 14/17] fix issues after first review for iLert plugin Signed-off-by: yacut --- .changeset/blue-sloths-camp.md | 5 - plugins/ilert/README.md | 38 +-- plugins/ilert/config.d.ts | 31 +++ plugins/ilert/package.json | 14 +- plugins/ilert/src/api/client.ts | 230 +++++++----------- plugins/ilert/src/api/index.ts | 2 +- plugins/ilert/src/api/types.ts | 7 +- .../AlertSource/AlertSourceLink.tsx | 5 +- .../EscalationPolicy/EscalationPolicyLink.tsx | 5 +- .../src/components/ILertCard/ILertCard.tsx | 16 +- .../ILertCard/ILertCardEmptyState.tsx | 6 +- .../ILertCard/ILertCardOnCallItem.tsx | 6 +- .../Incident/IncidentActionsMenu.tsx | 17 +- .../src/components/Incident/IncidentLink.tsx | 5 +- .../components/Incident/IncidentNewModal.tsx | 8 +- .../IncidentsPage/IncidentsPage.tsx | 24 +- .../IncidentsPage/IncidentsTable.tsx | 29 ++- .../OnCallSchedulesGrid.tsx | 5 +- .../OnCallSchedulesPage.tsx | 24 +- .../OnCallSchedulesPage/OnCallShiftItem.tsx | 8 +- .../components/Shift/ShiftOverrideModal.tsx | 8 +- .../UptimeMonitorActionsMenu.tsx | 9 +- .../UptimeMonitor/UptimeMonitorLink.tsx | 5 +- .../UptimeMonitorsPage/UptimeMonitorsPage.tsx | 24 +- .../UptimeMonitorsTable.tsx | 17 +- plugins/ilert/src/hooks/index.ts | 9 +- plugins/ilert/src/hooks/useAlertSource.ts | 7 +- .../ilert/src/hooks/useAlertSourceOnCalls.ts | 5 +- plugins/ilert/src/hooks/useAssignIncident.ts | 5 +- plugins/ilert/src/hooks/useIncidentActions.ts | 5 +- plugins/ilert/src/hooks/useIncidents.ts | 12 +- plugins/ilert/src/hooks/useNewIncident.ts | 5 +- plugins/ilert/src/hooks/useOnCallSchedules.ts | 5 +- plugins/ilert/src/hooks/useShiftOverride.ts | 5 +- plugins/ilert/src/hooks/useUptimeMonitors.ts | 5 +- 35 files changed, 312 insertions(+), 299 deletions(-) delete mode 100644 .changeset/blue-sloths-camp.md create mode 100644 plugins/ilert/config.d.ts diff --git a/.changeset/blue-sloths-camp.md b/.changeset/blue-sloths-camp.md deleted file mode 100644 index f05326831f..0000000000 --- a/.changeset/blue-sloths-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-ilert': minor ---- - -Add iLert plugin diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index c6b7a94731..da019d6bb6 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -31,12 +31,6 @@ cd packages/app yarn add @backstage/plugin-ilert ``` -Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: - -```js -export { plugin as ILert } from '@backstage/plugin-ilert'; -``` - Add it to the `EntityPage.tsx`: ```ts @@ -44,13 +38,16 @@ import { isPluginApplicableToEntity as isILertAvailable, EntityILertCard, } from '@backstage/plugin-ilert'; -{ - isILertAvailable(entity) && ( - + +// ... + + + - ); -} + +; +// ... ``` > To force an iLert card for each entity just add the `` component. An instruction card will appear if no integration key is set. @@ -61,11 +58,11 @@ Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blo ```tsx import { ILertPage } from '@backstage/plugin-ilert'; - + // ... } /> // ... -; +; ``` Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported by the plugin - for example: @@ -104,14 +101,13 @@ proxy: allowedMethods: ['GET', 'POST', 'PUT'] allowedHeaders: ['Authorization'] headers: - Authorization: - $env: ILERT_AUTH_HEADER + Authorization: ${ILERT_AUTH_HEADER} ``` -Then start the backend, passing the token as environment variable: +Then start the backend, passing the authorization header (bearer token or basic auth) as environment variable: ```bash -$ ILERT_AUTH_HEADER='Basic ' yarn start +$ ILERT_AUTH_HEADER='' yarn start ``` ## Integration Key @@ -123,6 +119,10 @@ The information displayed for each entity is based on the alert source integrati If you want to use this plugin for an entity, you need to label it with the below annotation: ```yml -annotations: - ilert.com/integration-key: [INTEGRATION_KEY] +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example + annotations: + ilert.com/integration-key: [INTEGRATION_KEY] ``` diff --git a/plugins/ilert/config.d.ts b/plugins/ilert/config.d.ts new file mode 100644 index 0000000000..52712454e8 --- /dev/null +++ b/plugins/ilert/config.d.ts @@ -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; + }; +} diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 9a16960233..31eff40dee 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -22,16 +22,16 @@ "dependencies": { "@backstage/catalog-model": "^0.7.6", "@backstage/core": "^0.7.7", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.4", "@backstage/theme": "^0.2.6", - "@date-io/date-fns": "1.x", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/pickers": "^3.3.10", - "date-fns": "^2.20.2", - "moment": "^2.29.1", - "moment-timezone": "^0.5.33", + "humanize-duration": "^3.26.0", + "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" @@ -49,6 +49,8 @@ "msw": "^0.21.2" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index f3c263e9e7..e635d76b99 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core'; +import { AuthenticationError, ResponseError } from '@backstage/errors'; import { AlertSource, EscalationPolicy, @@ -32,8 +33,7 @@ import { Options, EventRequest, } from './types'; -import moment from 'moment'; -import momentTimezone from 'moment-timezone'; +import { DateTime as dt } from 'luxon'; export const ilertApiRef = createApiRef({ id: 'plugin.ilert.service', @@ -41,7 +41,10 @@ export const ilertApiRef = createApiRef({ }); const DEFAULT_PROXY_PATH = '/ilert'; -export class UnauthorizedError extends Error {} +const JSON_HEADERS = { + 'Content-Type': 'application/json', + Accept: 'application/json', +}; export class ILertClient implements ILertApi { private readonly discoveryApi: DiscoveryApi; @@ -55,14 +58,15 @@ export class ILertClient implements ILertApi { return new ILertClient({ discoveryApi: discoveryApi, baseUrl, - proxyPath: configApi.getOptionalString('ilert.proxyPath'), + proxyPath: + configApi.getOptionalString('ilert.proxyPath') ?? DEFAULT_PROXY_PATH, }); } constructor(opts: Options) { this.discoveryApi = opts.discoveryApi; this.baseUrl = opts.baseUrl; - this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; + this.proxyPath = opts.proxyPath; } private async fetch(input: string, init?: RequestInit): Promise { @@ -70,40 +74,37 @@ export class ILertClient implements ILertApi { const response = await fetch(`${apiUrl}${input}`, init); if (response.status === 401) { - throw new UnauthorizedError(''); - } - if (!response.ok) { - throw new Error( - `Request failed with ${response.status} ${response.statusText}`, + throw new AuthenticationError( + 'This request requires HTTP authentication.', ); } + if (!response.ok || response.status >= 400) { + throw await ResponseError.fromResponse(response); + } return await response.json(); } async fetchIncidents(opts?: GetIncidentsOpts): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); - if (opts && opts.maxResults) { - query.append('max-results', `${opts.maxResults}`); + if (opts?.maxResults !== undefined) { + query.append('max-results', String(opts.maxResults)); } - if (opts && opts.startIndex) { - query.append('start-index', `${opts.startIndex}`); + if (opts?.startIndex !== undefined) { + query.append('start-index', String(opts.startIndex)); } - if (opts && opts.alertSources) { + if (opts?.alertSources !== undefined && Array.isArray(opts.alertSources)) { opts.alertSources.forEach((a: number) => { if (a) { - query.append('alert-source', `${a}`); + query.append('alert-source', String(a)); } }); } - if (opts && opts.states && Array.isArray(opts.states)) { + if (opts?.states !== undefined && Array.isArray(opts.states)) { opts.states.forEach(state => { query.append('state', state); }); @@ -118,10 +119,7 @@ export class ILertClient implements ILertApi { async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); if (opts && opts.states && Array.isArray(opts.states)) { @@ -137,14 +135,21 @@ export class ILertClient implements ILertApi { return response && response.count ? response.count : 0; } + async fetchIncident(id: number): Promise { + const init = { + headers: JSON_HEADERS, + }; + + const response = await this.fetch(`/api/v1/incidents/${id}`, init); + + return response; + } + async fetchIncidentResponders( incident: Incident, ): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -157,10 +162,7 @@ export class ILertClient implements ILertApi { async fetchIncidentActions(incident: Incident): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -171,40 +173,42 @@ export class ILertClient implements ILertApi { return response; } - async acceptIncident(incident: Incident): Promise { + async acceptIncident( + incident: Incident, + userName: string, + ): Promise { const init = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ + apiKey: incident.alertSource?.integrationKey || '', + incidentKey: incident.incidentKey, + summary: `from ${userName} via Backstage plugin`, + eventType: 'ACCEPT', + }), }; - const response = await this.fetch( - `/api/v1/incidents/${incident.id}/accept`, - init, - ); - - return response; + await this.fetch('/api/v1/events', init); + return this.fetchIncident(incident.id); } - async resolveIncident(incident: Incident): Promise { + async resolveIncident( + incident: Incident, + userName: string, + ): Promise { const init = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ + apiKey: incident.alertSource?.integrationKey || '', + incidentKey: incident.incidentKey, + summary: `from ${userName} via Backstage plugin`, + eventType: 'RESOLVE', + }), }; - const response = await this.fetch( - `/api/v1/incidents/${incident.id}/resolve`, - init, - ); - - return response; + await this.fetch('/api/v1/events', init); + return this.fetchIncident(incident.id); } async assignIncident( @@ -213,22 +217,19 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); switch (responder.group) { case 'ESCALATION_POLICY': - query.append('policy-id', `${responder.id}`); + query.append('policy-id', String(responder.id)); break; case 'ON_CALL_SCHEDULE': - query.append('schedule-id', `${responder.id}`); + query.append('schedule-id', String(responder.id)); break; default: - query.append('user-id', `${responder.id}`); + query.append('user-id', String(responder.id)); break; } @@ -246,10 +247,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ webhookId: action.webhookId, extensionId: action.extensionId, @@ -264,10 +262,7 @@ export class ILertClient implements ILertApi { async createIncident(eventRequest: EventRequest): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ apiKey: eventRequest.integrationKey, summary: eventRequest.summary, @@ -286,15 +281,12 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch('/api/v1/events', init); - return response.responseCode === 'NEW_INCIDENT_CREATED'; + return response; } async fetchUptimeMonitors(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/uptime-monitors', init); @@ -304,10 +296,7 @@ export class ILertClient implements ILertApi { async fetchUptimeMonitor(id: number): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response: UptimeMonitor = await this.fetch( @@ -323,10 +312,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...uptimeMonitor, paused: true }), }; @@ -343,10 +329,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...uptimeMonitor, paused: false }), }; @@ -360,10 +343,7 @@ export class ILertClient implements ILertApi { async fetchAlertSources(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/alert-sources', init); @@ -375,10 +355,7 @@ export class ILertClient implements ILertApi { idOrIntegrationKey: number | string, ): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -391,16 +368,13 @@ export class ILertClient implements ILertApi { async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( `/api/v1/on-calls?policies=${ alertSource.escalationPolicy.id - }&expand=user&expand=escalationPolicy&timezone=${momentTimezone.tz.guess()}`, + }&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, init, ); @@ -410,10 +384,7 @@ export class ILertClient implements ILertApi { async enableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...alertSource, active: true }), }; @@ -428,10 +399,7 @@ export class ILertClient implements ILertApi { async disableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...alertSource, active: false }), }; @@ -449,16 +417,13 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ - start: moment().utc().toISOString(), - end: moment().add(minutes, 'minutes').utc().toISOString(), + start: dt.utc().toISO(), + end: dt.utc().plus({ minutes }).toISO(), description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`, createdBy: 'Backstage', - timezone: momentTimezone.tz.guess(), + timezone: dt.local().zoneName, alertSources: [{ id: alertSourceId }], }), }; @@ -470,10 +435,7 @@ export class ILertClient implements ILertApi { async fetchOnCallSchedules(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/schedules', init); @@ -483,10 +445,7 @@ export class ILertClient implements ILertApi { async fetchUsers(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/users', init); @@ -502,10 +461,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ user: { id: userId }, start, end }), }; @@ -541,17 +497,7 @@ export class ILertClient implements ILertApi { } getUserPhoneNumber(user: User | null) { - if (!user) { - return ''; - } - if (user.mobile) { - return user.mobile.number; - } - - if (user.landline) { - return user.landline.number; - } - return ''; + return user?.mobile?.number || user?.landline?.number || ''; } getUserInitials(user: User | null) { diff --git a/plugins/ilert/src/api/index.ts b/plugins/ilert/src/api/index.ts index 128856ebdf..65c3571bff 100644 --- a/plugins/ilert/src/api/index.ts +++ b/plugins/ilert/src/api/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { ILertClient, ilertApiRef, UnauthorizedError } from './client'; +export { ILertClient, ilertApiRef } from './client'; export type { ILertApi, GetIncidentsCountOpts, diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index 84b4ecf654..ab9cca3dc6 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -54,10 +54,11 @@ export type EventRequest = { export interface ILertApi { fetchIncidents(opts?: GetIncidentsOpts): Promise; fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + fetchIncident(id: number): Promise; fetchIncidentResponders(incident: Incident): Promise; fetchIncidentActions(incident: Incident): Promise; - acceptIncident(incident: Incident): Promise; - resolveIncident(incident: Incident): Promise; + acceptIncident(incident: Incident, userName: string): Promise; + resolveIncident(incident: Incident, userName: string): Promise; assignIncident( incident: Incident, responder: IncidentResponder, @@ -115,5 +116,5 @@ export type Options = { /** * Path to use for requests via the proxy, defaults to /ilert/api */ - proxyPath?: string; + proxyPath: string; }; diff --git a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx index d94a9b0039..f7f6dbb78c 100644 --- a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx +++ b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import Grid from '@material-ui/core/Grid'; import { AlertSource } from '../../types'; import { ilertApiRef } from '../../api'; @@ -61,7 +60,7 @@ export const AlertSourceLink = ({ {alertSource.name} diff --git a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx index 7f1e3524ff..9e7ff102f8 100644 --- a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx +++ b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { EscalationPolicy } from '../../types'; import { ilertApiRef } from '../../api'; @@ -41,7 +40,7 @@ export const EscalationPolicyLink = ({ return ( {escalationPolicy.name} diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index dd4bdde3cf..bbc82c2b2d 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -15,14 +15,14 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { AuthenticationError } from '@backstage/errors'; +import { ResponseErrorPanel } from '@backstage/core'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Divider from '@material-ui/core/Divider'; import { makeStyles } from '@material-ui/core/styles'; -import { ILERT_INTEGRATION_KEY } from '../../constants'; +import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../../constants'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useIncidents } from '../../hooks/useIncidents'; import { IncidentsTable } from '../IncidentsPage'; @@ -36,7 +36,7 @@ import { ILertCardEmptyState } from './ILertCardEmptyState'; import { ILertCardOnCall } from './ILertCardOnCall'; export const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]); + Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION]); const useStyles = makeStyles({ content: { @@ -79,15 +79,11 @@ export const ILertCard = () => { ] = React.useState(false); if (error) { - if (error instanceof UnauthorizedError) { + if (error instanceof AuthenticationError) { return ; } - return ( - - {error.message} - - ); + return ; } if (!integrationKey) { diff --git a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx index 42dc140696..955d514564 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx @@ -40,14 +40,12 @@ spec: const useStyles = makeStyles(theme => ({ code: { borderRadius: 6, - margin: `${theme.spacing(2)}px 0px`, + margin: theme.spacing(2, 0), background: theme.palette.type === 'dark' ? '#444' : '#fff', }, header: { display: 'inline-block', - padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing( - 2, - )}px ${theme.spacing(2.5)}px`, + padding: theme.spacing(2, 2, 2, 2.5), }, })); diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx index 69ceb0ad55..994666084b 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx @@ -29,7 +29,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { OnCall } from '../../types'; import { useApi } from '@backstage/core'; import { ilertApiRef } from '../../api'; -import moment from 'moment'; +import { DateTime as dt } from 'luxon'; const useStyles = makeStyles({ listItemPrimary: { @@ -123,8 +123,8 @@ export const ILertCardOnCallItem = ({ diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index 0a1ae95ded..5b06cc4eb6 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -14,9 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, Progress, useApi } from '@backstage/core'; +import { + alertApiRef, + Progress, + useApi, + identityApiRef, + Link, +} from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; -import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; import { Incident, IncidentAction } from '../../types'; @@ -34,6 +39,8 @@ export const IncidentActionsMenu = ({ }) => { const ilertApi = useApi(ilertApiRef); const alertApi = useApi(alertApiRef); + const identityApi = useApi(identityApiRef); + const userName = identityApi.getUserId(); const [anchorEl, setAnchorEl] = React.useState(null); const callback = onIncidentChanged || ((_: Incident): void => {}); const setProcessing = setIsLoading || ((_: boolean): void => {}); @@ -59,7 +66,7 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); - const newIncident = await ilertApi.acceptIncident(incident); + const newIncident = await ilertApi.acceptIncident(incident, userName); alertApi.post({ message: 'Incident accepted.' }); callback(newIncident); @@ -74,7 +81,7 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); - const newIncident = await ilertApi.resolveIncident(incident); + const newIncident = await ilertApi.resolveIncident(incident, userName); alertApi.post({ message: 'Incident resolved.' }); callback(newIncident); @@ -179,7 +186,7 @@ export const IncidentActionsMenu = ({ - + View in iLert diff --git a/plugins/ilert/src/components/Incident/IncidentLink.tsx b/plugins/ilert/src/components/Incident/IncidentLink.tsx index 87818f363a..7bfe08b7a9 100644 --- a/plugins/ilert/src/components/Incident/IncidentLink.tsx +++ b/plugins/ilert/src/components/Incident/IncidentLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { Incident } from '../../types'; import { ilertApiRef } from '../../api'; @@ -37,7 +36,7 @@ export const IncidentLink = ({ incident }: { incident: Incident | null }) => { return ( #{incident.id} diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx index 3fae62bf0c..8ff33e6319 100644 --- a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -98,17 +98,15 @@ export const IncidentNewModal = ({ setIsLoading(true); setTimeout(async () => { try { - const success = await ilertApi.createIncident({ + await ilertApi.createIncident({ integrationKey, summary, details, userName, source, }); - if (success) { - alertApi.post({ message: 'Incident created.' }); - refetchIncidents(); - } + alertApi.post({ message: 'Incident created.' }); + refetchIncidents(); } catch (err) { alertApi.post({ message: err, severity: 'error' }); } diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx index e552ce7d26..782848645e 100644 --- a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx @@ -14,11 +14,15 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import Button from '@material-ui/core/Button'; import AddIcon from '@material-ui/icons/Add'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; import { IncidentsTable } from './IncidentsTable'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useIncidents } from '../../hooks/useIncidents'; @@ -44,14 +48,18 @@ export const IncidentsPage = () => { }; if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx index b7ee298062..087b2bfd40 100644 --- a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx @@ -22,7 +22,8 @@ import { StatusChip } from './StatusChip'; import { AlertSourceLink } from '../AlertSource/AlertSourceLink'; import { TableTitle } from './TableTitle'; import Typography from '@material-ui/core/Typography'; -import moment from 'moment'; +import { DateTime as dt, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu'; import { IncidentLink } from '../Incident/IncidentLink'; @@ -116,16 +117,24 @@ export const IncidentsTable = ({ render: rowData => ( {(rowData as Incident).status !== 'RESOLVED' - ? moment - .duration(moment((rowData as Incident).reportTime).diff(moment())) - .humanize() - : moment - .duration( - moment((rowData as Incident).reportTime).diff( - moment((rowData as Incident).resolvedOn), - ), + ? humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as Incident).reportTime), + dt.now(), ) - .humanize()} + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + ) + : humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as Incident).reportTime), + dt.fromISO((rowData as Incident).resolvedOn), + ) + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + )} ), }; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx index c84e0a7b8d..31da9c1097 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -14,13 +14,12 @@ * limitations under the License. */ import React from 'react'; -import { useApi, ItemCardGrid, Progress } from '@backstage/core'; +import { useApi, ItemCardGrid, Progress, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; import { Schedule } from '../../types'; import { ilertApiRef } from '../../api'; import { OnCallShiftItem } from './OnCallShiftItem'; @@ -132,7 +131,7 @@ export const OnCallSchedulesGrid = ({ classes={{ content: classes.cardHeader }} title={ {schedule.name} diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx index 09c2f2fc63..bb0cd0218e 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx @@ -14,9 +14,13 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { OnCallSchedulesGrid } from './OnCallSchedulesGrid'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useOnCallSchedules } from '../../hooks/useOnCallSchedules'; @@ -28,14 +32,18 @@ export const OnCallSchedulesPage = () => { ] = useOnCallSchedules(); if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx index c636609781..46f6963445 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx @@ -19,7 +19,7 @@ import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import RepeatIcon from '@material-ui/icons/Repeat'; import { Shift } from '../../types'; -import moment from 'moment'; +import { DateTime as dt } from 'luxon'; import { makeStyles } from '@material-ui/core/styles'; import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal'; @@ -77,9 +77,9 @@ export const OnCallShiftItem = ({ ) : null} - {`${moment(shift.start).format('D MMM, HH:mm')} - ${moment( - shift.end, - ).format('D MMM, HH:mm')}`} + {`${dt.fromISO(shift.start).toFormat('D MMM, HH:mm')} - ${dt + .fromISO(shift.end) + .toFormat('D MMM, HH:mm')}`} diff --git a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx index 1fdd05cb4c..47e13f9b7e 100644 --- a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx +++ b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx @@ -28,7 +28,7 @@ import { ilertApiRef } from '../../api'; import { useShiftOverride } from '../../hooks/useShiftOverride'; import { Shift } from '../../types'; import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; -import DateFnsUtils from '@date-io/date-fns'; +import LuxonUtils from '@date-io/luxon'; const useStyles = makeStyles(() => ({ container: { @@ -120,7 +120,7 @@ export const ShiftOverrideModal = ({ > Shift override - + { - setStart(date ? date.toISOString() : ''); + setStart(date ? date.toISO() : ''); }} /> { - setEnd(date ? date.toISOString() : ''); + setEnd(date ? date.toISO() : ''); }} /> diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx index efd69a4acf..36671bff90 100644 --- a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, useApi } from '@backstage/core'; +import { alertApiRef, useApi, Link } from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; -import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; @@ -117,13 +116,15 @@ export const UptimeMonitorActionsMenu = ({ - View Report + + View Report + - + View in iLert diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx index 0af5cd89df..507e0208e2 100644 --- a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { UptimeMonitor } from '../../types'; import { ilertApiRef } from '../../api'; @@ -41,7 +40,7 @@ export const UptimeMonitorLink = ({ return ( #{uptimeMonitor.id} diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx index dc7ea77a6c..9769fa408d 100644 --- a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx @@ -14,9 +14,13 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { UptimeMonitorsTable } from './UptimeMonitorsTable'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useUptimeMonitors } from '../../hooks/useUptimeMonitors'; @@ -28,14 +32,18 @@ export const UptimeMonitorsPage = () => { ] = useUptimeMonitors(); if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx index bd55310ad7..2173f8e1b7 100644 --- a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx @@ -20,7 +20,8 @@ import { TableState } from '../../api'; import { UptimeMonitor } from '../../types'; import { StatusChip } from './StatusChip'; import Typography from '@material-ui/core/Typography'; -import moment from 'moment'; +import { DateTime as dt, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink'; import { UptimeMonitorCheckType } from './UptimeMonitorCheckType'; import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu'; @@ -99,13 +100,15 @@ export const UptimeMonitorsTable = ({ headerStyle: mdColumnStyle, render: rowData => ( - {moment - .duration( - moment((rowData as UptimeMonitor).lastStatusChange).diff( - moment(), - ), + {humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as UptimeMonitor).lastStatusChange), + dt.now(), ) - .humanize()} + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + )} ), }, diff --git a/plugins/ilert/src/hooks/index.ts b/plugins/ilert/src/hooks/index.ts index e21c9584d2..6e7e207112 100644 --- a/plugins/ilert/src/hooks/index.ts +++ b/plugins/ilert/src/hooks/index.ts @@ -16,13 +16,16 @@ import { useEntity } from '@backstage/plugin-catalog-react'; -import { ILERT_INTEGRATION_KEY } from '../constants'; +import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../constants'; export function useILertEntity() { const { entity } = useEntity(); const integrationKey = - entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || ''; + entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION] || ''; const name = entity.metadata.name; + const identifier = `${entity.kind}:${ + entity.metadata.namespace || 'default' + }/${entity.metadata.name}`; - return { integrationKey, name }; + return { integrationKey, name, identifier }; } diff --git a/plugins/ilert/src/hooks/useAlertSource.ts b/plugins/ilert/src/hooks/useAlertSource.ts index 950f8f41c6..15cc213b48 100644 --- a/plugins/ilert/src/hooks/useAlertSource.ts +++ b/plugins/ilert/src/hooks/useAlertSource.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource, UptimeMonitor } from '../types'; @@ -46,7 +47,7 @@ export const useAlertSource = (integrationKey: string) => { setIsAlertSourceLoading(false); } catch (e) { setIsAlertSourceLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; @@ -69,7 +70,7 @@ export const useAlertSource = (integrationKey: string) => { setIsUptimeMonitorLoading(false); } catch (e) { setIsUptimeMonitorLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts index ac36c70097..0fbb190a02 100644 --- a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts +++ b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource, OnCall } from '../types'; @@ -37,7 +38,7 @@ export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useAssignIncident.ts b/plugins/ilert/src/hooks/useAssignIncident.ts index 1671acdc51..8f0f663d2b 100644 --- a/plugins/ilert/src/hooks/useAssignIncident.ts +++ b/plugins/ilert/src/hooks/useAssignIncident.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Incident, IncidentResponder } from '../types'; @@ -49,7 +50,7 @@ export const useAssignIncident = (incident: Incident | null, open: boolean) => { setIncidentRespondersList(data); } } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useIncidentActions.ts b/plugins/ilert/src/hooks/useIncidentActions.ts index d8dd08e17a..e23341ed9e 100644 --- a/plugins/ilert/src/hooks/useIncidentActions.ts +++ b/plugins/ilert/src/hooks/useIncidentActions.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Incident, IncidentAction } from '../types'; @@ -39,7 +40,7 @@ export const useIncidentActions = ( const data = await ilertApi.fetchIncidentActions(incident); setIncidentActionsList(data); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts index 2f3d12cf17..5f130fb20c 100644 --- a/plugins/ilert/src/hooks/useIncidents.ts +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -14,13 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { - GetIncidentsOpts, - ilertApiRef, - TableState, - UnauthorizedError, -} from '../api'; +import { GetIncidentsOpts, ilertApiRef, TableState } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { ACCEPTED, @@ -68,7 +64,7 @@ export const useIncidents = ( setIncidentsList(data || []); setIsLoading(false); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } setIsLoading(false); @@ -81,7 +77,7 @@ export const useIncidents = ( const count = await ilertApi.fetchIncidentsCount({ states }); setIncidentsCount(count || 0); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useNewIncident.ts b/plugins/ilert/src/hooks/useNewIncident.ts index 669fa74b63..6fbcfbb5a5 100644 --- a/plugins/ilert/src/hooks/useNewIncident.ts +++ b/plugins/ilert/src/hooks/useNewIncident.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource } from '../types'; @@ -44,7 +45,7 @@ export const useNewIncident = ( const count = await ilertApi.fetchAlertSources(); setAlertSourcesList(count || 0); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useOnCallSchedules.ts b/plugins/ilert/src/hooks/useOnCallSchedules.ts index 5e0538a370..ce76a0770c 100644 --- a/plugins/ilert/src/hooks/useOnCallSchedules.ts +++ b/plugins/ilert/src/hooks/useOnCallSchedules.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Schedule } from '../types'; @@ -36,7 +37,7 @@ export const useOnCallSchedules = () => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useShiftOverride.ts b/plugins/ilert/src/hooks/useShiftOverride.ts index fd1da7423b..136d20d286 100644 --- a/plugins/ilert/src/hooks/useShiftOverride.ts +++ b/plugins/ilert/src/hooks/useShiftOverride.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { User, Shift } from '../types'; @@ -38,7 +39,7 @@ export const useShiftOverride = (s: Shift, isModalOpened: boolean) => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useUptimeMonitors.ts b/plugins/ilert/src/hooks/useUptimeMonitors.ts index 02cd5aaa2f..fa1576642a 100644 --- a/plugins/ilert/src/hooks/useUptimeMonitors.ts +++ b/plugins/ilert/src/hooks/useUptimeMonitors.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, TableState, UnauthorizedError } from '../api'; +import { ilertApiRef, TableState } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { UptimeMonitor } from '../types'; @@ -40,7 +41,7 @@ export const useUptimeMonitors = () => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; From 6df7f02dba8e8328731ba8910a02f27a0827606a Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 7 May 2021 15:30:46 +0200 Subject: [PATCH 15/17] add encode to api urls for the iLert plugin Signed-off-by: yacut --- plugins/ilert/src/api/client.ts | 58 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index e635d76b99..ed1e671bc0 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -140,7 +140,10 @@ export class ILertClient implements ILertApi { headers: JSON_HEADERS, }; - const response = await this.fetch(`/api/v1/incidents/${id}`, init); + const response = await this.fetch( + `/api/v1/incidents/${encodeURIComponent(id)}`, + init, + ); return response; } @@ -153,7 +156,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/incidents/${incident.id}/responders`, + `/api/v1/incidents/${encodeURIComponent(incident.id)}/responders`, init, ); @@ -166,7 +169,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/incidents/${incident.id}/actions`, + `/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`, init, ); @@ -234,7 +237,9 @@ export class ILertClient implements ILertApi { } const response = await this.fetch( - `/api/v1/incidents/${incident.id}/assign?${query.toString()}`, + `/api/v1/incidents/${encodeURIComponent( + incident.id, + )}/assign?${query.toString()}`, init, ); @@ -256,7 +261,10 @@ export class ILertClient implements ILertApi { }), }; - await this.fetch(`/api/v1/incidents/${incident.id}/actions`, init); + await this.fetch( + `/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`, + init, + ); } async createIncident(eventRequest: EventRequest): Promise { @@ -300,7 +308,7 @@ export class ILertClient implements ILertApi { }; const response: UptimeMonitor = await this.fetch( - `/api/v1/uptime-monitors/${id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(id)}`, init, ); @@ -317,7 +325,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`, init, ); @@ -334,7 +342,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`, init, ); @@ -359,7 +367,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${idOrIntegrationKey}`, + `/api/v1/alert-sources/${encodeURIComponent(idOrIntegrationKey)}`, init, ); @@ -372,9 +380,9 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/on-calls?policies=${ - alertSource.escalationPolicy.id - }&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, + `/api/v1/on-calls?policies=${encodeURIComponent( + alertSource.escalationPolicy.id, + )}&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, init, ); @@ -389,7 +397,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${alertSource.id}`, + `/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`, init, ); @@ -404,7 +412,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${alertSource.id}`, + `/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`, init, ); @@ -466,7 +474,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/schedules/${scheduleId}/overrides`, + `/api/v1/schedules/${encodeURIComponent(scheduleId)}/overrides`, init, ); @@ -474,26 +482,36 @@ export class ILertClient implements ILertApi { } getIncidentDetailsURL(incident: Incident): string { - return `${this.baseUrl}/incident/view.jsf?id=${incident.id}`; + 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=${alertSource.id}`; + return `${this.baseUrl}/source/view.jsf?id=${encodeURIComponent( + alertSource.id, + )}`; } getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { - return `${this.baseUrl}/policy/view.jsf?id=${escalationPolicy.id}`; + return `${this.baseUrl}/policy/view.jsf?id=${encodeURIComponent( + escalationPolicy.id, + )}`; } getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { - return `${this.baseUrl}/uptime/view.jsf?id=${uptimeMonitor.id}`; + return `${this.baseUrl}/uptime/view.jsf?id=${encodeURIComponent( + uptimeMonitor.id, + )}`; } getScheduleDetailsURL(schedule: Schedule): string { - return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; + return `${this.baseUrl}/schedule/view.jsf?id=${encodeURIComponent( + schedule.id, + )}`; } getUserPhoneNumber(user: User | null) { From c6cc32b64eca164bffe04e1536940f6535cff6ad Mon Sep 17 00:00:00 2001 From: yacut Date: Thu, 13 May 2021 20:24:22 +0200 Subject: [PATCH 16/17] fix config schema for ilert Signed-off-by: yacut --- plugins/ilert/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/config.d.ts b/plugins/ilert/config.d.ts index 52712454e8..6bf3f569ab 100644 --- a/plugins/ilert/config.d.ts +++ b/plugins/ilert/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export interface Config { - ilert: { + ilert?: { /** * Domain used by users to access iLert web UI. * Example: https://my-app.ilert.com/ From fadb6c9dfa348f5c9563f1c43a07e10b5901d444 Mon Sep 17 00:00:00 2001 From: yacut Date: Thu, 13 May 2021 21:38:32 +0200 Subject: [PATCH 17/17] upgrade dependencies for ilert package Signed-off-by: yacut --- plugins/ilert/package.json | 14 +++++++------- yarn.lock | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 31eff40dee..24376e03a8 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.7", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.4", - "@backstage/theme": "^0.2.6", + "@backstage/theme": "^0.2.7", "@date-io/luxon": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,15 +34,15 @@ "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.9", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@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": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 330463a73a..76856b26b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"