From 7fdb8eb4fc3c07f57bc9403450062969f6cb1be7 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Wed, 25 May 2022 11:30:00 -0700 Subject: [PATCH 01/39] feat(plugins/pagerduty): add constant for service id annotation annotation: 'pagerduty.com/service-id' Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/components/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/pagerduty/src/components/constants.ts b/plugins/pagerduty/src/components/constants.ts index bfe1aea298..01308590f1 100644 --- a/plugins/pagerduty/src/components/constants.ts +++ b/plugins/pagerduty/src/components/constants.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; +export const PAGERDUTY_SERVICE_ID = 'pagerduty.com/service-id'; From 0e8fc434923321248bcd4ee09c3c90119f997c04 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Wed, 25 May 2022 15:49:45 -0700 Subject: [PATCH 02/39] feat(plugins/pagerduty): add serviceId to pager duty entity Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/hooks/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/pagerduty/src/hooks/index.ts b/plugins/pagerduty/src/hooks/index.ts index 40a34788f0..a5fa462e02 100644 --- a/plugins/pagerduty/src/hooks/index.ts +++ b/plugins/pagerduty/src/hooks/index.ts @@ -16,13 +16,18 @@ import { useEntity } from '@backstage/plugin-catalog-react'; -import { PAGERDUTY_INTEGRATION_KEY } from '../components/constants'; +import { + PAGERDUTY_INTEGRATION_KEY, + PAGERDUTY_SERVICE_ID, +} from '../components/constants'; export function usePagerdutyEntity() { const { entity } = useEntity(); - const integrationKey = - entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]; + const { + [PAGERDUTY_INTEGRATION_KEY]: integrationKey, + [PAGERDUTY_SERVICE_ID]: serviceId, + } = entity.metadata.annotations || ({} as Record); const name = entity.metadata.name; - return { integrationKey, name }; + return { integrationKey, serviceId, name }; } From 7959a35cc14d1954ee06fa43806dd4e3b6c5d168 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Wed, 25 May 2022 15:52:34 -0700 Subject: [PATCH 03/39] feat(plugins/pagerduty): check for service id annotation for applicable entities Signed-off-by: Alec Jacobs --- .../components/PagerDutyCard/index.test.tsx | 65 ++++++++++++++++++- .../src/components/PagerDutyCard/index.tsx | 7 +- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index e0a857ccc7..1195d6e7ee 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { render, waitFor, fireEvent, act } from '@testing-library/react'; -import { PagerDutyCard } from '../PagerDutyCard'; +import { PagerDutyCard, isPluginApplicableToEntity } from '../PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; @@ -35,6 +35,7 @@ const apis = TestApiRegistry.from( [pagerDutyApiRef, mockPagerDutyApi], [alertApiRef, {}], ); + const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -46,6 +47,38 @@ const entity: Entity = { }, }; +const entityWithoutAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: {}, + }, +}; + +const entityWithServiceId: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + +const entityWithAllAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + const user: User = { name: 'person1', id: 'p1', @@ -55,7 +88,7 @@ const user: User = { }; const service: Service = { - id: 'abc', + id: 'def456', name: 'pagerduty-name', html_url: 'www.example.com', escalation_policy: { @@ -63,9 +96,35 @@ const service: Service = { user: user, html_url: 'http://a.com/id1', }, - integrationKey: 'abcd', + integrationKey: 'abc123', }; +describe('isPluginApplicableToEntity', () => { + describe('when entity has no annotations', () => { + it('returns false', () => { + expect(isPluginApplicableToEntity(entityWithoutAnnotations)).toBe(false); + }); + }); + + describe('when entity has the pagerduty.com/integration-key annotation', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entity)).toBe(true); + }); + }); + + describe('when entity has the pagerduty.com/service-id annotation', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entityWithServiceId)).toBe(true); + }); + }); + + describe('when entity has all annotations', () => { + it('returns true', () => { + expect(isPluginApplicableToEntity(entityWithAllAnnotations)).toBe(true); + }); + }); +}); + describe('PageDutyCard', () => { it('Render pagerduty', async () => { mockPagerDutyApi.getServiceByIntegrationKey = jest diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index bbade09e51..0938f7288b 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -26,7 +26,7 @@ import { MissingTokenError } from '../Errors/MissingTokenError'; import WebIcon from '@material-ui/icons/Web'; import DateRangeIcon from '@material-ui/icons/DateRange'; import { usePagerdutyEntity } from '../../hooks'; -import { PAGERDUTY_INTEGRATION_KEY } from '../constants'; +import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; import { TriggerDialog } from '../TriggerDialog'; import { ChangeEvents } from '../ChangeEvents'; @@ -40,7 +40,10 @@ import { } from '@backstage/core-components'; export const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); + Boolean( + entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] || + entity.metadata.annotations?.[PAGERDUTY_SERVICE_ID], + ); export const PagerDutyCard = () => { const { integrationKey } = usePagerdutyEntity(); From e5af087fb5917bd58041557a5e7f1a49cbac1395 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Thu, 26 May 2022 10:19:12 -0700 Subject: [PATCH 04/39] feat(plugins/pagerduty): add getServiceByServiceId to PagerDutyClient * extract common params to helper constant Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 16 +++++++++++++++- plugins/pagerduty/src/api/types.ts | 10 ++++++++++ .../src/components/PagerDutyCard/index.test.tsx | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 43c6165c91..db883e8e35 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -19,6 +19,7 @@ import { PagerDutyApi, TriggerAlarmRequest, ServicesResponse, + ServiceResponse, IncidentsResponse, OnCallsResponse, ClientApiConfig, @@ -38,6 +39,9 @@ export const pagerDutyApiRef = createApiRef({ id: 'plugin.pagerduty.api', }); +const commonGetServiceParams = + 'time_zone=UTC&include[]=integrations&include[]=escalation_policies'; + export class PagerDutyClient implements PagerDutyApi { static fromConfig( configApi: ConfigApi, @@ -56,7 +60,7 @@ export class PagerDutyClient implements PagerDutyApi { constructor(private readonly config: ClientApiConfig) {} async getServiceByIntegrationKey(integrationKey: string): Promise { - const params = `time_zone=UTC&include[]=integrations&include[]=escalation_policies&query=${integrationKey}`; + const params = `${commonGetServiceParams}&query=${integrationKey}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/services?${params}`; @@ -65,6 +69,16 @@ export class PagerDutyClient implements PagerDutyApi { return services; } + async getServiceByServiceId(serviceId: string): Promise { + const params = commonGetServiceParams; + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/services/${serviceId}?${params}`; + const { service } = await this.getByUrl(url); + + return service; + } + async getIncidentsByServiceId(serviceId: string): Promise { const params = `time_zone=UTC&sort_by=created_at&statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`; const url = `${await this.config.discoveryApi.getBaseUrl( diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 8acd24f033..e473529980 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -31,6 +31,12 @@ export interface PagerDutyApi { */ getServiceByIntegrationKey(integrationKey: string): Promise; + /** + * Fetches the service for the provided service id. + * + */ + getServiceByServiceId(serviceId: string): Promise; + /** * Fetches a list of incidents a provided service has. * @@ -59,6 +65,10 @@ export type ServicesResponse = { services: Service[]; }; +export type ServiceResponse = { + service: Service; +}; + export type IncidentsResponse = { incidents: Incident[]; }; diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 1195d6e7ee..e633dde0c8 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -27,6 +27,7 @@ import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi: Partial = { getServiceByIntegrationKey: async () => [], + getServiceByServiceId: async () => service, getOnCallByPolicyId: async () => [], getIncidentsByServiceId: async () => [], }; From ff30bc3f061007200b0716d8e087da7785cf7343 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Thu, 26 May 2022 14:57:09 -0700 Subject: [PATCH 05/39] fix(plugins/pagerduty): use FetchApi over raw fetch with IdentityApi Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 10 ++++------ plugins/pagerduty/src/api/types.ts | 4 ++-- plugins/pagerduty/src/plugin.ts | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index db883e8e35..78d1c048f2 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -30,7 +30,7 @@ import { createApiRef, DiscoveryApi, ConfigApi, - IdentityApi, + FetchApi, } from '@backstage/core-plugin-api'; export class UnauthorizedError extends Error {} @@ -46,7 +46,7 @@ export class PagerDutyClient implements PagerDutyApi { static fromConfig( configApi: ConfigApi, discoveryApi: DiscoveryApi, - identityApi: IdentityApi, + fetchApi: FetchApi, ) { const eventsBaseUrl: string = configApi.getOptionalString('pagerDuty.eventsBaseUrl') ?? @@ -54,7 +54,7 @@ export class PagerDutyClient implements PagerDutyApi { return new PagerDutyClient({ eventsBaseUrl, discoveryApi, - identityApi, + fetchApi, }); } constructor(private readonly config: ClientApiConfig) {} @@ -144,13 +144,11 @@ export class PagerDutyClient implements PagerDutyApi { } private async getByUrl(url: string): Promise { - const { token: idToken } = await this.config.identityApi.getCredentials(); const options = { method: 'GET', headers: { Accept: 'application/vnd.pagerduty+json;version=2', 'Content-Type': 'application/json', - ...(idToken && { Authorization: `Bearer ${idToken}` }), }, }; const response = await this.request(url, options); @@ -161,7 +159,7 @@ export class PagerDutyClient implements PagerDutyApi { url: string, options: RequestOptions, ): Promise { - const response = await fetch(url, options); + const response = await this.config.fetchApi.fetch(url, options); if (response.status === 401) { throw new UnauthorizedError(); } diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index e473529980..e747fd40fe 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -15,7 +15,7 @@ */ import { Incident, ChangeEvent, OnCall, Service } from '../components/types'; -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; export type TriggerAlarmRequest = { integrationKey: string; @@ -84,7 +84,7 @@ export type OnCallsResponse = { export type ClientApiConfig = { eventsBaseUrl?: string; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; + fetchApi: FetchApi; }; export type RequestOptions = { diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 32093015ff..3baa36a50e 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -19,9 +19,9 @@ import { createPlugin, createRouteRef, discoveryApiRef, + fetchApiRef, configApiRef, createComponentExtension, - identityApiRef, } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ @@ -36,10 +36,10 @@ export const pagerDutyPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, configApi: configApiRef, - identityApi: identityApiRef, + fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi }) => - PagerDutyClient.fromConfig(configApi, discoveryApi, identityApi), + factory: ({ configApi, discoveryApi, fetchApi }) => + PagerDutyClient.fromConfig(configApi, discoveryApi, fetchApi), }), ], }); From 525641fc02aea84e0772f8bb8e7c149eecfcb483 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Thu, 26 May 2022 14:58:31 -0700 Subject: [PATCH 06/39] feat(plugins/pagerduty): add ServiceNotFoundError empty state Signed-off-by: Alec Jacobs --- .../Errors/ServiceNotFoundError.tsx | 35 +++++++++++++++++++ .../pagerduty/src/components/Errors/index.ts | 1 + 2 files changed, 36 insertions(+) create mode 100644 plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx diff --git a/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx b/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx new file mode 100644 index 0000000000..d8b61ef7cd --- /dev/null +++ b/plugins/pagerduty/src/components/Errors/ServiceNotFoundError.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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'; +import { EmptyState } from '@backstage/core-components'; + +export const ServiceNotFoundError = () => ( + + Read More + + } + /> +); diff --git a/plugins/pagerduty/src/components/Errors/index.ts b/plugins/pagerduty/src/components/Errors/index.ts index df255749e0..6c35e0bfd1 100644 --- a/plugins/pagerduty/src/components/Errors/index.ts +++ b/plugins/pagerduty/src/components/Errors/index.ts @@ -15,3 +15,4 @@ */ export { MissingTokenError } from './MissingTokenError'; +export { ServiceNotFoundError } from './ServiceNotFoundError'; From 845c7f88523c4f2f2c89e70da7ac14c7a370b2c4 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 10:14:20 -0700 Subject: [PATCH 07/39] feat(plugins/pagerduty): add NotFoundError Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 1 + plugins/pagerduty/src/api/index.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 78d1c048f2..ddf4248934 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -34,6 +34,7 @@ import { } from '@backstage/core-plugin-api'; export class UnauthorizedError extends Error {} +export class NotFoundError extends Error {} export const pagerDutyApiRef = createApiRef({ id: 'plugin.pagerduty.api', diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts index 015204b1e8..e23ee0c9a4 100644 --- a/plugins/pagerduty/src/api/index.ts +++ b/plugins/pagerduty/src/api/index.ts @@ -14,5 +14,10 @@ * limitations under the License. */ -export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client'; +export { + PagerDutyClient, + pagerDutyApiRef, + UnauthorizedError, + NotFoundError, +} from './client'; export type { PagerDutyApi } from './types'; From 3cf02b6e9c968e8cb598a92d058b11a93b97e616 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 10:22:01 -0700 Subject: [PATCH 08/39] feat(plugins/pagerduty): handle 404 error code in Api Client * raise NotFoundError Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index ddf4248934..6c31f2a5d0 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -164,6 +164,11 @@ export class PagerDutyClient implements PagerDutyApi { if (response.status === 401) { throw new UnauthorizedError(); } + + if (response.status === 404) { + throw new NotFoundError(); + } + if (!response.ok) { const payload = await response.json(); const errors = payload.errors.map((error: string) => error).join(' '); From bb4fb80dac1d4c29728a2cc52112181c7f414495 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 10:39:06 -0700 Subject: [PATCH 09/39] feat(plugins/pagerduty): add wrapper BasicCard component * ensures errors and progress bar have consistent styling with the rest of the Overview Page components Signed-off-by: Alec Jacobs --- .../src/components/PagerDutyCard/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 0938f7288b..e5c7eab071 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState, useCallback } from 'react'; +import React, { ReactNode, useState, useCallback } from 'react'; import { Entity } from '@backstage/catalog-model'; import { Card, CardHeader, Divider, CardContent } from '@material-ui/core'; import { Incidents } from '../Incident'; @@ -37,8 +37,13 @@ import { IconLinkVerticalProps, TabbedCard, CardTab, + InfoCard, } from '@backstage/core-components'; +const BasicCard = ({ children }: { children: ReactNode }) => ( + {children} +); + export const isPluginApplicableToEntity = (entity: Entity) => Boolean( entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY] || @@ -96,7 +101,11 @@ export const PagerDutyCard = () => { } if (loading) { - return ; + return ( + + + + ); } const serviceLink: IconLinkVerticalProps = { From 02c2978b79d8f09873fa4bfb3b806080edf772dc Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 10:43:28 -0700 Subject: [PATCH 10/39] feat(plugins/pagerduty): handle empty response when querying services * raise NotFoundError in async hook * show ServiceNotFoundError empty state when async hook errors Signed-off-by: Alec Jacobs --- .../components/PagerDutyCard/index.test.tsx | 44 +++++++++++++++++- .../src/components/PagerDutyCard/index.tsx | 46 ++++++++++++------- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index e633dde0c8..2b309533ed 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -19,7 +19,12 @@ import { PagerDutyCard, isPluginApplicableToEntity } from '../PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; -import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; +import { + pagerDutyApiRef, + UnauthorizedError, + NotFoundError, + PagerDutyClient, +} from '../../api'; import { Service, User } from '../types'; import { alertApiRef } from '@backstage/core-plugin-api'; @@ -166,6 +171,24 @@ describe('PageDutyCard', () => { expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument(); }); + it('Handles custom NotFoundError', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); + }); + it('handles general error', async () => { mockPagerDutyApi.getServiceByIntegrationKey = jest .fn() @@ -187,6 +210,25 @@ describe('PageDutyCard', () => { ), ).toBeInTheDocument(); }); + + it('handles empty response from getServiceByIntegrationKey', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); + }); + it('opens the dialog when trigger button is clicked', async () => { mockPagerDutyApi.getServiceByIntegrationKey = jest .fn() diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index e5c7eab071..b4b20fb25b 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -20,15 +20,16 @@ import { Incidents } from '../Incident'; import { EscalationPolicy } from '../Escalation'; import useAsync from 'react-use/lib/useAsync'; import { Alert } from '@material-ui/lab'; -import { pagerDutyApiRef, UnauthorizedError } from '../../api'; +import { pagerDutyApiRef, UnauthorizedError, NotFoundError } from '../../api'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; -import { MissingTokenError } from '../Errors/MissingTokenError'; +import { MissingTokenError, ServiceNotFoundError } from '../Errors'; import WebIcon from '@material-ui/icons/Web'; import DateRangeIcon from '@material-ui/icons/DateRange'; import { usePagerdutyEntity } from '../../hooks'; import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; import { TriggerDialog } from '../TriggerDialog'; import { ChangeEvents } from '../ChangeEvents'; +import { Service } from '../types'; import { useApi } from '@backstage/core-plugin-api'; import { @@ -75,29 +76,42 @@ export const PagerDutyCard = () => { loading, error, } = useAsync(async () => { + let service: Service; const services = await api.getServiceByIntegrationKey( integrationKey as string, ); + service = services[0]; + if (!service) throw new NotFoundError(); + return { - id: services[0].id, - name: services[0].name, - url: services[0].html_url, - policyId: services[0].escalation_policy.id, - policyLink: services[0].escalation_policy.html_url, + id: service.id, + name: service.name, + url: service.html_url, + policyId: service.escalation_policy.id, + policyLink: service.escalation_policy.html_url, }; }); - if (error instanceof UnauthorizedError) { - return ; - } - if (error) { - return ( - - Error encountered while fetching information. {error.message} - - ); + let errorNode: ReactNode; + + switch (error.constructor) { + case UnauthorizedError: + errorNode = ; + break; + case NotFoundError: + errorNode = ; + break; + default: + errorNode = ( + + Error encountered while fetching information. {error.message} + + ); + } + + return {errorNode}; } if (loading) { From 707191bf6f10a5a8e4dcfe3d0c77adf7d12e70b3 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 11:03:06 -0700 Subject: [PATCH 11/39] feat(plugins/pagerduty): lookup by service-id if that is available * prefer integration-key value over service-id * disable creation of incidents if integration-key is not available Signed-off-by: Alec Jacobs --- .../components/PagerDutyCard/index.test.tsx | 130 ++++++++++++++++++ .../src/components/PagerDutyCard/index.tsx | 40 ++++-- 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 2b309533ed..54b58489be 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -252,4 +252,134 @@ describe('PageDutyCard', () => { }); expect(getByRole('dialog')).toBeInTheDocument(); }); + + describe('when entity has the pagerduty.com/service-id annotation', () => { + it('Renders PagerDuty service information', async () => { + mockPagerDutyApi.getServiceByServiceId = jest + .fn() + .mockImplementationOnce(async () => service); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + + it('Handles custom error for missing token', async () => { + mockPagerDutyApi.getServiceByServiceId = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('Missing or invalid PagerDuty Token'), + ).toBeInTheDocument(); + }); + + it('Handles custom NotFoundError', async () => { + mockPagerDutyApi.getServiceByServiceId = jest + .fn() + .mockRejectedValueOnce(new NotFoundError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); + }); + + it('handles general error', async () => { + mockPagerDutyApi.getServiceByServiceId = jest + .fn() + .mockRejectedValueOnce(new Error('An error occurred')); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText( + 'Error encountered while fetching information. An error occurred', + ), + ).toBeInTheDocument(); + }); + + it('disables the Create Incident button', async () => { + mockPagerDutyApi.getServiceByServiceId = jest + .fn() + .mockImplementationOnce(async () => service); + + const { queryByTestId, getByTitle } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(queryByTestId('trigger-dialoger')).not.toBeInTheDocument(); + expect( + getByTitle('Must provide an integration-key to create incidents') + .className, + ).toMatch('disabled'); + }); + }); + + describe('when entity has all annotations', () => { + it('queries by integration key', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockImplementationOnce(async () => [service]); + mockPagerDutyApi.getServiceByServiceId = jest.fn(); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(mockPagerDutyApi.getServiceByServiceId).not.toHaveBeenCalled(); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index b4b20fb25b..308d17a08d 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -52,7 +52,7 @@ export const isPluginApplicableToEntity = (entity: Entity) => ); export const PagerDutyCard = () => { - const { integrationKey } = usePagerdutyEntity(); + const { integrationKey, serviceId } = usePagerdutyEntity(); const api = useApi(pagerDutyApiRef); const [refreshIncidents, setRefreshIncidents] = useState(false); const [refreshChangeEvents, setRefreshChangeEvents] = @@ -77,12 +77,16 @@ export const PagerDutyCard = () => { error, } = useAsync(async () => { let service: Service; - const services = await api.getServiceByIntegrationKey( - integrationKey as string, - ); - service = services[0]; - if (!service) throw new NotFoundError(); + if (integrationKey) { + const services = await api.getServiceByIntegrationKey( + integrationKey as string, + ); + service = services[0]; + if (!service) throw new NotFoundError(); + } else { + service = await api.getServiceByServiceId(serviceId); + } return { id: service.id, @@ -128,11 +132,21 @@ export const PagerDutyCard = () => { icon: , }; + /** + * In order to create incidents using the REST API, a valid user email address must be present. + * There is no guarantee the current user entity has a valid email association, so instead just + * only allow triggering incidents when an integration key is present. + */ + const createIncidentDisabled = !integrationKey; const triggerLink: IconLinkVerticalProps = { label: 'Create Incident', onClick: showDialog, icon: , color: 'secondary', + disabled: createIncidentDisabled, + title: createIncidentDisabled + ? 'Must provide an integration-key to create incidents' + : '', }; const escalationPolicyLink: IconLinkVerticalProps = { @@ -171,12 +185,14 @@ export const PagerDutyCard = () => { - + {!createIncidentDisabled && ( + + )} ); }; From 8901c1d28540f6c2b0e0d648f05da17f06d059a8 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 11:38:38 -0700 Subject: [PATCH 12/39] docs(plugins/pagerduty): regenerate api-report Signed-off-by: Alec Jacobs --- plugins/pagerduty/api-report.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 86b917c6eb..c708a1ca4e 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -10,7 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { IdentityApi } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; // Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -46,7 +46,7 @@ export class PagerDutyClient implements PagerDutyApi { static fromConfig( configApi: ConfigApi, discoveryApi: DiscoveryApi, - identityApi: IdentityApi, + fetchApi: FetchApi, ): PagerDutyClient; // Warning: (ae-forgotten-export) The symbol "ChangeEvent" needs to be exported by the entry point index.d.ts // @@ -64,6 +64,8 @@ export class PagerDutyClient implements PagerDutyApi { // // (undocumented) getServiceByIntegrationKey(integrationKey: string): Promise; + // (undocumented) + getServiceByServiceId(serviceId: string): Promise; // Warning: (ae-forgotten-export) The symbol "TriggerAlarmRequest" needs to be exported by the entry point index.d.ts // // (undocumented) From 946c11ad03c21363de665e2c2ad0eb895197dcf6 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 12:06:32 -0700 Subject: [PATCH 13/39] docs(plugins/pagerduty): update README for new annotation support Signed-off-by: Alec Jacobs --- plugins/pagerduty/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 71050dc54b..7b3124ab0f 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -7,6 +7,7 @@ - The Backstage PagerDuty plugin allows PagerDuty information about a Backstage entity to be displayed within Backstage. This includes active incidents, recent change events, as well as the current on-call responders' names, email addresses, and links to their profiles in PagerDuty. - Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders. + - _Note: This feature is only available when providing the `pagerduty.com/integration-key` annotation_ - Change events will be displayed in a separate tab. If the change event payload has additional links the first link only will be rendered. # Requirements @@ -117,6 +118,26 @@ This will proxy the request by adding an `Authorization` header with the provide ### Optional configuration +#### Annotating with Service ID + +If you want to integrate a PagerDuty service with Backstage but don't want to use an integration key, you can also [annotate](https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional) the appropriate entity with a PagerDuty Service ID instead + +```yaml +annotations: + pagerduty.com/service-id: [SERVICE_ID] +``` + +This service ID can be found by navigating to a Service within PagerDuty and pulling the ID value out of the URL. + +1. From the **Configuration** menu within PagerDuty, select **Services**. +2. Click the **name** of the service you want to represent for your Entity. + +Your browser URL should now be located at `https://pagerduty.com/service-directory/[SERVICE_ID]`. + +_Note: When annotating with `pagerduty.com/service-id`, the feature to Create Incidents is not currently supported_ + +#### Custom Events URL + If you want to override the default URL used for events, you can add it to `app-config.yaml`: ```yaml From 8798f8d93f6328834573d5d321215f833e5ac82c Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 27 May 2022 11:49:06 -0700 Subject: [PATCH 14/39] chore: generate changeset Signed-off-by: Alec Jacobs --- .changeset/weak-bananas-deliver.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/weak-bananas-deliver.md diff --git a/.changeset/weak-bananas-deliver.md b/.changeset/weak-bananas-deliver.md new file mode 100644 index 0000000000..db76f396a8 --- /dev/null +++ b/.changeset/weak-bananas-deliver.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-pagerduty': minor +--- + +Introduces a new annotation `pagerduty.com/service-id` that can be used instead of the `pagerduty.com/integration-key` annotation. +_Note: If both annotations are specified on a given Entity, then the `pagerduty.com/integration-key` annotation will be prefered_ + +**BREAKING** The `PagerDutyClient.fromConfig` static method now expects a `FetchApi` compatible object as a third argument. +The `PagerDutyClient` now relies on a `fetchApi` being available to execute `fetch` requests. + +In addition, various enhancements/bug fixes were introduced: + +- The `PagerDutyCard` component now wraps error and loading messages with an `InfoCard` to contain errors/messages. This enforces a consistent experience on the EntityPage +- If no service can be found for the provided integration key, a new Error Message Empty State component will be shown instead of an error alert +- Introduces the `fetchApi` to replace standard `window.fetch` + - ensures that Identity Authorization is respected and provided in API requests From f13bf8b510cb94f3922f1157efd5fb57186b853c Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Mon, 30 May 2022 07:31:01 -0700 Subject: [PATCH 15/39] feat(plugins/pagerduty): add @backstage/errors package Signed-off-by: Alec Jacobs --- plugins/pagerduty/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 73a92ed5f7..6d11ee8d69 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -37,6 +37,7 @@ "@backstage/catalog-model": "^1.0.3", "@backstage/core-components": "^0.9.5", "@backstage/core-plugin-api": "^1.0.3", + "@backstage/errors": "^1.0.0", "@backstage/plugin-catalog-react": "^1.1.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", From aba92d8f6aa7e07eabd185024c0766a5f72f5726 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Mon, 30 May 2022 07:26:42 -0700 Subject: [PATCH 16/39] fix(plugins/pagerduty): use NotFoundError from @backstage/errors Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 2 +- plugins/pagerduty/src/api/index.ts | 7 +------ .../pagerduty/src/components/PagerDutyCard/index.test.tsx | 2 +- plugins/pagerduty/src/components/PagerDutyCard/index.tsx | 3 ++- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 6c31f2a5d0..8ab0bcdc31 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -32,9 +32,9 @@ import { ConfigApi, FetchApi, } from '@backstage/core-plugin-api'; +import { NotFoundError } from '@backstage/errors'; export class UnauthorizedError extends Error {} -export class NotFoundError extends Error {} export const pagerDutyApiRef = createApiRef({ id: 'plugin.pagerduty.api', diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts index e23ee0c9a4..015204b1e8 100644 --- a/plugins/pagerduty/src/api/index.ts +++ b/plugins/pagerduty/src/api/index.ts @@ -14,10 +14,5 @@ * limitations under the License. */ -export { - PagerDutyClient, - pagerDutyApiRef, - UnauthorizedError, - NotFoundError, -} from './client'; +export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client'; export type { PagerDutyApi } from './types'; diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 54b58489be..041f76fb8c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -18,11 +18,11 @@ import { render, waitFor, fireEvent, act } from '@testing-library/react'; import { PagerDutyCard, isPluginApplicableToEntity } from '../PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { NotFoundError } from '@backstage/errors'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef, UnauthorizedError, - NotFoundError, PagerDutyClient, } from '../../api'; import { Service, User } from '../types'; diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 308d17a08d..48a7f69e3c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -20,7 +20,7 @@ import { Incidents } from '../Incident'; import { EscalationPolicy } from '../Escalation'; import useAsync from 'react-use/lib/useAsync'; import { Alert } from '@material-ui/lab'; -import { pagerDutyApiRef, UnauthorizedError, NotFoundError } from '../../api'; +import { pagerDutyApiRef, UnauthorizedError } from '../../api'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { MissingTokenError, ServiceNotFoundError } from '../Errors'; import WebIcon from '@material-ui/icons/Web'; @@ -32,6 +32,7 @@ import { ChangeEvents } from '../ChangeEvents'; import { Service } from '../types'; import { useApi } from '@backstage/core-plugin-api'; +import { NotFoundError } from '@backstage/errors'; import { Progress, HeaderIconLinkRow, From d8abae2ce6a2f1d10b56728da68edc4ce0c9202d Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Mon, 30 May 2022 07:21:52 -0700 Subject: [PATCH 17/39] chore(plugins/pagerduty): dont shadow outer variable Signed-off-by: Alec Jacobs --- .../src/components/PagerDutyCard/index.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 48a7f69e3c..2edc8cbd44 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -77,24 +77,24 @@ export const PagerDutyCard = () => { loading, error, } = useAsync(async () => { - let service: Service; + let foundService: Service; if (integrationKey) { const services = await api.getServiceByIntegrationKey( integrationKey as string, ); - service = services[0]; - if (!service) throw new NotFoundError(); + foundService = services[0]; + if (!foundService) throw new NotFoundError(); } else { - service = await api.getServiceByServiceId(serviceId); + foundService = await api.getServiceByServiceId(serviceId); } return { - id: service.id, - name: service.name, - url: service.html_url, - policyId: service.escalation_policy.id, - policyLink: service.escalation_policy.html_url, + id: foundService.id, + name: foundService.name, + url: foundService.html_url, + policyId: foundService.escalation_policy.id, + policyLink: foundService.escalation_policy.html_url, }; }); From 56926b6f1e365209f5079086820754222b88c82b Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Mon, 30 May 2022 07:28:51 -0700 Subject: [PATCH 18/39] chore(plugins/pagerduty): move mockPagerDutyApi to not reference const before it is defined Signed-off-by: Alec Jacobs --- .../components/PagerDutyCard/index.test.tsx | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 041f76fb8c..fbb19c9897 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -20,28 +20,12 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; -import { - pagerDutyApiRef, - UnauthorizedError, - PagerDutyClient, -} from '../../api'; +import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; import { Service, User } from '../types'; import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; -const mockPagerDutyApi: Partial = { - getServiceByIntegrationKey: async () => [], - getServiceByServiceId: async () => service, - getOnCallByPolicyId: async () => [], - getIncidentsByServiceId: async () => [], -}; - -const apis = TestApiRegistry.from( - [pagerDutyApiRef, mockPagerDutyApi], - [alertApiRef, {}], -); - const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -105,6 +89,18 @@ const service: Service = { integrationKey: 'abc123', }; +const mockPagerDutyApi: Partial = { + getServiceByIntegrationKey: async () => [], + getServiceByServiceId: async () => service, + getOnCallByPolicyId: async () => [], + getIncidentsByServiceId: async () => [], +}; + +const apis = TestApiRegistry.from( + [pagerDutyApiRef, mockPagerDutyApi], + [alertApiRef, {}], +); + describe('isPluginApplicableToEntity', () => { describe('when entity has no annotations', () => { it('returns false', () => { From 67bfd75de1396cddce6d3e8a7be6787c111c6847 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 09:46:39 -0700 Subject: [PATCH 19/39] feat(plugins/pagerduty): add ClientApiDependencies type * have ClientApiConfig join ClientApiDependencies with custom configuration Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/types.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index e747fd40fe..4f80d39816 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -81,12 +81,15 @@ export type OnCallsResponse = { oncalls: OnCall[]; }; -export type ClientApiConfig = { - eventsBaseUrl?: string; +export type ClientApiDependencies = { discoveryApi: DiscoveryApi; fetchApi: FetchApi; }; +export type ClientApiConfig = ClientApiDependencies & { + eventsBaseUrl?: string; +}; + export type RequestOptions = { method: string; headers: HeadersInit; From 89f596bb4a671659492e6c796c60292badd23709 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 09:50:33 -0700 Subject: [PATCH 20/39] feat(plugins/pagerduty): refactor PagerDutyClient.fromConfig to accept second argument of ClientApiDependencies Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 12 ++++-------- plugins/pagerduty/src/plugin.ts | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 8ab0bcdc31..9bfd6a5594 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -22,16 +22,12 @@ import { ServiceResponse, IncidentsResponse, OnCallsResponse, + ClientApiDependencies, ClientApiConfig, RequestOptions, ChangeEventsResponse, } from './types'; -import { - createApiRef, - DiscoveryApi, - ConfigApi, - FetchApi, -} from '@backstage/core-plugin-api'; +import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; export class UnauthorizedError extends Error {} @@ -46,12 +42,12 @@ const commonGetServiceParams = export class PagerDutyClient implements PagerDutyApi { static fromConfig( configApi: ConfigApi, - discoveryApi: DiscoveryApi, - fetchApi: FetchApi, + { discoveryApi, fetchApi }: ClientApiDependencies, ) { const eventsBaseUrl: string = configApi.getOptionalString('pagerDuty.eventsBaseUrl') ?? 'https://events.pagerduty.com/v2'; + return new PagerDutyClient({ eventsBaseUrl, discoveryApi, diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 3baa36a50e..d49de29416 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -39,7 +39,7 @@ export const pagerDutyPlugin = createPlugin({ fetchApi: fetchApiRef, }, factory: ({ configApi, discoveryApi, fetchApi }) => - PagerDutyClient.fromConfig(configApi, discoveryApi, fetchApi), + PagerDutyClient.fromConfig(configApi, { discoveryApi, fetchApi }), }), ], }); From 1b0ab057575e9e3b08742464798ffa919cb382b1 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 10:46:59 -0700 Subject: [PATCH 21/39] feat(plugins/pagerduty): add PagerDutyEntity type Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/types.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 plugins/pagerduty/src/types.ts diff --git a/plugins/pagerduty/src/types.ts b/plugins/pagerduty/src/types.ts new file mode 100644 index 0000000000..07f7f2bbe3 --- /dev/null +++ b/plugins/pagerduty/src/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 type PagerDutyEntity = { + integrationKey?: string; + serviceId?: string; + name: string; +}; From 05da90db84104fd67541f644407d5d41974b05bd Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 10:56:29 -0700 Subject: [PATCH 22/39] feat(plugins/pagerduty): enforce PagerDutyEntity type from usePagerdutyEntity Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/components/PagerDutyCard/index.tsx | 2 +- plugins/pagerduty/src/hooks/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 2edc8cbd44..e089cf817c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -86,7 +86,7 @@ export const PagerDutyCard = () => { foundService = services[0]; if (!foundService) throw new NotFoundError(); } else { - foundService = await api.getServiceByServiceId(serviceId); + foundService = await api.getServiceByServiceId(serviceId!); } return { diff --git a/plugins/pagerduty/src/hooks/index.ts b/plugins/pagerduty/src/hooks/index.ts index a5fa462e02..503597a69e 100644 --- a/plugins/pagerduty/src/hooks/index.ts +++ b/plugins/pagerduty/src/hooks/index.ts @@ -15,13 +15,14 @@ */ import { useEntity } from '@backstage/plugin-catalog-react'; +import { PagerDutyEntity } from '../types'; import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID, } from '../components/constants'; -export function usePagerdutyEntity() { +export function usePagerdutyEntity(): PagerDutyEntity { const { entity } = useEntity(); const { [PAGERDUTY_INTEGRATION_KEY]: integrationKey, From 4d755224cc572b7440a0c3f29f5102e41b7e8293 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 14:30:22 -0700 Subject: [PATCH 23/39] feat(plugins/pagerduty): add getServiceByEntity client function Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.test.ts | 359 +++++++++++++++++++++++ plugins/pagerduty/src/api/client.ts | 32 ++ plugins/pagerduty/src/api/types.ts | 9 + 3 files changed, 400 insertions(+) create mode 100644 plugins/pagerduty/src/api/client.test.ts diff --git a/plugins/pagerduty/src/api/client.test.ts b/plugins/pagerduty/src/api/client.test.ts new file mode 100644 index 0000000000..d3e9b58542 --- /dev/null +++ b/plugins/pagerduty/src/api/client.test.ts @@ -0,0 +1,359 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { MockFetchApi } from '@backstage/test-utils'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { PagerDutyClient, UnauthorizedError } from './client'; +import { PagerDutyEntity } from '../types'; +import { Service, User } from '../components/types'; +import { NotFoundError } from '@backstage/errors'; + +const mockFetch = jest.fn().mockName('fetch'); +const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest + .fn() + .mockName('discoveryApi') + .mockResolvedValue('http://localhost:7007/proxy'), +}; +const mockFetchApi: MockFetchApi = new MockFetchApi({ + baseImplementation: mockFetch, +}); + +let client: PagerDutyClient; +let pagerDutyEntity: PagerDutyEntity; + +const user: User = { + name: 'person1', + id: 'p1', + summary: 'person1', + email: 'person1@example.com', + html_url: 'http://a.com/id1', +}; + +const service: Service = { + id: 'def456', + name: 'pagerduty-name', + html_url: 'www.example.com', + escalation_policy: { + id: 'def', + user: user, + html_url: 'http://a.com/id1', + }, + integrationKey: 'abc123', +}; + +const requestHeaders = { + headers: { + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }, + method: 'GET', +}; + +describe('PagerDutyClient', () => { + beforeEach(() => { + mockFetch.mockReset(); + + client = new PagerDutyClient({ + eventsBaseUrl: 'https://events.pagerduty.com/v2', + discoveryApi: mockDiscoveryApi, + fetchApi: mockFetchApi, + }); + }); + + describe('getServiceByEntity', () => { + describe('when provided entity has an integrationKey value', () => { + beforeEach(() => { + pagerDutyEntity = { + name: 'pagerduty-test', + integrationKey: 'abc123', + }; + }); + + it('queries proxy path by integration id', async () => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve({ services: [service] }), + }); + + expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + service, + }); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:7007/proxy/pagerduty/services?time_zone=UTC&include[]=integrations&include[]=escalation_policies&query=abc123', + requestHeaders, + ); + }); + + describe('on 401 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 401, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws UnauthorizedError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(UnauthorizedError); + }); + }); + + describe('on 404 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 404, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + }); + }); + + describe('on other non-ok response', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 500, + ok: false, + json: () => + Promise.resolve({ + errors: ['Not valid request', 'internal error occurred'], + }), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError( + 'Request failed with 500, Not valid request internal error occurred', + ); + }); + }); + + describe('when services response is empty', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve({ services: [] }), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + }); + }); + }); + + describe('when provided entity has a serviceId value', () => { + beforeEach(() => { + pagerDutyEntity = { + name: 'pagerduty-test', + serviceId: 'def456', + }; + }); + + it('queries proxy path by integration id', async () => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve({ service }), + }); + + expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + service, + }); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:7007/proxy/pagerduty/services/def456?time_zone=UTC&include[]=integrations&include[]=escalation_policies', + requestHeaders, + ); + }); + + describe('on 401 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 401, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws UnauthorizedError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(UnauthorizedError); + }); + }); + + describe('on 404 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 404, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + }); + }); + + describe('on other non-ok response', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 500, + ok: false, + json: () => + Promise.resolve({ + errors: ['Not valid request', 'internal error occurred'], + }), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError( + 'Request failed with 500, Not valid request internal error occurred', + ); + }); + }); + }); + + describe('when provided entity has both integrationKey and serviceId', () => { + beforeEach(() => { + pagerDutyEntity = { + name: 'pagerduty-test', + integrationKey: 'abc123', + serviceId: 'def456', + }; + }); + + it('queries proxy path by integration id', async () => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve({ services: [service] }), + }); + + expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + service, + }); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:7007/proxy/pagerduty/services?time_zone=UTC&include[]=integrations&include[]=escalation_policies&query=abc123', + requestHeaders, + ); + }); + + describe('on 401 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 401, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws UnauthorizedError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(UnauthorizedError); + }); + }); + + describe('on 404 response code', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 404, + ok: false, + json: () => Promise.resolve({}), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + }); + }); + + describe('on other non-ok response', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 500, + ok: false, + json: () => + Promise.resolve({ + errors: ['Not valid request', 'internal error occurred'], + }), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError( + 'Request failed with 500, Not valid request internal error occurred', + ); + }); + }); + + describe('when services response is empty', () => { + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve({ services: [] }), + }); + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + }); + }); + }); + + describe('when provided entity has no integrationKey or serviceId values', () => { + beforeEach(() => { + pagerDutyEntity = { + name: 'pagerduty-test', + }; + }); + + it('throws NotFoundError', async () => { + await expect( + client.getServiceByEntity(pagerDutyEntity), + ).rejects.toThrowError(NotFoundError); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 9bfd6a5594..e6f4eb4aef 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -29,6 +29,7 @@ import { } from './types'; import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; +import { PagerDutyEntity } from '../types'; export class UnauthorizedError extends Error {} @@ -76,6 +77,37 @@ export class PagerDutyClient implements PagerDutyApi { return service; } + async getServiceByEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise { + const { integrationKey, serviceId } = pagerDutyEntity; + + let response: ServiceResponse; + let url: string; + + if (integrationKey) { + url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/services?${commonGetServiceParams}&query=${integrationKey}`; + const { services } = await this.getByUrl(url); + const service = services[0]; + + if (!service) throw new NotFoundError(); + + response = { service }; + } else if (serviceId) { + url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/services/${serviceId}?${commonGetServiceParams}`; + + response = await this.getByUrl(url); + } else { + throw new NotFoundError(); + } + + return response; + } + async getIncidentsByServiceId(serviceId: string): Promise { const params = `time_zone=UTC&sort_by=created_at&statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`; const url = `${await this.config.discoveryApi.getBaseUrl( diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 4f80d39816..a0736304f0 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -16,6 +16,7 @@ import { Incident, ChangeEvent, OnCall, Service } from '../components/types'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { PagerDutyEntity } from '../types'; export type TriggerAlarmRequest = { integrationKey: string; @@ -37,6 +38,14 @@ export interface PagerDutyApi { */ getServiceByServiceId(serviceId: string): Promise; + /** + * Fetches the service for the provided PagerDutyEntity. + * + */ + getServiceByEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise; + /** * Fetches a list of incidents a provided service has. * From 71cc97f207760d2cf6ad402c7e771e9c20eca362 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 15:13:02 -0700 Subject: [PATCH 24/39] feat(plugins/pagerduty): refactor to use PagerDutyClient.getServiceByEntity Signed-off-by: Alec Jacobs --- .../components/PagerDutyCard/index.test.tsx | 58 ++++++------------- .../src/components/PagerDutyCard/index.tsx | 19 ++---- 2 files changed, 23 insertions(+), 54 deletions(-) diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index fbb19c9897..68eea5990c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -90,8 +90,7 @@ const service: Service = { }; const mockPagerDutyApi: Partial = { - getServiceByIntegrationKey: async () => [], - getServiceByServiceId: async () => service, + getServiceByEntity: async () => ({ service }), getOnCallByPolicyId: async () => [], getIncidentsByServiceId: async () => [], }; @@ -129,9 +128,9 @@ describe('isPluginApplicableToEntity', () => { describe('PageDutyCard', () => { it('Render pagerduty', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() - .mockImplementationOnce(async () => [service]); + .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -150,7 +149,7 @@ describe('PageDutyCard', () => { }); it('Handles custom error for missing token', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new UnauthorizedError()); @@ -168,7 +167,7 @@ describe('PageDutyCard', () => { }); it('Handles custom NotFoundError', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new NotFoundError()); @@ -186,7 +185,7 @@ describe('PageDutyCard', () => { }); it('handles general error', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new Error('An error occurred')); const { getByText, queryByTestId } = render( @@ -207,28 +206,10 @@ describe('PageDutyCard', () => { ).toBeInTheDocument(); }); - it('handles empty response from getServiceByIntegrationKey', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest - .fn() - .mockImplementationOnce(async () => []); - - const { getByText, queryByTestId } = render( - wrapInTestApp( - - - - - , - ), - ); - await waitFor(() => !queryByTestId('progress')); - expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument(); - }); - it('opens the dialog when trigger button is clicked', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() - .mockImplementationOnce(async () => [service]); + .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId, getByRole } = render( wrapInTestApp( @@ -251,9 +232,9 @@ describe('PageDutyCard', () => { describe('when entity has the pagerduty.com/service-id annotation', () => { it('Renders PagerDuty service information', async () => { - mockPagerDutyApi.getServiceByServiceId = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() - .mockImplementationOnce(async () => service); + .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -272,7 +253,7 @@ describe('PageDutyCard', () => { }); it('Handles custom error for missing token', async () => { - mockPagerDutyApi.getServiceByServiceId = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new UnauthorizedError()); @@ -292,7 +273,7 @@ describe('PageDutyCard', () => { }); it('Handles custom NotFoundError', async () => { - mockPagerDutyApi.getServiceByServiceId = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new NotFoundError()); @@ -310,7 +291,7 @@ describe('PageDutyCard', () => { }); it('handles general error', async () => { - mockPagerDutyApi.getServiceByServiceId = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() .mockRejectedValueOnce(new Error('An error occurred')); const { getByText, queryByTestId } = render( @@ -332,9 +313,9 @@ describe('PageDutyCard', () => { }); it('disables the Create Incident button', async () => { - mockPagerDutyApi.getServiceByServiceId = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() - .mockImplementationOnce(async () => service); + .mockImplementationOnce(async () => ({ service })); const { queryByTestId, getByTitle } = render( wrapInTestApp( @@ -346,7 +327,6 @@ describe('PageDutyCard', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(queryByTestId('trigger-dialoger')).not.toBeInTheDocument(); expect( getByTitle('Must provide an integration-key to create incidents') .className, @@ -356,22 +336,20 @@ describe('PageDutyCard', () => { describe('when entity has all annotations', () => { it('queries by integration key', async () => { - mockPagerDutyApi.getServiceByIntegrationKey = jest + mockPagerDutyApi.getServiceByEntity = jest .fn() - .mockImplementationOnce(async () => [service]); - mockPagerDutyApi.getServiceByServiceId = jest.fn(); + .mockImplementationOnce(async () => ({ service })); const { getByText, queryByTestId } = render( wrapInTestApp( - + , ), ); await waitFor(() => !queryByTestId('progress')); - expect(mockPagerDutyApi.getServiceByServiceId).not.toHaveBeenCalled(); expect(getByText('Service Directory')).toBeInTheDocument(); expect(getByText('Create Incident')).toBeInTheDocument(); expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index e089cf817c..174caf4900 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -29,7 +29,6 @@ import { usePagerdutyEntity } from '../../hooks'; import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; import { TriggerDialog } from '../TriggerDialog'; import { ChangeEvents } from '../ChangeEvents'; -import { Service } from '../types'; import { useApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; @@ -53,7 +52,7 @@ export const isPluginApplicableToEntity = (entity: Entity) => ); export const PagerDutyCard = () => { - const { integrationKey, serviceId } = usePagerdutyEntity(); + const pagerDutyEntity = usePagerdutyEntity(); const api = useApi(pagerDutyApiRef); const [refreshIncidents, setRefreshIncidents] = useState(false); const [refreshChangeEvents, setRefreshChangeEvents] = @@ -77,17 +76,9 @@ export const PagerDutyCard = () => { loading, error, } = useAsync(async () => { - let foundService: Service; - - if (integrationKey) { - const services = await api.getServiceByIntegrationKey( - integrationKey as string, - ); - foundService = services[0]; - if (!foundService) throw new NotFoundError(); - } else { - foundService = await api.getServiceByServiceId(serviceId!); - } + const { service: foundService } = await api.getServiceByEntity( + pagerDutyEntity, + ); return { id: foundService.id, @@ -138,7 +129,7 @@ export const PagerDutyCard = () => { * There is no guarantee the current user entity has a valid email association, so instead just * only allow triggering incidents when an integration key is present. */ - const createIncidentDisabled = !integrationKey; + const createIncidentDisabled = !pagerDutyEntity.integrationKey; const triggerLink: IconLinkVerticalProps = { label: 'Create Incident', onClick: showDialog, From 8fe1113c96468277217cc59f9d886babc779aedb Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 15:14:53 -0700 Subject: [PATCH 25/39] feat(plugins/pagerduty): remove unused getServiceByServiceId Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 10 ---------- plugins/pagerduty/src/api/types.ts | 6 ------ 2 files changed, 16 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index e6f4eb4aef..50428e0a18 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -67,16 +67,6 @@ export class PagerDutyClient implements PagerDutyApi { return services; } - async getServiceByServiceId(serviceId: string): Promise { - const params = commonGetServiceParams; - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/pagerduty/services/${serviceId}?${params}`; - const { service } = await this.getByUrl(url); - - return service; - } - async getServiceByEntity( pagerDutyEntity: PagerDutyEntity, ): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index a0736304f0..421a0f9772 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -32,12 +32,6 @@ export interface PagerDutyApi { */ getServiceByIntegrationKey(integrationKey: string): Promise; - /** - * Fetches the service for the provided service id. - * - */ - getServiceByServiceId(serviceId: string): Promise; - /** * Fetches the service for the provided PagerDutyEntity. * From 9b8438fd7216cffe8376213b9ee454c0e126e14b Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 15:15:29 -0700 Subject: [PATCH 26/39] feat(plugins/pagerduty): remove unused getServiceByIntegrationKey Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 10 ---------- plugins/pagerduty/src/api/types.ts | 6 ------ 2 files changed, 16 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 50428e0a18..50d9c7cdd9 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -57,16 +57,6 @@ export class PagerDutyClient implements PagerDutyApi { } constructor(private readonly config: ClientApiConfig) {} - async getServiceByIntegrationKey(integrationKey: string): Promise { - const params = `${commonGetServiceParams}&query=${integrationKey}`; - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/pagerduty/services?${params}`; - const { services } = await this.getByUrl(url); - - return services; - } - async getServiceByEntity( pagerDutyEntity: PagerDutyEntity, ): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 421a0f9772..8ec95c285b 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -26,12 +26,6 @@ export type TriggerAlarmRequest = { }; export interface PagerDutyApi { - /** - * Fetches a list of services, filtered by the provided integration key. - * - */ - getServiceByIntegrationKey(integrationKey: string): Promise; - /** * Fetches the service for the provided PagerDutyEntity. * From 206fc1f60199ad0fd9042761a072fcaff40ef5ce Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 15:27:48 -0700 Subject: [PATCH 27/39] feat(plugins/pagerduty): update getIncidentsByServiceId response to be Promise Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 5 ++--- plugins/pagerduty/src/api/types.ts | 2 +- .../src/components/Incident/Incidents.test.tsx | 11 ++++++----- .../pagerduty/src/components/Incident/Incidents.tsx | 7 ++++++- .../src/components/PagerDutyCard/index.test.tsx | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 50d9c7cdd9..ac5afdae79 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -88,14 +88,13 @@ export class PagerDutyClient implements PagerDutyApi { return response; } - async getIncidentsByServiceId(serviceId: string): Promise { + async getIncidentsByServiceId(serviceId: string): Promise { const params = `time_zone=UTC&sort_by=created_at&statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/incidents?${params}`; - const { incidents } = await this.getByUrl(url); - return incidents; + return await this.getByUrl(url); } async getChangeEventsByServiceId(serviceId: string): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 8ec95c285b..9ecd233bab 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -38,7 +38,7 @@ export interface PagerDutyApi { * Fetches a list of incidents a provided service has. * */ - getIncidentsByServiceId(serviceId: string): Promise; + getIncidentsByServiceId(serviceId: string): Promise; /** * Fetches a list of change events a provided service has. diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index 74e7d82e8b..ad69bc083e 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -30,7 +30,7 @@ describe('Incidents', () => { it('Renders an empty state when there are no incidents', async () => { mockPagerDutyApi.getIncidentsByServiceId = jest .fn() - .mockImplementationOnce(async () => []); + .mockImplementationOnce(async () => ({ incidents: [] })); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -44,9 +44,10 @@ describe('Incidents', () => { }); it('Renders all incidents', async () => { - mockPagerDutyApi.getIncidentsByServiceId = jest.fn().mockImplementationOnce( - async () => - [ + mockPagerDutyApi.getIncidentsByServiceId = jest + .fn() + .mockImplementationOnce(async () => ({ + incidents: [ { id: 'id1', status: 'triggered', @@ -83,7 +84,7 @@ describe('Incidents', () => { serviceId: 'sId2', }, ] as Incident[], - ); + })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx index 06dd50bdaa..2e0e201c79 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx @@ -34,7 +34,12 @@ export const Incidents = ({ serviceId, refreshIncidents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( - async () => await api.getIncidentsByServiceId(serviceId), + async () => { + const { incidents: foundIncidents } = await api.getIncidentsByServiceId( + serviceId, + ); + return foundIncidents; + }, ); useEffect(() => { diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 68eea5990c..dce1bb536a 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -92,7 +92,7 @@ const service: Service = { const mockPagerDutyApi: Partial = { getServiceByEntity: async () => ({ service }), getOnCallByPolicyId: async () => [], - getIncidentsByServiceId: async () => [], + getIncidentsByServiceId: async () => ({ incidents: [] }), }; const apis = TestApiRegistry.from( From 7378e1710cb3430d33b4a163a8a2b0afdecd43c5 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 16:59:59 -0700 Subject: [PATCH 28/39] feat(plugins/pagerduty): update getChangeEventsByServiceId response to be Promise Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 8 +- plugins/pagerduty/src/api/types.ts | 2 +- .../ChangeEvents/ChangeEvents.test.tsx | 122 +++++++++--------- .../components/ChangeEvents/ChangeEvents.tsx | 5 +- 4 files changed, 69 insertions(+), 68 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index ac5afdae79..bf6835ec2a 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -97,15 +97,15 @@ export class PagerDutyClient implements PagerDutyApi { return await this.getByUrl(url); } - async getChangeEventsByServiceId(serviceId: string): Promise { + async getChangeEventsByServiceId( + serviceId: string, + ): Promise { const params = `limit=5&time_zone=UTC&sort_by=timestamp`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/services/${serviceId}/change_events?${params}`; - const { change_events } = await this.getByUrl(url); - - return change_events; + return await this.getByUrl(url); } async getOnCallByPolicyId(policyId: string): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 9ecd233bab..8914c4ed97 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -44,7 +44,7 @@ export interface PagerDutyApi { * Fetches a list of change events a provided service has. * */ - getChangeEventsByServiceId(serviceId: string): Promise; + getChangeEventsByServiceId(serviceId: string): Promise; /** * Fetches the list of users in an escalation policy. diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx index fca2422198..017e394de0 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx @@ -30,7 +30,7 @@ describe('Incidents', () => { it('Renders an empty state when there are no change events', async () => { mockPagerDutyApi.getChangeEventsByServiceId = jest .fn() - .mockImplementationOnce(async () => []); + .mockImplementationOnce(async () => ({ change_events: [] })); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -46,37 +46,36 @@ describe('Incidents', () => { it('Renders all change events', async () => { mockPagerDutyApi.getChangeEventsByServiceId = jest .fn() - .mockImplementationOnce( - async () => - [ - { - id: 'id1', - source: 'changeSource1', - html_url: 'www.pdlink.com', - links: [ - { - href: 'www.externalLink1.com', - text: 'link1', - }, - ], - summary: 'summary of event', - timestamp: '2020-07-17T08:42:58.315+0000', - }, - { - id: 'id2', - source: 'changeSource1', - html_url: 'www.pdlink.com/link', - links: [ - { - href: 'www.externalLink1.com', - text: 'link1', - }, - ], - summary: 'sum of EVENT', - timestamp: '2020-07-18T08:42:58.315+0000', - }, - ] as ChangeEvent[], - ); + .mockImplementationOnce(async () => ({ + change_events: [ + { + id: 'id1', + source: 'changeSource1', + html_url: 'www.pdlink.com', + links: [ + { + href: 'www.externalLink1.com', + text: 'link1', + }, + ], + summary: 'summary of event', + timestamp: '2020-07-17T08:42:58.315+0000', + }, + { + id: 'id2', + source: 'changeSource1', + html_url: 'www.pdlink.com/link', + links: [ + { + href: 'www.externalLink1.com', + text: 'link1', + }, + ], + summary: 'sum of EVENT', + timestamp: '2020-07-18T08:42:58.315+0000', + }, + ] as ChangeEvent[], + })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( @@ -95,36 +94,35 @@ describe('Incidents', () => { it('Does not render a pagerduty link when html_url is not present in response', async () => { mockPagerDutyApi.getChangeEventsByServiceId = jest .fn() - .mockImplementationOnce( - async () => - [ - { - id: 'id1', - source: 'changeSource1', - links: [ - { - href: 'www.externalLink1.com', - text: 'link1', - }, - ], - summary: 'summary of event', - timestamp: '2020-07-17T08:42:58.315+0000', - }, - { - id: 'id2', - source: 'changeSource1', - html_url: 'www.pdlink.com/link', - links: [ - { - href: 'www.externalLink1.com', - text: 'link1', - }, - ], - summary: 'sum of EVENT', - timestamp: '2020-07-18T08:42:58.315+0000', - }, - ] as ChangeEvent[], - ); + .mockImplementationOnce(async () => ({ + change_events: [ + { + id: 'id1', + source: 'changeSource1', + links: [ + { + href: 'www.externalLink1.com', + text: 'link1', + }, + ], + summary: 'summary of event', + timestamp: '2020-07-17T08:42:58.315+0000', + }, + { + id: 'id2', + source: 'changeSource1', + html_url: 'www.pdlink.com/link', + links: [ + { + href: 'www.externalLink1.com', + text: 'link1', + }, + ], + summary: 'sum of EVENT', + timestamp: '2020-07-18T08:42:58.315+0000', + }, + ] as ChangeEvent[], + })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.tsx index 58b4dd6acf..4b19257cdc 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.tsx @@ -33,7 +33,10 @@ export const ChangeEvents = ({ serviceId, refreshEvents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: changeEvents, loading, error }, getChangeEvents] = useAsyncFn( - async () => await api.getChangeEventsByServiceId(serviceId), + async () => { + const { change_events } = await api.getChangeEventsByServiceId(serviceId); + return change_events; + }, ); useEffect(() => { From 48a7d96e7270fdce9728823b1d864aa9f2c9e221 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 17:05:34 -0700 Subject: [PATCH 29/39] feat(plugins/pagerduty): update getOnCallByPolicyId response to be Promise Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 5 ++-- plugins/pagerduty/src/api/types.ts | 2 +- .../components/Escalation/Escalation.test.tsx | 26 ++++++++++--------- .../Escalation/EscalationPolicy.tsx | 2 +- .../components/PagerDutyCard/index.test.tsx | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index bf6835ec2a..7f9f3d6c59 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -108,14 +108,13 @@ export class PagerDutyClient implements PagerDutyApi { return await this.getByUrl(url); } - async getOnCallByPolicyId(policyId: string): Promise { + async getOnCallByPolicyId(policyId: string): Promise { const params = `time_zone=UTC&include[]=users&escalation_policy_ids[]=${policyId}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/oncalls?${params}`; - const { oncalls } = await this.getByUrl(url); - return oncalls; + return await this.getByUrl(url); } triggerAlarm(request: TriggerAlarmRequest): Promise { diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 8914c4ed97..d2ab0290d3 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -50,7 +50,7 @@ export interface PagerDutyApi { * Fetches the list of users in an escalation policy. * */ - getOnCallByPolicyId(policyId: string): Promise; + getOnCallByPolicyId(policyId: string): Promise; /** * Triggers an incident to whoever is on-call. diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx index 47f18002fe..33e8865f59 100644 --- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx +++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx @@ -30,7 +30,7 @@ describe('Escalation', () => { it('Handles an empty response', async () => { mockPagerDutyApi.getOnCallByPolicyId = jest .fn() - .mockImplementationOnce(async () => []); + .mockImplementationOnce(async () => ({ oncalls: [] })); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -48,17 +48,19 @@ describe('Escalation', () => { it('Render a list of users', async () => { mockPagerDutyApi.getOnCallByPolicyId = jest .fn() - .mockImplementationOnce(async () => [ - { - user: { - name: 'person1', - id: 'p1', - summary: 'person1', - email: 'person1@example.com', - html_url: 'http://a.com/id1', - } as User, - }, - ]); + .mockImplementationOnce(async () => ({ + oncalls: [ + { + user: { + name: 'person1', + id: 'p1', + summary: 'person1', + email: 'person1@example.com', + html_url: 'http://a.com/id1', + } as User, + }, + ], + })); const { getByText, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx index 704887be58..8602733ee3 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx @@ -37,7 +37,7 @@ export const EscalationPolicy = ({ policyId }: Props) => { loading, error, } = useAsync(async () => { - const oncalls = await api.getOnCallByPolicyId(policyId); + const { oncalls } = await api.getOnCallByPolicyId(policyId); const usersItem = oncalls .sort((a, b) => a.escalation_level - b.escalation_level) .map(oncall => oncall.user); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index dce1bb536a..9a6e9a3ed2 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -91,7 +91,7 @@ const service: Service = { const mockPagerDutyApi: Partial = { getServiceByEntity: async () => ({ service }), - getOnCallByPolicyId: async () => [], + getOnCallByPolicyId: async () => ({ oncalls: [] }), getIncidentsByServiceId: async () => ({ incidents: [] }), }; From 0be087f637a58c382930e26d9f3b3b7c72e1b52e Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 17:07:32 -0700 Subject: [PATCH 30/39] fix(plugins/pagerduty): define responses types before utilizing them Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.ts | 1 - plugins/pagerduty/src/api/types.ts | 40 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 7f9f3d6c59..ebf350add2 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Service, Incident, ChangeEvent, OnCall } from '../components/types'; import { PagerDutyApi, TriggerAlarmRequest, diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index d2ab0290d3..ed6c1f222a 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -18,6 +18,26 @@ import { Incident, ChangeEvent, OnCall, Service } from '../components/types'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { PagerDutyEntity } from '../types'; +export type ServicesResponse = { + services: Service[]; +}; + +export type ServiceResponse = { + service: Service; +}; + +export type IncidentsResponse = { + incidents: Incident[]; +}; + +export type ChangeEventsResponse = { + change_events: ChangeEvent[]; +}; + +export type OnCallsResponse = { + oncalls: OnCall[]; +}; + export type TriggerAlarmRequest = { integrationKey: string; source: string; @@ -58,26 +78,6 @@ export interface PagerDutyApi { triggerAlarm(request: TriggerAlarmRequest): Promise; } -export type ServicesResponse = { - services: Service[]; -}; - -export type ServiceResponse = { - service: Service; -}; - -export type IncidentsResponse = { - incidents: Incident[]; -}; - -export type ChangeEventsResponse = { - change_events: ChangeEvent[]; -}; - -export type OnCallsResponse = { - oncalls: OnCall[]; -}; - export type ClientApiDependencies = { discoveryApi: DiscoveryApi; fetchApi: FetchApi; From f22f6050b4fcfada1823953afa83887fd752a748 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 17:31:01 -0700 Subject: [PATCH 31/39] feat(plugins/pagerduty): ensure proper types are exported and regenerate api-report Signed-off-by: Alec Jacobs --- plugins/pagerduty/api-report.md | 170 +++++++++++++++++++--- plugins/pagerduty/src/api/index.ts | 10 +- plugins/pagerduty/src/components/index.ts | 32 ++++ plugins/pagerduty/src/index.ts | 16 +- 4 files changed, 197 insertions(+), 31 deletions(-) create mode 100644 plugins/pagerduty/src/components/index.ts diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index c708a1ca4e..dd6155521b 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -13,11 +13,88 @@ import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; +// Warning: (ae-missing-release-tag) "Assignee" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Assignee = { + id: string; + summary: string; + html_url: string; +}; + +// Warning: (ae-missing-release-tag) "ChangeEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ChangeEvent = { + id: string; + integration: [ + { + service: Service; + }, + ]; + source: string; + html_url: string; + links: [ + { + href: string; + text: string; + }, + ]; + summary: string; + timestamp: string; +}; + +// Warning: (ae-missing-release-tag) "ChangeEventsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ChangeEventsResponse = { + change_events: ChangeEvent[]; +}; + +// Warning: (ae-missing-release-tag) "ClientApiConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ClientApiConfig = ClientApiDependencies & { + eventsBaseUrl?: string; +}; + +// Warning: (ae-missing-release-tag) "ClientApiDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ClientApiDependencies = { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; +}; + // Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const EntityPagerDutyCard: () => JSX.Element; +// Warning: (ae-missing-release-tag) "Incident" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Incident = { + id: string; + title: string; + status: string; + html_url: string; + assignments: [ + { + assignee: Assignee; + }, + ]; + serviceId: string; + created_at: string; +}; + +// Warning: (ae-missing-release-tag) "IncidentsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type IncidentsResponse = { + incidents: Incident[]; +}; + // Warning: (ae-missing-release-tag) "isPluginApplicableToEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -25,6 +102,21 @@ const isPluginApplicableToEntity: (entity: Entity) => boolean; export { isPluginApplicableToEntity as isPagerDutyAvailable }; export { isPluginApplicableToEntity }; +// Warning: (ae-missing-release-tag) "OnCall" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OnCall = { + user: User; + escalation_level: number; +}; + +// Warning: (ae-missing-release-tag) "OnCallsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OnCallsResponse = { + oncalls: OnCall[]; +}; + // Warning: (ae-forgotten-export) The symbol "PagerDutyApi" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "pagerDutyApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -40,38 +132,35 @@ export const PagerDutyCard: () => JSX.Element; // // @public (undocumented) export class PagerDutyClient implements PagerDutyApi { - // Warning: (ae-forgotten-export) The symbol "ClientApiConfig" needs to be exported by the entry point index.d.ts constructor(config: ClientApiConfig); // (undocumented) static fromConfig( configApi: ConfigApi, - discoveryApi: DiscoveryApi, - fetchApi: FetchApi, + { discoveryApi, fetchApi }: ClientApiDependencies, ): PagerDutyClient; - // Warning: (ae-forgotten-export) The symbol "ChangeEvent" needs to be exported by the entry point index.d.ts - // // (undocumented) - getChangeEventsByServiceId(serviceId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "Incident" needs to be exported by the entry point index.d.ts - // + getChangeEventsByServiceId(serviceId: string): Promise; // (undocumented) - getIncidentsByServiceId(serviceId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "OnCall" needs to be exported by the entry point index.d.ts - // + getIncidentsByServiceId(serviceId: string): Promise; // (undocumented) - getOnCallByPolicyId(policyId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "Service" needs to be exported by the entry point index.d.ts - // + getOnCallByPolicyId(policyId: string): Promise; // (undocumented) - getServiceByIntegrationKey(integrationKey: string): Promise; - // (undocumented) - getServiceByServiceId(serviceId: string): Promise; - // Warning: (ae-forgotten-export) The symbol "TriggerAlarmRequest" needs to be exported by the entry point index.d.ts - // + getServiceByEntity( + pagerDutyEntity: PagerDutyEntity, + ): Promise; // (undocumented) triggerAlarm(request: TriggerAlarmRequest): Promise; } +// Warning: (ae-missing-release-tag) "PagerDutyEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyEntity = { + integrationKey?: string; + serviceId?: string; + name: string; +}; + // Warning: (ae-missing-release-tag) "pagerDutyPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -79,6 +168,38 @@ const pagerDutyPlugin: BackstagePlugin<{}, {}>; export { pagerDutyPlugin }; export { pagerDutyPlugin as plugin }; +// Warning: (ae-missing-release-tag) "Service" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Service = { + id: string; + name: string; + html_url: string; + integrationKey: string; + escalation_policy: { + id: string; + user: User; + html_url: string; + }; +}; + +// Warning: (ae-missing-release-tag) "ServiceResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ServiceResponse = { + service: Service; +}; + +// Warning: (ae-missing-release-tag) "TriggerAlarmRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TriggerAlarmRequest = { + integrationKey: string; + source: string; + description: string; + userName: string; +}; + // Warning: (ae-forgotten-export) The symbol "TriggerButtonProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TriggerButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -89,4 +210,15 @@ export function TriggerButton(props: TriggerButtonProps): JSX.Element; // // @public (undocumented) export class UnauthorizedError extends Error {} + +// Warning: (ae-missing-release-tag) "User" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type User = { + id: string; + summary: string; + email: string; + html_url: string; + name: string; +}; ``` diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts index 015204b1e8..a88137b979 100644 --- a/plugins/pagerduty/src/api/index.ts +++ b/plugins/pagerduty/src/api/index.ts @@ -15,4 +15,12 @@ */ export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client'; -export type { PagerDutyApi } from './types'; +export type { + ServiceResponse, + IncidentsResponse, + ChangeEventsResponse, + OnCallsResponse, + TriggerAlarmRequest, + ClientApiDependencies, + ClientApiConfig, +} from './types'; diff --git a/plugins/pagerduty/src/components/index.ts b/plugins/pagerduty/src/components/index.ts new file mode 100644 index 0000000000..a452c12e12 --- /dev/null +++ b/plugins/pagerduty/src/components/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 type { + ChangeEvent, + Incident, + Service, + OnCall, + Assignee, + User, +} from './types'; + +export { + isPluginApplicableToEntity, + isPluginApplicableToEntity as isPagerDutyAvailable, + PagerDutyCard, +} from './PagerDutyCard'; + +export { TriggerButton } from './TriggerButton'; diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 8c970c01bb..2e0eddc9a8 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -25,14 +25,8 @@ export { pagerDutyPlugin as plugin, EntityPagerDutyCard, } from './plugin'; -export { - isPluginApplicableToEntity, - isPluginApplicableToEntity as isPagerDutyAvailable, - PagerDutyCard, -} from './components/PagerDutyCard'; -export { TriggerButton } from './components/TriggerButton'; -export { - PagerDutyClient, - pagerDutyApiRef, - UnauthorizedError, -} from './api/client'; + +export * from './components'; +export * from './api'; + +export type { PagerDutyEntity } from './types'; From 24511a3beb3f23959d0025cda1b618c163ca96f2 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 3 Jun 2022 17:42:49 -0700 Subject: [PATCH 32/39] chore: enhance changeset definition to call out all new breaking changes Signed-off-by: Alec Jacobs --- .changeset/weak-bananas-deliver.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.changeset/weak-bananas-deliver.md b/.changeset/weak-bananas-deliver.md index db76f396a8..2be1e87d29 100644 --- a/.changeset/weak-bananas-deliver.md +++ b/.changeset/weak-bananas-deliver.md @@ -5,9 +5,19 @@ Introduces a new annotation `pagerduty.com/service-id` that can be used instead of the `pagerduty.com/integration-key` annotation. _Note: If both annotations are specified on a given Entity, then the `pagerduty.com/integration-key` annotation will be prefered_ -**BREAKING** The `PagerDutyClient.fromConfig` static method now expects a `FetchApi` compatible object as a third argument. +**BREAKING** The `PagerDutyClient.fromConfig` static method now expects a `FetchApi` compatible object and has been refactored to +accept 2 arguments: config and ClientApiDependencies The `PagerDutyClient` now relies on a `fetchApi` being available to execute `fetch` requests. +**BREAKING** A new query method `getServiceByEntity` that is used to query for Services by either the `integrationKey` or `serviceId` +annotation values if they are defined. The `integrationKey` value is preferred currently over `serviceId`. As such, the previous +`getServiceByIntegrationKey` method has been removed. + +**BREAKING** The return values for each Client query method has been changed to return an object instead of raw values. +For example, the `getIncidentsByServiceId` query method now returns an object in the shape of `{ incidents: Incident[] }` +instead of just `Incident[]`. +This same pattern goes for `getChangeEventsByServiceId` and `getOnCallByPolicyId` functions. + In addition, various enhancements/bug fixes were introduced: - The `PagerDutyCard` component now wraps error and loading messages with an `InfoCard` to contain errors/messages. This enforces a consistent experience on the EntityPage From 8ac5b73da2a534632bd9b88ed9b03a8165f0a195 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 08:37:28 -0700 Subject: [PATCH 33/39] feat(plugins/pagerduty): prefix exported types with PagerDuty * generate api-report.md * ensure generic exported types are prefixed with PagerDuty for ease of import by consumers Signed-off-by: Alec Jacobs --- plugins/pagerduty/api-report.md | 226 +++++++++--------- plugins/pagerduty/src/api/client.test.ts | 6 +- plugins/pagerduty/src/api/client.ts | 46 ++-- plugins/pagerduty/src/api/index.ts | 14 +- plugins/pagerduty/src/api/types.ts | 47 ++-- .../ChangeEvents/ChangeEventListItem.tsx | 4 +- .../ChangeEvents/ChangeEvents.test.tsx | 6 +- .../components/Escalation/Escalation.test.tsx | 4 +- .../components/Escalation/EscalationUser.tsx | 4 +- .../components/Incident/IncidentListItem.tsx | 4 +- .../components/Incident/Incidents.test.tsx | 4 +- .../components/PagerDutyCard/index.test.tsx | 6 +- plugins/pagerduty/src/components/index.ts | 12 +- plugins/pagerduty/src/components/types.ts | 20 +- 14 files changed, 210 insertions(+), 193 deletions(-) diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index dd6155521b..04cda3da91 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -13,23 +13,46 @@ import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; -// Warning: (ae-missing-release-tag) "Assignee" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type Assignee = { +export const EntityPagerDutyCard: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "isPluginApplicableToEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const isPluginApplicableToEntity: (entity: Entity) => boolean; +export { isPluginApplicableToEntity as isPagerDutyAvailable }; +export { isPluginApplicableToEntity }; + +// Warning: (ae-forgotten-export) The symbol "PagerDutyApi" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "pagerDutyApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const pagerDutyApiRef: ApiRef; + +// Warning: (ae-missing-release-tag) "PagerDutyAssignee" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyAssignee = { id: string; summary: string; html_url: string; }; -// Warning: (ae-missing-release-tag) "ChangeEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "PagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ChangeEvent = { +export const PagerDutyCard: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "PagerDutyChangeEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyChangeEvent = { id: string; integration: [ { - service: Service; + service: PagerDutyService; }, ]; source: string; @@ -44,114 +67,56 @@ export type ChangeEvent = { timestamp: string; }; -// Warning: (ae-missing-release-tag) "ChangeEventsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "PagerDutyChangeEventsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ChangeEventsResponse = { - change_events: ChangeEvent[]; +export type PagerDutyChangeEventsResponse = { + change_events: PagerDutyChangeEvent[]; }; -// Warning: (ae-missing-release-tag) "ClientApiConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ClientApiConfig = ClientApiDependencies & { - eventsBaseUrl?: string; -}; - -// Warning: (ae-missing-release-tag) "ClientApiDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ClientApiDependencies = { - discoveryApi: DiscoveryApi; - fetchApi: FetchApi; -}; - -// Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityPagerDutyCard: () => JSX.Element; - -// Warning: (ae-missing-release-tag) "Incident" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type Incident = { - id: string; - title: string; - status: string; - html_url: string; - assignments: [ - { - assignee: Assignee; - }, - ]; - serviceId: string; - created_at: string; -}; - -// Warning: (ae-missing-release-tag) "IncidentsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IncidentsResponse = { - incidents: Incident[]; -}; - -// Warning: (ae-missing-release-tag) "isPluginApplicableToEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -const isPluginApplicableToEntity: (entity: Entity) => boolean; -export { isPluginApplicableToEntity as isPagerDutyAvailable }; -export { isPluginApplicableToEntity }; - -// Warning: (ae-missing-release-tag) "OnCall" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type OnCall = { - user: User; - escalation_level: number; -}; - -// Warning: (ae-missing-release-tag) "OnCallsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type OnCallsResponse = { - oncalls: OnCall[]; -}; - -// Warning: (ae-forgotten-export) The symbol "PagerDutyApi" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "pagerDutyApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const pagerDutyApiRef: ApiRef; - -// Warning: (ae-missing-release-tag) "PagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const PagerDutyCard: () => JSX.Element; - // Warning: (ae-missing-release-tag) "PagerDutyClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export class PagerDutyClient implements PagerDutyApi { - constructor(config: ClientApiConfig); + constructor(config: PagerDutyClientApiConfig); // (undocumented) static fromConfig( configApi: ConfigApi, - { discoveryApi, fetchApi }: ClientApiDependencies, + { discoveryApi, fetchApi }: PagerDutyClientApiDependencies, ): PagerDutyClient; // (undocumented) - getChangeEventsByServiceId(serviceId: string): Promise; + getChangeEventsByServiceId( + serviceId: string, + ): Promise; // (undocumented) - getIncidentsByServiceId(serviceId: string): Promise; + getIncidentsByServiceId( + serviceId: string, + ): Promise; // (undocumented) - getOnCallByPolicyId(policyId: string): Promise; + getOnCallByPolicyId(policyId: string): Promise; // (undocumented) getServiceByEntity( pagerDutyEntity: PagerDutyEntity, - ): Promise; + ): Promise; // (undocumented) - triggerAlarm(request: TriggerAlarmRequest): Promise; + triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise; } +// Warning: (ae-missing-release-tag) "PagerDutyClientApiConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyClientApiConfig = PagerDutyClientApiDependencies & { + eventsBaseUrl?: string; +}; + +// Warning: (ae-missing-release-tag) "PagerDutyClientApiDependencies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyClientApiDependencies = { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; +}; + // Warning: (ae-missing-release-tag) "PagerDutyEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -161,6 +126,45 @@ export type PagerDutyEntity = { name: string; }; +// Warning: (ae-missing-release-tag) "PagerDutyIncident" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyIncident = { + id: string; + title: string; + status: string; + html_url: string; + assignments: [ + { + assignee: PagerDutyAssignee; + }, + ]; + serviceId: string; + created_at: string; +}; + +// Warning: (ae-missing-release-tag) "PagerDutyIncidentsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyIncidentsResponse = { + incidents: PagerDutyIncident[]; +}; + +// Warning: (ae-missing-release-tag) "PagerDutyOnCall" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyOnCall = { + user: PagerDutyUser; + escalation_level: number; +}; + +// Warning: (ae-missing-release-tag) "PagerDutyOnCallsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyOnCallsResponse = { + oncalls: PagerDutyOnCall[]; +}; + // Warning: (ae-missing-release-tag) "pagerDutyPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -168,38 +172,49 @@ const pagerDutyPlugin: BackstagePlugin<{}, {}>; export { pagerDutyPlugin }; export { pagerDutyPlugin as plugin }; -// Warning: (ae-missing-release-tag) "Service" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "PagerDutyService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type Service = { +export type PagerDutyService = { id: string; name: string; html_url: string; integrationKey: string; escalation_policy: { id: string; - user: User; + user: PagerDutyUser; html_url: string; }; }; -// Warning: (ae-missing-release-tag) "ServiceResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "PagerDutyServiceResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ServiceResponse = { - service: Service; +export type PagerDutyServiceResponse = { + service: PagerDutyService; }; -// Warning: (ae-missing-release-tag) "TriggerAlarmRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "PagerDutyTriggerAlarmRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type TriggerAlarmRequest = { +export type PagerDutyTriggerAlarmRequest = { integrationKey: string; source: string; description: string; userName: string; }; +// Warning: (ae-missing-release-tag) "PagerDutyUser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PagerDutyUser = { + id: string; + summary: string; + email: string; + html_url: string; + name: string; +}; + // Warning: (ae-forgotten-export) The symbol "TriggerButtonProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "TriggerButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -210,15 +225,4 @@ export function TriggerButton(props: TriggerButtonProps): JSX.Element; // // @public (undocumented) export class UnauthorizedError extends Error {} - -// Warning: (ae-missing-release-tag) "User" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type User = { - id: string; - summary: string; - email: string; - html_url: string; - name: string; -}; ``` diff --git a/plugins/pagerduty/src/api/client.test.ts b/plugins/pagerduty/src/api/client.test.ts index d3e9b58542..2e9efefb18 100644 --- a/plugins/pagerduty/src/api/client.test.ts +++ b/plugins/pagerduty/src/api/client.test.ts @@ -17,7 +17,7 @@ import { MockFetchApi } from '@backstage/test-utils'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { PagerDutyClient, UnauthorizedError } from './client'; import { PagerDutyEntity } from '../types'; -import { Service, User } from '../components/types'; +import { PagerDutyService, PagerDutyUser } from '../components/types'; import { NotFoundError } from '@backstage/errors'; const mockFetch = jest.fn().mockName('fetch'); @@ -34,7 +34,7 @@ const mockFetchApi: MockFetchApi = new MockFetchApi({ let client: PagerDutyClient; let pagerDutyEntity: PagerDutyEntity; -const user: User = { +const user: PagerDutyUser = { name: 'person1', id: 'p1', summary: 'person1', @@ -42,7 +42,7 @@ const user: User = { html_url: 'http://a.com/id1', }; -const service: Service = { +const service: PagerDutyService = { id: 'def456', name: 'pagerduty-name', html_url: 'www.example.com', diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index ebf350add2..dbb4248f0f 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -16,15 +16,15 @@ import { PagerDutyApi, - TriggerAlarmRequest, - ServicesResponse, - ServiceResponse, - IncidentsResponse, - OnCallsResponse, - ClientApiDependencies, - ClientApiConfig, + PagerDutyTriggerAlarmRequest, + PagerDutyServicesResponse, + PagerDutyServiceResponse, + PagerDutyIncidentsResponse, + PagerDutyOnCallsResponse, + PagerDutyClientApiDependencies, + PagerDutyClientApiConfig, RequestOptions, - ChangeEventsResponse, + PagerDutyChangeEventsResponse, } from './types'; import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; @@ -42,7 +42,7 @@ const commonGetServiceParams = export class PagerDutyClient implements PagerDutyApi { static fromConfig( configApi: ConfigApi, - { discoveryApi, fetchApi }: ClientApiDependencies, + { discoveryApi, fetchApi }: PagerDutyClientApiDependencies, ) { const eventsBaseUrl: string = configApi.getOptionalString('pagerDuty.eventsBaseUrl') ?? @@ -54,21 +54,21 @@ export class PagerDutyClient implements PagerDutyApi { fetchApi, }); } - constructor(private readonly config: ClientApiConfig) {} + constructor(private readonly config: PagerDutyClientApiConfig) {} async getServiceByEntity( pagerDutyEntity: PagerDutyEntity, - ): Promise { + ): Promise { const { integrationKey, serviceId } = pagerDutyEntity; - let response: ServiceResponse; + let response: PagerDutyServiceResponse; let url: string; if (integrationKey) { url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/services?${commonGetServiceParams}&query=${integrationKey}`; - const { services } = await this.getByUrl(url); + const { services } = await this.getByUrl(url); const service = services[0]; if (!service) throw new NotFoundError(); @@ -79,7 +79,7 @@ export class PagerDutyClient implements PagerDutyApi { 'proxy', )}/pagerduty/services/${serviceId}?${commonGetServiceParams}`; - response = await this.getByUrl(url); + response = await this.getByUrl(url); } else { throw new NotFoundError(); } @@ -87,36 +87,40 @@ export class PagerDutyClient implements PagerDutyApi { return response; } - async getIncidentsByServiceId(serviceId: string): Promise { + async getIncidentsByServiceId( + serviceId: string, + ): Promise { const params = `time_zone=UTC&sort_by=created_at&statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/incidents?${params}`; - return await this.getByUrl(url); + return await this.getByUrl(url); } async getChangeEventsByServiceId( serviceId: string, - ): Promise { + ): Promise { const params = `limit=5&time_zone=UTC&sort_by=timestamp`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/services/${serviceId}/change_events?${params}`; - return await this.getByUrl(url); + return await this.getByUrl(url); } - async getOnCallByPolicyId(policyId: string): Promise { + async getOnCallByPolicyId( + policyId: string, + ): Promise { const params = `time_zone=UTC&include[]=users&escalation_policy_ids[]=${policyId}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/oncalls?${params}`; - return await this.getByUrl(url); + return await this.getByUrl(url); } - triggerAlarm(request: TriggerAlarmRequest): Promise { + triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise { const { integrationKey, source, description, userName } = request; const body = JSON.stringify({ diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts index a88137b979..9a0c08973d 100644 --- a/plugins/pagerduty/src/api/index.ts +++ b/plugins/pagerduty/src/api/index.ts @@ -16,11 +16,11 @@ export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client'; export type { - ServiceResponse, - IncidentsResponse, - ChangeEventsResponse, - OnCallsResponse, - TriggerAlarmRequest, - ClientApiDependencies, - ClientApiConfig, + PagerDutyServiceResponse, + PagerDutyIncidentsResponse, + PagerDutyChangeEventsResponse, + PagerDutyOnCallsResponse, + PagerDutyTriggerAlarmRequest, + PagerDutyClientApiDependencies, + PagerDutyClientApiConfig, } from './types'; diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index ed6c1f222a..5163718154 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -14,31 +14,36 @@ * limitations under the License. */ -import { Incident, ChangeEvent, OnCall, Service } from '../components/types'; +import { + PagerDutyIncident, + PagerDutyChangeEvent, + PagerDutyOnCall, + PagerDutyService, +} from '../components/types'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { PagerDutyEntity } from '../types'; -export type ServicesResponse = { - services: Service[]; +export type PagerDutyServicesResponse = { + services: PagerDutyService[]; }; -export type ServiceResponse = { - service: Service; +export type PagerDutyServiceResponse = { + service: PagerDutyService; }; -export type IncidentsResponse = { - incidents: Incident[]; +export type PagerDutyIncidentsResponse = { + incidents: PagerDutyIncident[]; }; -export type ChangeEventsResponse = { - change_events: ChangeEvent[]; +export type PagerDutyChangeEventsResponse = { + change_events: PagerDutyChangeEvent[]; }; -export type OnCallsResponse = { - oncalls: OnCall[]; +export type PagerDutyOnCallsResponse = { + oncalls: PagerDutyOnCall[]; }; -export type TriggerAlarmRequest = { +export type PagerDutyTriggerAlarmRequest = { integrationKey: string; source: string; description: string; @@ -52,38 +57,42 @@ export interface PagerDutyApi { */ getServiceByEntity( pagerDutyEntity: PagerDutyEntity, - ): Promise; + ): Promise; /** * Fetches a list of incidents a provided service has. * */ - getIncidentsByServiceId(serviceId: string): Promise; + getIncidentsByServiceId( + serviceId: string, + ): Promise; /** * Fetches a list of change events a provided service has. * */ - getChangeEventsByServiceId(serviceId: string): Promise; + getChangeEventsByServiceId( + serviceId: string, + ): Promise; /** * Fetches the list of users in an escalation policy. * */ - getOnCallByPolicyId(policyId: string): Promise; + getOnCallByPolicyId(policyId: string): Promise; /** * Triggers an incident to whoever is on-call. */ - triggerAlarm(request: TriggerAlarmRequest): Promise; + triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise; } -export type ClientApiDependencies = { +export type PagerDutyClientApiDependencies = { discoveryApi: DiscoveryApi; fetchApi: FetchApi; }; -export type ClientApiConfig = ClientApiDependencies & { +export type PagerDutyClientApiConfig = PagerDutyClientApiDependencies & { eventsBaseUrl?: string; }; diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx index 4912277cee..505d58eabb 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx @@ -26,7 +26,7 @@ import { Typography, } from '@material-ui/core'; import { DateTime, Duration } from 'luxon'; -import { ChangeEvent } from '../types'; +import { PagerDutyChangeEvent } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { BackstageTheme } from '@backstage/theme'; @@ -44,7 +44,7 @@ const useStyles = makeStyles({ }); type Props = { - changeEvent: ChangeEvent; + changeEvent: PagerDutyChangeEvent; }; export const ChangeEventListItem = ({ changeEvent }: Props) => { diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx index 017e394de0..a518e0caea 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { render, waitFor } from '@testing-library/react'; -import { ChangeEvent } from '../types'; +import { PagerDutyChangeEvent } from '../types'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { ApiProvider } from '@backstage/core-app-api'; @@ -74,7 +74,7 @@ describe('Incidents', () => { summary: 'sum of EVENT', timestamp: '2020-07-18T08:42:58.315+0000', }, - ] as ChangeEvent[], + ] as PagerDutyChangeEvent[], })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( @@ -121,7 +121,7 @@ describe('Incidents', () => { summary: 'sum of EVENT', timestamp: '2020-07-18T08:42:58.315+0000', }, - ] as ChangeEvent[], + ] as PagerDutyChangeEvent[], })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx index 33e8865f59..97c02a37f3 100644 --- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx +++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; -import { User } from '../types'; +import { PagerDutyUser } from '../types'; import { pagerDutyApiRef } from '../../api'; import { ApiProvider } from '@backstage/core-app-api'; @@ -57,7 +57,7 @@ describe('Escalation', () => { summary: 'person1', email: 'person1@example.com', html_url: 'http://a.com/id1', - } as User, + } as PagerDutyUser, }, ], })); diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx index a460906d22..d418ad02c0 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx @@ -28,7 +28,7 @@ import { import Avatar from '@material-ui/core/Avatar'; import EmailIcon from '@material-ui/icons/Email'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; -import { User } from '../types'; +import { PagerDutyUser } from '../types'; const useStyles = makeStyles({ listItemPrimary: { @@ -37,7 +37,7 @@ const useStyles = makeStyles({ }); type Props = { - user: User; + user: PagerDutyUser; }; export const EscalationUser = ({ user }: Props) => { diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 807685a83e..5945c45e83 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -29,7 +29,7 @@ import { import Done from '@material-ui/icons/Done'; import Warning from '@material-ui/icons/Warning'; import { DateTime, Duration } from 'luxon'; -import { Incident } from '../types'; +import { PagerDutyIncident } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { BackstageTheme } from '@backstage/theme'; @@ -61,7 +61,7 @@ const useStyles = makeStyles(theme => ({ })); type Props = { - incident: Incident; + incident: PagerDutyIncident; }; export const IncidentListItem = ({ incident }: Props) => { diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index ad69bc083e..a83874a995 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -18,7 +18,7 @@ import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; -import { Incident } from '../types'; +import { PagerDutyIncident } from '../types'; import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi = { @@ -83,7 +83,7 @@ describe('Incidents', () => { html_url: 'http://a.com/id2', serviceId: 'sId2', }, - ] as Incident[], + ] as PagerDutyIncident[], })); const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 9a6e9a3ed2..85202a9cfc 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -21,7 +21,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; -import { Service, User } from '../types'; +import { PagerDutyService, PagerDutyUser } from '../types'; import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; @@ -69,7 +69,7 @@ const entityWithAllAnnotations: Entity = { }, }; -const user: User = { +const user: PagerDutyUser = { name: 'person1', id: 'p1', summary: 'person1', @@ -77,7 +77,7 @@ const user: User = { html_url: 'http://a.com/id1', }; -const service: Service = { +const service: PagerDutyService = { id: 'def456', name: 'pagerduty-name', html_url: 'www.example.com', diff --git a/plugins/pagerduty/src/components/index.ts b/plugins/pagerduty/src/components/index.ts index a452c12e12..17bb2c03dc 100644 --- a/plugins/pagerduty/src/components/index.ts +++ b/plugins/pagerduty/src/components/index.ts @@ -15,12 +15,12 @@ */ export type { - ChangeEvent, - Incident, - Service, - OnCall, - Assignee, - User, + PagerDutyChangeEvent, + PagerDutyIncident, + PagerDutyService, + PagerDutyOnCall, + PagerDutyAssignee, + PagerDutyUser, } from './types'; export { diff --git a/plugins/pagerduty/src/components/types.ts b/plugins/pagerduty/src/components/types.ts index d0d05d82d1..89cdffea16 100644 --- a/plugins/pagerduty/src/components/types.ts +++ b/plugins/pagerduty/src/components/types.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -export type ChangeEvent = { +export type PagerDutyChangeEvent = { id: string; integration: [ { - service: Service; + service: PagerDutyService; }, ]; source: string; @@ -33,44 +33,44 @@ export type ChangeEvent = { timestamp: string; }; -export type Incident = { +export type PagerDutyIncident = { id: string; title: string; status: string; html_url: string; assignments: [ { - assignee: Assignee; + assignee: PagerDutyAssignee; }, ]; serviceId: string; created_at: string; }; -export type Service = { +export type PagerDutyService = { id: string; name: string; html_url: string; integrationKey: string; escalation_policy: { id: string; - user: User; + user: PagerDutyUser; html_url: string; }; }; -export type OnCall = { - user: User; +export type PagerDutyOnCall = { + user: PagerDutyUser; escalation_level: number; }; -export type Assignee = { +export type PagerDutyAssignee = { id: string; summary: string; html_url: string; }; -export type User = { +export type PagerDutyUser = { id: string; summary: string; email: string; From 7889629264d2d3c6b31da00496bd8be9aa6c2cf5 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 08:39:59 -0700 Subject: [PATCH 34/39] fix(plugins/pagerduty): properly type optional variable Signed-off-by: Alec Jacobs --- .../src/components/ChangeEvents/ChangeEventListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx index 505d58eabb..7b531bf1de 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEventListItem.tsx @@ -54,7 +54,7 @@ export const ChangeEventListItem = ({ changeEvent }: Props) => { const changedAt = DateTime.local() .minus(Duration.fromMillis(duration)) .toRelative({ locale: 'en' }); - let externalLinkElem = null; + let externalLinkElem: JSX.Element | undefined; if (changeEvent.links.length > 0) { const text: string = changeEvent.links[0].text; externalLinkElem = ( From ce89b1801ef25fd6052135d68b4e175a9b0499e2 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 09:28:19 -0700 Subject: [PATCH 35/39] feat(plugins/pagerduty): add pagerDutyEntity helper * parse a PagerDutyEntity from a Backstage Entity Signed-off-by: Alec Jacobs --- .../src/components/pagerDutyEntity.test.ts | 98 +++++++++++++++++++ .../src/components/pagerDutyEntity.ts | 29 ++++++ 2 files changed, 127 insertions(+) create mode 100644 plugins/pagerduty/src/components/pagerDutyEntity.test.ts create mode 100644 plugins/pagerduty/src/components/pagerDutyEntity.ts diff --git a/plugins/pagerduty/src/components/pagerDutyEntity.test.ts b/plugins/pagerduty/src/components/pagerDutyEntity.test.ts new file mode 100644 index 0000000000..cf551d3d6e --- /dev/null +++ b/plugins/pagerduty/src/components/pagerDutyEntity.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { Entity } from '@backstage/catalog-model'; +import { getPagerDutyEntity } from './pagerDutyEntity'; + +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + }, + }, +}; + +const entityWithoutAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: {}, + }, +}; + +const entityWithServiceId: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + +const entityWithAllAnnotations: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + 'pagerduty.com/service-id': 'def456', + }, + }, +}; + +describe('getPagerDutyEntity', () => { + describe('when entity has no annotations', () => { + it('returns a PagerDutyEntity', () => { + expect(getPagerDutyEntity(entityWithoutAnnotations)).toEqual({ + name: 'pagerduty-test', + }); + }); + }); + + describe('when entity has integration key annotation', () => { + it('returns a PagerDutyEntity', () => { + expect(getPagerDutyEntity(entity)).toEqual({ + name: 'pagerduty-test', + integrationKey: 'abc123', + }); + }); + }); + + describe('when entity has service id annotation', () => { + it('returns a PagerDutyEntity', () => { + expect(getPagerDutyEntity(entityWithServiceId)).toEqual({ + name: 'pagerduty-test', + serviceId: 'def456', + }); + }); + }); + + describe('when entity has all annotations', () => { + it('returns a PagerDutyEntity', () => { + expect(getPagerDutyEntity(entityWithAllAnnotations)).toEqual({ + name: 'pagerduty-test', + integrationKey: 'abc123', + serviceId: 'def456', + }); + }); + }); +}); diff --git a/plugins/pagerduty/src/components/pagerDutyEntity.ts b/plugins/pagerduty/src/components/pagerDutyEntity.ts new file mode 100644 index 0000000000..950525881e --- /dev/null +++ b/plugins/pagerduty/src/components/pagerDutyEntity.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { Entity } from '@backstage/catalog-model'; +import { PagerDutyEntity } from '../types'; +import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from './constants'; + +export function getPagerDutyEntity(entity: Entity): PagerDutyEntity { + const { + [PAGERDUTY_INTEGRATION_KEY]: integrationKey, + [PAGERDUTY_SERVICE_ID]: serviceId, + } = entity.metadata.annotations || ({} as Record); + const name = entity.metadata.name; + + return { integrationKey, serviceId, name }; +} From fb9b81780aec0367a368c8ce692846ca6c5a0ad0 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 09:29:45 -0700 Subject: [PATCH 36/39] feat(plugins/pagerduty): use getPagerDutyEntity helper in hook Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/hooks/index.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/plugins/pagerduty/src/hooks/index.ts b/plugins/pagerduty/src/hooks/index.ts index 503597a69e..8950e443e2 100644 --- a/plugins/pagerduty/src/hooks/index.ts +++ b/plugins/pagerduty/src/hooks/index.ts @@ -16,19 +16,10 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { PagerDutyEntity } from '../types'; - -import { - PAGERDUTY_INTEGRATION_KEY, - PAGERDUTY_SERVICE_ID, -} from '../components/constants'; +import { getPagerDutyEntity } from '../components/pagerDutyEntity'; export function usePagerdutyEntity(): PagerDutyEntity { const { entity } = useEntity(); - const { - [PAGERDUTY_INTEGRATION_KEY]: integrationKey, - [PAGERDUTY_SERVICE_ID]: serviceId, - } = entity.metadata.annotations || ({} as Record); - const name = entity.metadata.name; - return { integrationKey, serviceId, name }; + return getPagerDutyEntity(entity); } From a9b6b729c48e6bdafc8099ad2ccdead858f0b746 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 14:39:46 -0700 Subject: [PATCH 37/39] feat(plugins/pagerduty): refactor PagerDutyClient.getServiceByEntity * provide raw backstage entity Signed-off-by: Alec Jacobs --- plugins/pagerduty/src/api/client.test.ts | 123 ++++++++++-------- plugins/pagerduty/src/api/client.ts | 9 +- plugins/pagerduty/src/api/types.ts | 8 +- .../src/components/PagerDutyCard/index.tsx | 10 +- 4 files changed, 82 insertions(+), 68 deletions(-) diff --git a/plugins/pagerduty/src/api/client.test.ts b/plugins/pagerduty/src/api/client.test.ts index 2e9efefb18..a32a533e68 100644 --- a/plugins/pagerduty/src/api/client.test.ts +++ b/plugins/pagerduty/src/api/client.test.ts @@ -16,9 +16,9 @@ import { MockFetchApi } from '@backstage/test-utils'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { PagerDutyClient, UnauthorizedError } from './client'; -import { PagerDutyEntity } from '../types'; import { PagerDutyService, PagerDutyUser } from '../components/types'; import { NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; const mockFetch = jest.fn().mockName('fetch'); const mockDiscoveryApi: jest.Mocked = { @@ -32,7 +32,7 @@ const mockFetchApi: MockFetchApi = new MockFetchApi({ }); let client: PagerDutyClient; -let pagerDutyEntity: PagerDutyEntity; +let entity: Entity; const user: PagerDutyUser = { name: 'person1', @@ -76,9 +76,15 @@ describe('PagerDutyClient', () => { describe('getServiceByEntity', () => { describe('when provided entity has an integrationKey value', () => { beforeEach(() => { - pagerDutyEntity = { - name: 'pagerduty-test', - integrationKey: 'abc123', + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + }, + }, }; }); @@ -89,7 +95,7 @@ describe('PagerDutyClient', () => { json: () => Promise.resolve({ services: [service] }), }); - expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + expect(await client.getServiceByEntity(entity)).toEqual({ service, }); expect(mockFetch).toHaveBeenCalledWith( @@ -108,9 +114,9 @@ describe('PagerDutyClient', () => { }); it('throws UnauthorizedError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(UnauthorizedError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + UnauthorizedError, + ); }); }); @@ -124,9 +130,9 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); }); }); @@ -143,9 +149,7 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError( + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( 'Request failed with 500, Not valid request internal error occurred', ); }); @@ -161,18 +165,24 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); }); }); }); describe('when provided entity has a serviceId value', () => { beforeEach(() => { - pagerDutyEntity = { - name: 'pagerduty-test', - serviceId: 'def456', + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/service-id': 'def456', + }, + }, }; }); @@ -183,7 +193,7 @@ describe('PagerDutyClient', () => { json: () => Promise.resolve({ service }), }); - expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + expect(await client.getServiceByEntity(entity)).toEqual({ service, }); expect(mockFetch).toHaveBeenCalledWith( @@ -202,9 +212,9 @@ describe('PagerDutyClient', () => { }); it('throws UnauthorizedError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(UnauthorizedError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + UnauthorizedError, + ); }); }); @@ -218,9 +228,9 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); }); }); @@ -237,9 +247,7 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError( + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( 'Request failed with 500, Not valid request internal error occurred', ); }); @@ -248,10 +256,16 @@ describe('PagerDutyClient', () => { describe('when provided entity has both integrationKey and serviceId', () => { beforeEach(() => { - pagerDutyEntity = { - name: 'pagerduty-test', - integrationKey: 'abc123', - serviceId: 'def456', + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + 'pagerduty.com/service-id': 'def456', + }, + }, }; }); @@ -262,7 +276,7 @@ describe('PagerDutyClient', () => { json: () => Promise.resolve({ services: [service] }), }); - expect(await client.getServiceByEntity(pagerDutyEntity)).toEqual({ + expect(await client.getServiceByEntity(entity)).toEqual({ service, }); expect(mockFetch).toHaveBeenCalledWith( @@ -281,9 +295,9 @@ describe('PagerDutyClient', () => { }); it('throws UnauthorizedError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(UnauthorizedError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + UnauthorizedError, + ); }); }); @@ -297,9 +311,9 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); }); }); @@ -316,9 +330,7 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError( + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( 'Request failed with 500, Not valid request internal error occurred', ); }); @@ -334,24 +346,29 @@ describe('PagerDutyClient', () => { }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); }); }); }); describe('when provided entity has no integrationKey or serviceId values', () => { beforeEach(() => { - pagerDutyEntity = { - name: 'pagerduty-test', + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: {}, + }, }; }); it('throws NotFoundError', async () => { - await expect( - client.getServiceByEntity(pagerDutyEntity), - ).rejects.toThrowError(NotFoundError); + await expect(client.getServiceByEntity(entity)).rejects.toThrowError( + NotFoundError, + ); expect(mockFetch).not.toHaveBeenCalled(); }); }); diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index dbb4248f0f..69a702f66d 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -28,7 +28,8 @@ import { } from './types'; import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; -import { PagerDutyEntity } from '../types'; +import { Entity } from '@backstage/catalog-model'; +import { getPagerDutyEntity } from '../components/pagerDutyEntity'; export class UnauthorizedError extends Error {} @@ -56,10 +57,8 @@ export class PagerDutyClient implements PagerDutyApi { } constructor(private readonly config: PagerDutyClientApiConfig) {} - async getServiceByEntity( - pagerDutyEntity: PagerDutyEntity, - ): Promise { - const { integrationKey, serviceId } = pagerDutyEntity; + async getServiceByEntity(entity: Entity): Promise { + const { integrationKey, serviceId } = getPagerDutyEntity(entity); let response: PagerDutyServiceResponse; let url: string; diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 5163718154..786b0bdffd 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -21,7 +21,7 @@ import { PagerDutyService, } from '../components/types'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; -import { PagerDutyEntity } from '../types'; +import { Entity } from '@backstage/catalog-model'; export type PagerDutyServicesResponse = { services: PagerDutyService[]; @@ -52,12 +52,10 @@ export type PagerDutyTriggerAlarmRequest = { export interface PagerDutyApi { /** - * Fetches the service for the provided PagerDutyEntity. + * Fetches the service for the provided Entity. * */ - getServiceByEntity( - pagerDutyEntity: PagerDutyEntity, - ): Promise; + getServiceByEntity(entity: Entity): Promise; /** * Fetches a list of incidents a provided service has. diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 174caf4900..e8ba2c522c 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -25,7 +25,6 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { MissingTokenError, ServiceNotFoundError } from '../Errors'; import WebIcon from '@material-ui/icons/Web'; import DateRangeIcon from '@material-ui/icons/DateRange'; -import { usePagerdutyEntity } from '../../hooks'; import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants'; import { TriggerDialog } from '../TriggerDialog'; import { ChangeEvents } from '../ChangeEvents'; @@ -40,6 +39,8 @@ import { CardTab, InfoCard, } from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { getPagerDutyEntity } from '../pagerDutyEntity'; const BasicCard = ({ children }: { children: ReactNode }) => ( {children} @@ -52,7 +53,8 @@ export const isPluginApplicableToEntity = (entity: Entity) => ); export const PagerDutyCard = () => { - const pagerDutyEntity = usePagerdutyEntity(); + const { entity } = useEntity(); + const pagerDutyEntity = getPagerDutyEntity(entity); const api = useApi(pagerDutyApiRef); const [refreshIncidents, setRefreshIncidents] = useState(false); const [refreshChangeEvents, setRefreshChangeEvents] = @@ -76,9 +78,7 @@ export const PagerDutyCard = () => { loading, error, } = useAsync(async () => { - const { service: foundService } = await api.getServiceByEntity( - pagerDutyEntity, - ); + const { service: foundService } = await api.getServiceByEntity(entity); return { id: foundService.id, From 75813194a68085f54c22f55d63fee29129e9f2fd Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 14:42:48 -0700 Subject: [PATCH 38/39] chore(plugins/pagerduty): regenerate api-report Signed-off-by: Alec Jacobs --- plugins/pagerduty/api-report.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 04cda3da91..be3fa9fa23 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -95,9 +95,7 @@ export class PagerDutyClient implements PagerDutyApi { // (undocumented) getOnCallByPolicyId(policyId: string): Promise; // (undocumented) - getServiceByEntity( - pagerDutyEntity: PagerDutyEntity, - ): Promise; + getServiceByEntity(entity: Entity): Promise; // (undocumented) triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise; } From d0014de1e1acf8a69523acc5c53f37463bc6ef5a Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 17 Jun 2022 14:44:59 -0700 Subject: [PATCH 39/39] chore: enhance changeset definition for exposed types Signed-off-by: Alec Jacobs --- .changeset/weak-bananas-deliver.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/weak-bananas-deliver.md b/.changeset/weak-bananas-deliver.md index 2be1e87d29..f46c7b7bfe 100644 --- a/.changeset/weak-bananas-deliver.md +++ b/.changeset/weak-bananas-deliver.md @@ -18,6 +18,8 @@ For example, the `getIncidentsByServiceId` query method now returns an object in instead of just `Incident[]`. This same pattern goes for `getChangeEventsByServiceId` and `getOnCallByPolicyId` functions. +**BREAKING** All public exported types that relate to entities within PagerDuty have been prefixed with `PagerDuty` (e.g. `ServicesResponse` is now `PagerDutyServicesResponse` and `User` is now `PagerDutyUser`) + In addition, various enhancements/bug fixes were introduced: - The `PagerDutyCard` component now wraps error and loading messages with an `InfoCard` to contain errors/messages. This enforces a consistent experience on the EntityPage