Merge pull request #11697 from alecjacobs5401/ajacobs/pagerduty-plugin-service-id-annotation
feat(plugins/pagerduty): support pagerduty.com/service-id annotation
This commit is contained in:
@@ -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
|
||||
|
||||
+156
-20
@@ -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)
|
||||
@@ -31,45 +31,138 @@ export { isPluginApplicableToEntity };
|
||||
// @public (undocumented)
|
||||
export const pagerDutyApiRef: ApiRef<PagerDutyApi>;
|
||||
|
||||
// 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) "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) "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: PagerDutyService;
|
||||
},
|
||||
];
|
||||
source: string;
|
||||
html_url: string;
|
||||
links: [
|
||||
{
|
||||
href: string;
|
||||
text: string;
|
||||
},
|
||||
];
|
||||
summary: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
// 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 PagerDutyChangeEventsResponse = {
|
||||
change_events: PagerDutyChangeEvent[];
|
||||
};
|
||||
|
||||
// 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 {
|
||||
// Warning: (ae-forgotten-export) The symbol "ClientApiConfig" needs to be exported by the entry point index.d.ts
|
||||
constructor(config: ClientApiConfig);
|
||||
constructor(config: PagerDutyClientApiConfig);
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
configApi: ConfigApi,
|
||||
discoveryApi: DiscoveryApi,
|
||||
identityApi: IdentityApi,
|
||||
{ discoveryApi, fetchApi }: PagerDutyClientApiDependencies,
|
||||
): PagerDutyClient;
|
||||
// Warning: (ae-forgotten-export) The symbol "ChangeEvent" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
getChangeEventsByServiceId(serviceId: string): Promise<ChangeEvent[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "Incident" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
getChangeEventsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyChangeEventsResponse>;
|
||||
// (undocumented)
|
||||
getIncidentsByServiceId(serviceId: string): Promise<Incident[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "OnCall" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
getIncidentsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyIncidentsResponse>;
|
||||
// (undocumented)
|
||||
getOnCallByPolicyId(policyId: string): Promise<OnCall[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "Service" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
getOnCallByPolicyId(policyId: string): Promise<PagerDutyOnCallsResponse>;
|
||||
// (undocumented)
|
||||
getServiceByIntegrationKey(integrationKey: string): Promise<Service[]>;
|
||||
// Warning: (ae-forgotten-export) The symbol "TriggerAlarmRequest" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse>;
|
||||
// (undocumented)
|
||||
triggerAlarm(request: TriggerAlarmRequest): Promise<Response>;
|
||||
triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise<Response>;
|
||||
}
|
||||
|
||||
// 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)
|
||||
export type PagerDutyEntity = {
|
||||
integrationKey?: string;
|
||||
serviceId?: string;
|
||||
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)
|
||||
@@ -77,6 +170,49 @@ const pagerDutyPlugin: BackstagePlugin<{}, {}>;
|
||||
export { pagerDutyPlugin };
|
||||
export { pagerDutyPlugin as plugin };
|
||||
|
||||
// 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 PagerDutyService = {
|
||||
id: string;
|
||||
name: string;
|
||||
html_url: string;
|
||||
integrationKey: string;
|
||||
escalation_policy: {
|
||||
id: string;
|
||||
user: PagerDutyUser;
|
||||
html_url: string;
|
||||
};
|
||||
};
|
||||
|
||||
// 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 PagerDutyServiceResponse = {
|
||||
service: PagerDutyService;
|
||||
};
|
||||
|
||||
// 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 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)
|
||||
//
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* 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 { 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<DiscoveryApi> = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockName('discoveryApi')
|
||||
.mockResolvedValue('http://localhost:7007/proxy'),
|
||||
};
|
||||
const mockFetchApi: MockFetchApi = new MockFetchApi({
|
||||
baseImplementation: mockFetch,
|
||||
});
|
||||
|
||||
let client: PagerDutyClient;
|
||||
let entity: Entity;
|
||||
|
||||
const user: PagerDutyUser = {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
summary: 'person1',
|
||||
email: 'person1@example.com',
|
||||
html_url: 'http://a.com/id1',
|
||||
};
|
||||
|
||||
const service: PagerDutyService = {
|
||||
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(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('queries proxy path by integration id', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ services: [service] }),
|
||||
});
|
||||
|
||||
expect(await client.getServiceByEntity(entity)).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(entity)).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(entity)).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(entity)).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(entity)).rejects.toThrowError(
|
||||
NotFoundError,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided entity has a serviceId value', () => {
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('queries proxy path by integration id', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ service }),
|
||||
});
|
||||
|
||||
expect(await client.getServiceByEntity(entity)).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(entity)).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(entity)).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(entity)).rejects.toThrowError(
|
||||
'Request failed with 500, Not valid request internal error occurred',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided entity has both integrationKey and serviceId', () => {
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {
|
||||
'pagerduty.com/integration-key': 'abc123',
|
||||
'pagerduty.com/service-id': 'def456',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('queries proxy path by integration id', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ services: [service] }),
|
||||
});
|
||||
|
||||
expect(await client.getServiceByEntity(entity)).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(entity)).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(entity)).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(entity)).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(entity)).rejects.toThrowError(
|
||||
NotFoundError,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided entity has no integrationKey or serviceId values', () => {
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'pagerduty-test',
|
||||
annotations: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('throws NotFoundError', async () => {
|
||||
await expect(client.getServiceByEntity(entity)).rejects.toThrowError(
|
||||
NotFoundError,
|
||||
);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,23 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Service, Incident, ChangeEvent, OnCall } from '../components/types';
|
||||
import {
|
||||
PagerDutyApi,
|
||||
TriggerAlarmRequest,
|
||||
ServicesResponse,
|
||||
IncidentsResponse,
|
||||
OnCallsResponse,
|
||||
ClientApiConfig,
|
||||
PagerDutyTriggerAlarmRequest,
|
||||
PagerDutyServicesResponse,
|
||||
PagerDutyServiceResponse,
|
||||
PagerDutyIncidentsResponse,
|
||||
PagerDutyOnCallsResponse,
|
||||
PagerDutyClientApiDependencies,
|
||||
PagerDutyClientApiConfig,
|
||||
RequestOptions,
|
||||
ChangeEventsResponse,
|
||||
PagerDutyChangeEventsResponse,
|
||||
} from './types';
|
||||
import {
|
||||
createApiRef,
|
||||
DiscoveryApi,
|
||||
ConfigApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createApiRef, ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { getPagerDutyEntity } from '../components/pagerDutyEntity';
|
||||
|
||||
export class UnauthorizedError extends Error {}
|
||||
|
||||
@@ -38,65 +37,89 @@ export const pagerDutyApiRef = createApiRef<PagerDutyApi>({
|
||||
id: 'plugin.pagerduty.api',
|
||||
});
|
||||
|
||||
const commonGetServiceParams =
|
||||
'time_zone=UTC&include[]=integrations&include[]=escalation_policies';
|
||||
|
||||
export class PagerDutyClient implements PagerDutyApi {
|
||||
static fromConfig(
|
||||
configApi: ConfigApi,
|
||||
discoveryApi: DiscoveryApi,
|
||||
identityApi: IdentityApi,
|
||||
{ discoveryApi, fetchApi }: PagerDutyClientApiDependencies,
|
||||
) {
|
||||
const eventsBaseUrl: string =
|
||||
configApi.getOptionalString('pagerDuty.eventsBaseUrl') ??
|
||||
'https://events.pagerduty.com/v2';
|
||||
|
||||
return new PagerDutyClient({
|
||||
eventsBaseUrl,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
}
|
||||
constructor(private readonly config: ClientApiConfig) {}
|
||||
constructor(private readonly config: PagerDutyClientApiConfig) {}
|
||||
|
||||
async getServiceByIntegrationKey(integrationKey: string): Promise<Service[]> {
|
||||
const params = `time_zone=UTC&include[]=integrations&include[]=escalation_policies&query=${integrationKey}`;
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/pagerduty/services?${params}`;
|
||||
const { services } = await this.getByUrl<ServicesResponse>(url);
|
||||
async getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse> {
|
||||
const { integrationKey, serviceId } = getPagerDutyEntity(entity);
|
||||
|
||||
return services;
|
||||
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<PagerDutyServicesResponse>(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<PagerDutyServiceResponse>(url);
|
||||
} else {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getIncidentsByServiceId(serviceId: string): Promise<Incident[]> {
|
||||
async getIncidentsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyIncidentsResponse> {
|
||||
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<IncidentsResponse>(url);
|
||||
|
||||
return incidents;
|
||||
return await this.getByUrl<PagerDutyIncidentsResponse>(url);
|
||||
}
|
||||
|
||||
async getChangeEventsByServiceId(serviceId: string): Promise<ChangeEvent[]> {
|
||||
async getChangeEventsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyChangeEventsResponse> {
|
||||
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<ChangeEventsResponse>(url);
|
||||
|
||||
return change_events;
|
||||
return await this.getByUrl<PagerDutyChangeEventsResponse>(url);
|
||||
}
|
||||
|
||||
async getOnCallByPolicyId(policyId: string): Promise<OnCall[]> {
|
||||
async getOnCallByPolicyId(
|
||||
policyId: string,
|
||||
): Promise<PagerDutyOnCallsResponse> {
|
||||
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<OnCallsResponse>(url);
|
||||
|
||||
return oncalls;
|
||||
return await this.getByUrl<PagerDutyOnCallsResponse>(url);
|
||||
}
|
||||
|
||||
triggerAlarm(request: TriggerAlarmRequest): Promise<Response> {
|
||||
triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise<Response> {
|
||||
const { integrationKey, source, description, userName } = request;
|
||||
|
||||
const body = JSON.stringify({
|
||||
@@ -130,13 +153,11 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
}
|
||||
|
||||
private async getByUrl<T>(url: string): Promise<T> {
|
||||
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);
|
||||
@@ -147,10 +168,15 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
): Promise<Response> {
|
||||
const response = await fetch(url, options);
|
||||
const response = await this.config.fetchApi.fetch(url, options);
|
||||
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(' ');
|
||||
|
||||
@@ -15,4 +15,12 @@
|
||||
*/
|
||||
|
||||
export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client';
|
||||
export type { PagerDutyApi } from './types';
|
||||
export type {
|
||||
PagerDutyServiceResponse,
|
||||
PagerDutyIncidentsResponse,
|
||||
PagerDutyChangeEventsResponse,
|
||||
PagerDutyOnCallsResponse,
|
||||
PagerDutyTriggerAlarmRequest,
|
||||
PagerDutyClientApiDependencies,
|
||||
PagerDutyClientApiConfig,
|
||||
} from './types';
|
||||
|
||||
@@ -14,10 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Incident, ChangeEvent, OnCall, Service } from '../components/types';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
PagerDutyIncident,
|
||||
PagerDutyChangeEvent,
|
||||
PagerDutyOnCall,
|
||||
PagerDutyService,
|
||||
} from '../components/types';
|
||||
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export type TriggerAlarmRequest = {
|
||||
export type PagerDutyServicesResponse = {
|
||||
services: PagerDutyService[];
|
||||
};
|
||||
|
||||
export type PagerDutyServiceResponse = {
|
||||
service: PagerDutyService;
|
||||
};
|
||||
|
||||
export type PagerDutyIncidentsResponse = {
|
||||
incidents: PagerDutyIncident[];
|
||||
};
|
||||
|
||||
export type PagerDutyChangeEventsResponse = {
|
||||
change_events: PagerDutyChangeEvent[];
|
||||
};
|
||||
|
||||
export type PagerDutyOnCallsResponse = {
|
||||
oncalls: PagerDutyOnCall[];
|
||||
};
|
||||
|
||||
export type PagerDutyTriggerAlarmRequest = {
|
||||
integrationKey: string;
|
||||
source: string;
|
||||
description: string;
|
||||
@@ -26,55 +52,46 @@ export type TriggerAlarmRequest = {
|
||||
|
||||
export interface PagerDutyApi {
|
||||
/**
|
||||
* Fetches a list of services, filtered by the provided integration key.
|
||||
* Fetches the service for the provided Entity.
|
||||
*
|
||||
*/
|
||||
getServiceByIntegrationKey(integrationKey: string): Promise<Service[]>;
|
||||
getServiceByEntity(entity: Entity): Promise<PagerDutyServiceResponse>;
|
||||
|
||||
/**
|
||||
* Fetches a list of incidents a provided service has.
|
||||
*
|
||||
*/
|
||||
getIncidentsByServiceId(serviceId: string): Promise<Incident[]>;
|
||||
getIncidentsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyIncidentsResponse>;
|
||||
|
||||
/**
|
||||
* Fetches a list of change events a provided service has.
|
||||
*
|
||||
*/
|
||||
getChangeEventsByServiceId(serviceId: string): Promise<ChangeEvent[]>;
|
||||
getChangeEventsByServiceId(
|
||||
serviceId: string,
|
||||
): Promise<PagerDutyChangeEventsResponse>;
|
||||
|
||||
/**
|
||||
* Fetches the list of users in an escalation policy.
|
||||
*
|
||||
*/
|
||||
getOnCallByPolicyId(policyId: string): Promise<OnCall[]>;
|
||||
getOnCallByPolicyId(policyId: string): Promise<PagerDutyOnCallsResponse>;
|
||||
|
||||
/**
|
||||
* Triggers an incident to whoever is on-call.
|
||||
*/
|
||||
triggerAlarm(request: TriggerAlarmRequest): Promise<Response>;
|
||||
triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise<Response>;
|
||||
}
|
||||
|
||||
export type ServicesResponse = {
|
||||
services: Service[];
|
||||
};
|
||||
|
||||
export type IncidentsResponse = {
|
||||
incidents: Incident[];
|
||||
};
|
||||
|
||||
export type ChangeEventsResponse = {
|
||||
change_events: ChangeEvent[];
|
||||
};
|
||||
|
||||
export type OnCallsResponse = {
|
||||
oncalls: OnCall[];
|
||||
};
|
||||
|
||||
export type ClientApiConfig = {
|
||||
eventsBaseUrl?: string;
|
||||
export type PagerDutyClientApiDependencies = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
fetchApi: FetchApi;
|
||||
};
|
||||
|
||||
export type PagerDutyClientApiConfig = PagerDutyClientApiDependencies & {
|
||||
eventsBaseUrl?: string;
|
||||
};
|
||||
|
||||
export type RequestOptions = {
|
||||
|
||||
@@ -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<BackstageTheme>({
|
||||
});
|
||||
|
||||
type Props = {
|
||||
changeEvent: ChangeEvent;
|
||||
changeEvent: PagerDutyChangeEvent;
|
||||
};
|
||||
|
||||
export const ChangeEventListItem = ({ changeEvent }: Props) => {
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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';
|
||||
@@ -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 PagerDutyChangeEvent[],
|
||||
}));
|
||||
const { getByText, getAllByTitle, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -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 PagerDutyChangeEvent[],
|
||||
}));
|
||||
const { getByText, getAllByTitle, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = () => (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="PagerDuty Service Not Found"
|
||||
description="A service could not be found within PagerDuty based on the provided annotations. Please verify your annotation details."
|
||||
action={
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
href="https://github.com/backstage/backstage/blob/master/plugins/pagerduty/README.md"
|
||||
>
|
||||
Read More
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { MissingTokenError } from './MissingTokenError';
|
||||
export { ServiceNotFoundError } from './ServiceNotFoundError';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 PagerDutyUser,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<BackstageTheme>(theme => ({
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
incident: Incident;
|
||||
incident: PagerDutyIncident;
|
||||
};
|
||||
|
||||
export const IncidentListItem = ({ incident }: Props) => {
|
||||
|
||||
@@ -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 = {
|
||||
@@ -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',
|
||||
@@ -82,8 +83,8 @@ describe('Incidents', () => {
|
||||
html_url: 'http://a.com/id2',
|
||||
serviceId: 'sId2',
|
||||
},
|
||||
] as Incident[],
|
||||
);
|
||||
] as PagerDutyIncident[],
|
||||
}));
|
||||
const { getByText, getAllByTitle, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -15,26 +15,17 @@
|
||||
*/
|
||||
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 { 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';
|
||||
|
||||
const mockPagerDutyApi: Partial<PagerDutyClient> = {
|
||||
getServiceByIntegrationKey: async () => [],
|
||||
getOnCallByPolicyId: async () => [],
|
||||
getIncidentsByServiceId: async () => [],
|
||||
};
|
||||
|
||||
const apis = TestApiRegistry.from(
|
||||
[pagerDutyApiRef, mockPagerDutyApi],
|
||||
[alertApiRef, {}],
|
||||
);
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
@@ -46,7 +37,39 @@ const entity: Entity = {
|
||||
},
|
||||
};
|
||||
|
||||
const user: User = {
|
||||
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: PagerDutyUser = {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
summary: 'person1',
|
||||
@@ -54,8 +77,8 @@ const user: User = {
|
||||
html_url: 'http://a.com/id1',
|
||||
};
|
||||
|
||||
const service: Service = {
|
||||
id: 'abc',
|
||||
const service: PagerDutyService = {
|
||||
id: 'def456',
|
||||
name: 'pagerduty-name',
|
||||
html_url: 'www.example.com',
|
||||
escalation_policy: {
|
||||
@@ -63,14 +86,51 @@ const service: Service = {
|
||||
user: user,
|
||||
html_url: 'http://a.com/id1',
|
||||
},
|
||||
integrationKey: 'abcd',
|
||||
integrationKey: 'abc123',
|
||||
};
|
||||
|
||||
const mockPagerDutyApi: Partial<PagerDutyClient> = {
|
||||
getServiceByEntity: async () => ({ service }),
|
||||
getOnCallByPolicyId: async () => ({ oncalls: [] }),
|
||||
getIncidentsByServiceId: async () => ({ incidents: [] }),
|
||||
};
|
||||
|
||||
const apis = TestApiRegistry.from(
|
||||
[pagerDutyApiRef, mockPagerDutyApi],
|
||||
[alertApiRef, {}],
|
||||
);
|
||||
|
||||
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
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => [service]);
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
@@ -89,7 +149,7 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
|
||||
it('Handles custom error for missing token', async () => {
|
||||
mockPagerDutyApi.getServiceByIntegrationKey = jest
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
@@ -106,8 +166,26 @@ describe('PageDutyCard', () => {
|
||||
expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByIntegrationKey = jest
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
@@ -127,10 +205,11 @@ describe('PageDutyCard', () => {
|
||||
),
|
||||
).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(
|
||||
@@ -150,4 +229,131 @@ describe('PageDutyCard', () => {
|
||||
});
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when entity has the pagerduty.com/service-id annotation', () => {
|
||||
it('Renders PagerDuty service information', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
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.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new UnauthorizedError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('Missing or invalid PagerDuty Token'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Handles custom NotFoundError', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new NotFoundError());
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('PagerDuty Service Not Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles general error', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('An error occurred'));
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
|
||||
expect(
|
||||
getByText(
|
||||
'Error encountered while fetching information. An error occurred',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the Create Incident button', async () => {
|
||||
mockPagerDutyApi.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { queryByTestId, getByTitle } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithServiceId}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
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.getServiceByEntity = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => ({ service }));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entityWithAllAnnotations}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -22,28 +22,39 @@ import useAsync from 'react-use/lib/useAsync';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { pagerDutyApiRef, UnauthorizedError } 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 } from '../constants';
|
||||
import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from '../constants';
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
import { ChangeEvents } from '../ChangeEvents';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
Progress,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
TabbedCard,
|
||||
CardTab,
|
||||
InfoCard,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { getPagerDutyEntity } from '../pagerDutyEntity';
|
||||
|
||||
const BasicCard = ({ children }: { children: ReactNode }) => (
|
||||
<InfoCard title="PagerDuty">{children}</InfoCard>
|
||||
);
|
||||
|
||||
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();
|
||||
const { entity } = useEntity();
|
||||
const pagerDutyEntity = getPagerDutyEntity(entity);
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const [refreshChangeEvents, setRefreshChangeEvents] =
|
||||
@@ -67,33 +78,44 @@ export const PagerDutyCard = () => {
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
const services = await api.getServiceByIntegrationKey(
|
||||
integrationKey as string,
|
||||
);
|
||||
const { service: foundService } = await api.getServiceByEntity(entity);
|
||||
|
||||
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: foundService.id,
|
||||
name: foundService.name,
|
||||
url: foundService.html_url,
|
||||
policyId: foundService.escalation_policy.id,
|
||||
policyLink: foundService.escalation_policy.html_url,
|
||||
};
|
||||
});
|
||||
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return <MissingTokenError />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching information. {error.message}
|
||||
</Alert>
|
||||
);
|
||||
let errorNode: ReactNode;
|
||||
|
||||
switch (error.constructor) {
|
||||
case UnauthorizedError:
|
||||
errorNode = <MissingTokenError />;
|
||||
break;
|
||||
case NotFoundError:
|
||||
errorNode = <ServiceNotFoundError />;
|
||||
break;
|
||||
default:
|
||||
errorNode = (
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching information. {error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <BasicCard>{errorNode}</BasicCard>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
return (
|
||||
<BasicCard>
|
||||
<Progress />
|
||||
</BasicCard>
|
||||
);
|
||||
}
|
||||
|
||||
const serviceLink: IconLinkVerticalProps = {
|
||||
@@ -102,11 +124,21 @@ export const PagerDutyCard = () => {
|
||||
icon: <WebIcon />,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = !pagerDutyEntity.integrationKey;
|
||||
const triggerLink: IconLinkVerticalProps = {
|
||||
label: 'Create Incident',
|
||||
onClick: showDialog,
|
||||
icon: <AlarmAddIcon />,
|
||||
color: 'secondary',
|
||||
disabled: createIncidentDisabled,
|
||||
title: createIncidentDisabled
|
||||
? 'Must provide an integration-key to create incidents'
|
||||
: '',
|
||||
};
|
||||
|
||||
const escalationPolicyLink: IconLinkVerticalProps = {
|
||||
@@ -145,12 +177,14 @@ export const PagerDutyCard = () => {
|
||||
<EscalationPolicy policyId={service!.policyId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TriggerDialog
|
||||
data-testid="trigger-dialog"
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
{!createIncidentDisabled && (
|
||||
<TriggerDialog
|
||||
data-testid="trigger-dialog"
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
PagerDutyChangeEvent,
|
||||
PagerDutyIncident,
|
||||
PagerDutyService,
|
||||
PagerDutyOnCall,
|
||||
PagerDutyAssignee,
|
||||
PagerDutyUser,
|
||||
} from './types';
|
||||
|
||||
export {
|
||||
isPluginApplicableToEntity,
|
||||
isPluginApplicableToEntity as isPagerDutyAvailable,
|
||||
PagerDutyCard,
|
||||
} from './PagerDutyCard';
|
||||
|
||||
export { TriggerButton } from './TriggerButton';
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, string>);
|
||||
const name = entity.metadata.name;
|
||||
|
||||
return { integrationKey, serviceId, name };
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -15,14 +15,11 @@
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { PagerDutyEntity } from '../types';
|
||||
import { getPagerDutyEntity } from '../components/pagerDutyEntity';
|
||||
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../components/constants';
|
||||
|
||||
export function usePagerdutyEntity() {
|
||||
export function usePagerdutyEntity(): PagerDutyEntity {
|
||||
const { entity } = useEntity();
|
||||
const integrationKey =
|
||||
entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY];
|
||||
const name = entity.metadata.name;
|
||||
|
||||
return { integrationKey, name };
|
||||
return getPagerDutyEntity(entity);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user