From ee2c4e699d3eeaf2d572be3923110b55237f5a5d Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Fri, 12 Jul 2024 23:38:51 +0100 Subject: [PATCH 1/4] chore: upgrade pagerduty/backstage-plugin-common to latest Signed-off-by: Tiago Barbosa --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 44e56fc..6ff33f4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@mui/icons-material": "^5.15.19", "@mui/material": "^5.15.19", "@mui/x-date-pickers": "^7.6.1", - "@pagerduty/backstage-plugin-common": "0.1.5", + "@pagerduty/backstage-plugin-common": "0.2.0", "@tanstack/react-query": "^5.40.1", "classnames": "^2.2.6", "luxon": "^3.4.1", diff --git a/yarn.lock b/yarn.lock index 58339e0..35af09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4092,10 +4092,10 @@ __metadata: languageName: node linkType: hard -"@pagerduty/backstage-plugin-common@npm:0.1.5": - version: 0.1.5 - resolution: "@pagerduty/backstage-plugin-common@npm:0.1.5" - checksum: 07d527c07aee79d5bb6bf7f310827b11534922c1f817b2cea5cf672ebe393b2def07293b2d42241a033f257139f8d935114edd5e9385dbbc427f8720f20265b6 +"@pagerduty/backstage-plugin-common@npm:0.2.0": + version: 0.2.0 + resolution: "@pagerduty/backstage-plugin-common@npm:0.2.0" + checksum: d7243ef9c11408eee046be351346455316dcd8984c33a61d773fec292843d3df9eb9906dfc5ce0ed4fc0c17a60b9b3ebbfe1a8f4a25b8ecc003ffa8d4304db77 languageName: node linkType: hard @@ -4124,7 +4124,7 @@ __metadata: "@mui/icons-material": ^5.15.19 "@mui/material": ^5.15.19 "@mui/x-date-pickers": ^7.6.1 - "@pagerduty/backstage-plugin-common": 0.1.5 + "@pagerduty/backstage-plugin-common": 0.2.0 "@tanstack/react-query": ^5.40.1 "@testing-library/dom": ^8.0.0 "@testing-library/jest-dom": ^5.10.1 From 11e75c3602014910aa27e89db5db9b1184afb499 Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Fri, 12 Jul 2024 23:39:55 +0100 Subject: [PATCH 2/4] feat: update types and API requests to support multiple accounts Signed-off-by: Tiago Barbosa --- src/api/client.ts | 56 +++++++++++++++++++++++++++++++++++++++-------- src/api/types.ts | 10 +++++++-- src/types.ts | 1 + 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 57d4bc4..efca3d4 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -71,7 +71,7 @@ export class PagerDutyClient implements PagerDutyApi { async getServiceByPagerDutyEntity( pagerDutyEntity: PagerDutyEntity, ): Promise { - const { integrationKey, serviceId } = pagerDutyEntity; + const { integrationKey, serviceId, account } = pagerDutyEntity; let response: PagerDutyServiceResponse; let url: string; @@ -80,6 +80,10 @@ export class PagerDutyClient implements PagerDutyApi { url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services?integration_key=${integrationKey}`; + + if(account) { + url = `${url}&account=${account}`; + } const serviceResponse = await this.findByUrl(url); if (serviceResponse.service === undefined) throw new NotFoundError(); @@ -90,6 +94,10 @@ export class PagerDutyClient implements PagerDutyApi { 'pagerduty', )}/services/${serviceId}`; + if (account) { + url = `${url}?account=${account}`; + } + response = await this.findByUrl(url); } else { throw new NotFoundError(); @@ -106,12 +114,13 @@ export class PagerDutyClient implements PagerDutyApi { return await this.findByUrl(url); } - async storeServiceMapping(serviceId: string, integrationKey: string, backstageEntityRef: string): Promise { + async storeServiceMapping(serviceId: string, integrationKey: string, backstageEntityRef: string, account: string): Promise { const body = JSON.stringify({ entityRef: backstageEntityRef, serviceId: serviceId, integrationKey: integrationKey, + account: account, }); const options = { @@ -134,62 +143,91 @@ export class PagerDutyClient implements PagerDutyApi { return await this.getServiceByPagerDutyEntity(getPagerDutyEntity(entity)); } - async getServiceById(serviceId: string): Promise { - const url = `${await this.config.discoveryApi.getBaseUrl( + async getServiceById(serviceId: string, account?: string): Promise { + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services/${serviceId}`; + if(account) { + url = url.concat(`?account=${account}`); + } + return await this.findByUrl(url); } async getIncidentsByServiceId( serviceId: string, + account?: string, ): Promise { - const url = `${await this.config.discoveryApi.getBaseUrl( + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services/${serviceId}/incidents`; + if(account) { + url = url.concat(`?account=${account}`); + } + return await this.findByUrl(url); } async getChangeEventsByServiceId( serviceId: string, + account?: string, ): Promise { - const url = `${await this.config.discoveryApi.getBaseUrl( + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services/${serviceId}/change-events`; + if(account) { + url = url.concat(`?account=${account}`); + } + return await this.findByUrl(url); } async getServiceStandardsByServiceId( serviceId: string, + account?: string ): Promise { - const url = `${await this.config.discoveryApi.getBaseUrl( + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services/${serviceId}/standards`; + if(account) { + url = url.concat(`?account=${account}`); + } + return await this.findByUrl(url); } async getServiceMetricsByServiceId( serviceId: string, + account?: string ): Promise { - const url = `${await this.config.discoveryApi.getBaseUrl( + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/services/${serviceId}/metrics`; + if (account){ + url = url.concat(`?account=${account}`); + } + return await this.findByUrl(url); } async getOnCallByPolicyId( policyId: string, + account?: string, ): Promise { const params = `escalation_policy_ids[]=${policyId}`; - const url = `${await this.config.discoveryApi.getBaseUrl( + let url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', )}/oncall-users?${params}`; + if (account) { + url = url.concat(`&account=${account}`); + } + const response: PagerDutyOnCallUsersResponse = await this.findByUrl(url); return response.users; } diff --git a/src/api/types.ts b/src/api/types.ts index 10571bc..b31b843 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -45,6 +45,7 @@ export type PagerDutyCardServiceResponse = { policyLink: string; policyName: string; status?: string; + account?: string; standards?: PagerDutyServiceStandards; metrics?: PagerDutyServiceMetrics[]; } @@ -61,7 +62,7 @@ export interface PagerDutyApi { * Stores the service mapping in the database. * */ - storeServiceMapping(serviceId: string, integrationKey: string, entityRef: string): Promise; + storeServiceMapping(serviceId: string, integrationKey: string, entityRef: string, account: string): Promise; /** * Fetches the service for the provided pager duty Entity. @@ -83,6 +84,7 @@ export interface PagerDutyApi { */ getServiceById( serviceId: string, + account?: string, ): Promise; /** @@ -91,6 +93,7 @@ export interface PagerDutyApi { */ getIncidentsByServiceId( serviceId: string, + account?: string, ): Promise; /** @@ -99,6 +102,7 @@ export interface PagerDutyApi { */ getChangeEventsByServiceId( serviceId: string, + account?: string, ): Promise; /** @@ -107,6 +111,7 @@ export interface PagerDutyApi { */ getServiceStandardsByServiceId( serviceId: string, + account?: string, ): Promise; /** @@ -115,13 +120,14 @@ export interface PagerDutyApi { */ getServiceMetricsByServiceId( serviceId: string, + account?: string, ): Promise; /** * Fetches the list of users in an escalation policy. * */ - getOnCallByPolicyId(policyId: string): Promise; + getOnCallByPolicyId(policyId: string, account?: string): Promise; /** * Triggers an incident to whoever is on-call. diff --git a/src/types.ts b/src/types.ts index 9f38b42..4e32e07 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,5 +18,6 @@ export type PagerDutyEntity = { integrationKey?: string; serviceId?: string; + account?: string; name: string; }; From 90ab30edb9ea6ce88c98290c7d84a3d3ceff2b84 Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Fri, 12 Jul 2024 23:40:34 +0100 Subject: [PATCH 3/4] feat: update UI components with multi-account support Signed-off-by: Tiago Barbosa --- src/components/ChangeEvents/ChangeEvents.tsx | 5 +++-- src/components/Escalation/Escalation.test.tsx | 15 ++++++++++++--- src/components/Escalation/EscalationPolicy.tsx | 4 +++- src/components/Incident/Incidents.tsx | 4 +++- src/components/PagerDutyCard/index.tsx | 18 ++++++++++++++---- .../PagerDutyCardCommon/StatusCard.tsx | 5 +++-- src/components/PagerDutyPage/MappingTable.tsx | 9 ++++++++- src/components/PagerDutySmallCard/index.tsx | 10 +++++++--- src/components/constants.ts | 1 + src/components/pagerDutyEntity.ts | 5 +++-- 10 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/components/ChangeEvents/ChangeEvents.tsx b/src/components/ChangeEvents/ChangeEvents.tsx index 84d92f7..ca84189 100644 --- a/src/components/ChangeEvents/ChangeEvents.tsx +++ b/src/components/ChangeEvents/ChangeEvents.tsx @@ -28,15 +28,16 @@ import { Alert } from '@material-ui/lab'; type Props = { serviceId: string; + account?: string; refreshEvents: boolean; }; -export const ChangeEvents = ({ serviceId, refreshEvents }: Props) => { +export const ChangeEvents = ({ serviceId, account, refreshEvents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: changeEvents, loading, error }, getChangeEvents] = useAsyncFn( async () => { - const { change_events } = await api.getChangeEventsByServiceId(serviceId); + const { change_events } = await api.getChangeEventsByServiceId(serviceId, account); return change_events; }, ); diff --git a/src/components/Escalation/Escalation.test.tsx b/src/components/Escalation/Escalation.test.tsx index 67bd33f..8a3f46f 100644 --- a/src/components/Escalation/Escalation.test.tsx +++ b/src/components/Escalation/Escalation.test.tsx @@ -49,7 +49,10 @@ describe("Escalation", () => { expect( getByText("No one is on-call. Update the escalation policy.") ).toBeInTheDocument(); - expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("456"); + expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith( + "456", + undefined + ); // undefined is the default value for the optional account parameter }); it("Renders a forbidden state when change events is undefined", async () => { @@ -109,7 +112,10 @@ describe("Escalation", () => { expect(getByText("person1")).toBeInTheDocument(); expect(getByText("person1@example.com")).toBeInTheDocument(); - expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("abc"); + expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith( + "abc", + undefined + ); // undefined is the default value for the optional account parameter }); it("Renders a user with profile picture", async () => { @@ -147,7 +153,10 @@ describe("Escalation", () => { "src", "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y" ); - expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith("abc"); + expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith( + "abc", + undefined + ); // undefined is the default value for the optional account parameter }); it("Handles errors", async () => { diff --git a/src/components/Escalation/EscalationPolicy.tsx b/src/components/Escalation/EscalationPolicy.tsx index e2af10d..04fbf78 100644 --- a/src/components/Escalation/EscalationPolicy.tsx +++ b/src/components/Escalation/EscalationPolicy.tsx @@ -31,6 +31,7 @@ type Props = { policyId: string; policyUrl: string; policyName: string; + account?: string; }; const useStyles = makeStyles(() => createStyles({ @@ -44,6 +45,7 @@ export const EscalationPolicy = ({ policyId, policyUrl, policyName, + account, }: Props) => { const api = useApi(pagerDutyApiRef); const classes = useStyles(); @@ -53,7 +55,7 @@ export const EscalationPolicy = ({ loading, error, } = useAsync(async () => { - return await api.getOnCallByPolicyId(policyId); + return await api.getOnCallByPolicyId(policyId, account); }); if (error) { diff --git a/src/components/Incident/Incidents.tsx b/src/components/Incident/Incidents.tsx index e008b3b..6a79cfa 100644 --- a/src/components/Incident/Incidents.tsx +++ b/src/components/Incident/Incidents.tsx @@ -28,16 +28,18 @@ import { IncidentForbiddenState } from './IncidentForbiddenState'; type Props = { serviceId: string; + account?: string; refreshIncidents: boolean; }; -export const Incidents = ({ serviceId, refreshIncidents }: Props) => { +export const Incidents = ({ serviceId, account, refreshIncidents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( async () => { const { incidents: foundIncidents } = await api.getIncidentsByServiceId( serviceId, + account ); return foundIncidents; }, diff --git a/src/components/PagerDutyCard/index.tsx b/src/components/PagerDutyCard/index.tsx index b5175c7..aefa8b2 100644 --- a/src/components/PagerDutyCard/index.tsx +++ b/src/components/PagerDutyCard/index.tsx @@ -47,7 +47,7 @@ import { OpenServiceButton, ServiceStandardsCard, StatusCard, - TriggerIncidentButton + TriggerIncidentButton, } from "../PagerDutyCardCommon"; import { createStyles, makeStyles, useTheme } from "@material-ui/core/styles"; import { BackstageTheme } from "@backstage/theme"; @@ -142,15 +142,18 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { ); const serviceStandards = await api.getServiceStandardsByServiceId( - foundService.id + foundService.id, + props.account ); const serviceMetrics = await api.getServiceMetricsByServiceId( - foundService.id + foundService.id, + props.account ); const result: PagerDutyCardServiceResponse = { id: foundService.id, + account: props.account, name: foundService.name, url: foundService.html_url, policyId: foundService.escalation_policy.id, @@ -242,7 +245,11 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { - + @@ -307,6 +314,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { {disableChangeEvents !== true ? ( @@ -315,6 +323,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { data-testid="change-events" serviceId={service!.id} refreshEvents={refreshChangeEvents} + account={service!.account} /> ) : ( @@ -331,6 +340,7 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { policyId={service!.policyId} policyUrl={service!.policyLink} policyName={service!.policyName} + account={service!.account} /> ) : ( diff --git a/src/components/PagerDutyCardCommon/StatusCard.tsx b/src/components/PagerDutyCardCommon/StatusCard.tsx index c6700f2..da0ef08 100644 --- a/src/components/PagerDutyCardCommon/StatusCard.tsx +++ b/src/components/PagerDutyCardCommon/StatusCard.tsx @@ -11,6 +11,7 @@ import { Progress } from "@backstage/core-components"; type Props = { serviceId: string; refreshStatus: boolean; + account?: string; compact?: boolean; }; @@ -66,11 +67,11 @@ function colorFromStatus(theme: Theme, status: string) { return color; } -function StatusCard({ serviceId, refreshStatus, compact}: Props) { +function StatusCard({ serviceId, refreshStatus, account, compact}: Props) { const api = useApi(pagerDutyApiRef); const [{ value: status, loading, error }, getStatus] = useAsyncFn( async () => { - const { service: foundService } = await api.getServiceById(serviceId); + const { service: foundService } = await api.getServiceById(serviceId, account); return foundService.status; } ); diff --git a/src/components/PagerDutyPage/MappingTable.tsx b/src/components/PagerDutyPage/MappingTable.tsx index d1b7583..0f276c2 100644 --- a/src/components/PagerDutyPage/MappingTable.tsx +++ b/src/components/PagerDutyPage/MappingTable.tsx @@ -120,6 +120,12 @@ export const MappingTable = ({ header: "PagerDuty Service", enableEditing: false, }, + { + accessorKey: "account", + header: "Account", + enableEditing: false, + Edit: () => null, + }, { accessorKey: "team", header: "Team", @@ -185,7 +191,8 @@ export const MappingTable = ({ return await pagerDutyApi.storeServiceMapping( mapping.serviceId, mapping.integrationKey || "", - mapping.entityRef + mapping.entityRef, + mapping.account || "" ); }, }); diff --git a/src/components/PagerDutySmallCard/index.tsx b/src/components/PagerDutySmallCard/index.tsx index 9fc9012..3592d8b 100644 --- a/src/components/PagerDutySmallCard/index.tsx +++ b/src/components/PagerDutySmallCard/index.tsx @@ -132,16 +132,19 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { ); const serviceStandards = await api.getServiceStandardsByServiceId( - foundService.id + foundService.id, + props.account ); const serviceMetrics = await api.getServiceMetricsByServiceId( - foundService.id + foundService.id, + props.account ); const result: PagerDutyCardServiceResponse = { id: foundService.id, name: foundService.name, + account: props.account, url: foundService.html_url, policyId: foundService.escalation_policy.id, policyLink: foundService.escalation_policy.html_url as string, @@ -224,7 +227,7 @@ export const PagerDutySmallCard = (props: PagerDutyCardProps) => { - + { policyId={service!.policyId} policyUrl={service!.policyLink} policyName={service!.policyName} + account={service!.account} /> diff --git a/src/components/constants.ts b/src/components/constants.ts index 0130859..322cb4b 100644 --- a/src/components/constants.ts +++ b/src/components/constants.ts @@ -15,3 +15,4 @@ */ export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; export const PAGERDUTY_SERVICE_ID = 'pagerduty.com/service-id'; +export const PAGERDUTY_ACCOUNT_NAME = 'pagerduty.com/account'; diff --git a/src/components/pagerDutyEntity.ts b/src/components/pagerDutyEntity.ts index 9505258..af19fad 100644 --- a/src/components/pagerDutyEntity.ts +++ b/src/components/pagerDutyEntity.ts @@ -16,14 +16,15 @@ import { Entity } from '@backstage/catalog-model'; import { PagerDutyEntity } from '../types'; -import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID } from './constants'; +import { PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_SERVICE_ID, PAGERDUTY_ACCOUNT_NAME } from './constants'; export function getPagerDutyEntity(entity: Entity): PagerDutyEntity { const { [PAGERDUTY_INTEGRATION_KEY]: integrationKey, [PAGERDUTY_SERVICE_ID]: serviceId, + [PAGERDUTY_ACCOUNT_NAME]: account, } = entity.metadata.annotations || ({} as Record); const name = entity.metadata.name; - return { integrationKey, serviceId, name }; + return { integrationKey, serviceId, account, name }; } From d0c0ca4fddfd8ff34e8b602895809f7a4a51de7b Mon Sep 17 00:00:00 2001 From: Tiago Barbosa Date: Fri, 12 Jul 2024 23:44:13 +0100 Subject: [PATCH 4/4] chore: update plugin config schema Signed-off-by: Tiago Barbosa --- config.d.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/config.d.ts b/config.d.ts index ffb3ffc..fce94d3 100644 --- a/config.d.ts +++ b/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { PagerDutyOAuthConfig } from "@pagerduty/backstage-plugin-common"; +import { PagerDutyAccountConfig, PagerDutyOAuthConfig } from '@pagerduty/backstage-plugin-common'; export interface Config { /** @@ -28,9 +28,24 @@ export interface Config { */ eventsBaseUrl?: string; /** - * Optional API Base URL to override the default. - * @visibility frontend - */ + * Optional API Base URL to override the default. + * @visibility frontend + */ apiBaseUrl?: string; + /** + * Optional PagerDuty API Token used in API calls from the backend component. + * @visibility secret + */ + apiToken?: string; + /** + * Optional PagerDuty Scoped OAuth Token used in API calls from the backend component. + * @deepVisibility secret + */ + oauth?: PagerDutyOAuthConfig; + /** + * Optional PagerDuty multi-account configuration + * @deepVisibility secret + */ + accounts?: PagerDutyAccountConfig[]; }; }