diff --git a/dev/mockPagerDutyApi.ts b/dev/mockPagerDutyApi.ts index d227943..9d13c83 100644 --- a/dev/mockPagerDutyApi.ts +++ b/dev/mockPagerDutyApi.ts @@ -23,6 +23,15 @@ import { Entity } from '@backstage/catalog-model'; import { v4 as uuidv4 } from 'uuid'; export const mockPagerDutyApi: PagerDutyApi = { + async getSetting(id: string) { + return { + id: id, + value: 'backstage', + }; + }, + async storeSettings(settings) { + return new Response(JSON.stringify(settings)); + }, async getEntityMappings() { return { mappings: [ diff --git a/package.json b/package.json index 62f7cc2..b707e05 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@mui/icons-material": "^5.15.19", "@mui/material": "^5.15.19", "@mui/x-date-pickers": "^7.6.1", - "@pagerduty/backstage-plugin-common": "0.2.0", + "@pagerduty/backstage-plugin-common": "0.2.1", "@tanstack/react-query": "^5.40.1", "classnames": "^2.2.6", "luxon": "^3.4.1", diff --git a/src/api/client.test.ts b/src/api/client.test.ts index 4897e26..c2803bd 100644 --- a/src/api/client.test.ts +++ b/src/api/client.test.ts @@ -16,7 +16,7 @@ import { MockFetchApi } from '@backstage/test-utils'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { PagerDutyClient, UnauthorizedError } from './client'; -import { PagerDutyService } from '@pagerduty/backstage-plugin-common'; +import { PagerDutyService, PagerDutySetting } from '@pagerduty/backstage-plugin-common'; import { NotFoundError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; @@ -365,3 +365,81 @@ describe('PagerDutyClient', () => { }); }); }); + +describe('getSetting', () => { + const settingId = 'settingId'; + const setting: PagerDutySetting = { + id: settingId, + value: 'disabled', + }; + + beforeEach(() => { + mockFetch.mockResolvedValueOnce({ + status: 200, + ok: true, + json: () => Promise.resolve(setting), + }); + }); + + it('should fetch the setting by ID', async () => { + const result = await client.getSetting(settingId); + + expect(result).toEqual(setting); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:7007/pagerduty/settings/settingId', + requestHeaders, + ); + }); + + describe('storeSettings', () => { + const settings: PagerDutySetting[] = [ + { + id: 'setting1', + value: 'disabled', + }, + { + id: 'setting2', + value: 'backstage', + }, + ]; + + beforeEach(() => { + mockFetch.mockReset(); + }); + + it('should send a POST request to the correct URL with the settings', async () => { + const response = new Response(null, { status: 200 }); + mockFetch.mockResolvedValueOnce(response); + + const expectedUrl = 'http://localhost:7007/pagerduty/settings'; + const expectedOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json, text/plain, */*', + }, + body: JSON.stringify(settings), + }; + + await expect(client.storeSettings(settings)).resolves.toEqual(response); + expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + it('should throw an error if the request fails', async () => { + const errorResponse = { + status: 500, + ok: false, + json: () => + Promise.resolve({ + errors: ['Internal server error'], + }), + }; + mockFetch.mockResolvedValueOnce(errorResponse); + + await expect(client.storeSettings(settings)).rejects.toThrow( + 'Request failed with 500, Internal server error', + ); + }); + }); + +}); \ No newline at end of file diff --git a/src/api/client.ts b/src/api/client.ts index efca3d4..76104dd 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -28,7 +28,8 @@ import { PagerDutyChangeEventsResponse, PagerDutyIncidentsResponse, PagerDutyServiceStandardsResponse, PagerDutyServiceMetricsResponse, - PagerDutyEntityMappingsResponse + PagerDutyEntityMappingsResponse, + PagerDutySetting } from '@pagerduty/backstage-plugin-common'; import { createApiRef, ConfigApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; @@ -106,6 +107,33 @@ export class PagerDutyClient implements PagerDutyApi { return response; } + async getSetting(id: string): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'pagerduty', + )}/settings/${id}`; + + return await this.findByUrl(url); + } + + async storeSettings(settings: PagerDutySetting[]): Promise { + const body = JSON.stringify(settings); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json, text/plain, */*', + }, + body, + }; + + const url = `${await this.config.discoveryApi.getBaseUrl( + 'pagerduty', + )}/settings`; + + return this.request(url, options); + } + async getEntityMappings(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'pagerduty', diff --git a/src/api/types.ts b/src/api/types.ts index b31b843..1a0fa82 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -22,7 +22,8 @@ import { PagerDutyChangeEventsResponse, PagerDutyServiceMetricsResponse, PagerDutyServiceStandards, PagerDutyServiceMetrics, - PagerDutyEntityMappingsResponse + PagerDutyEntityMappingsResponse, + PagerDutySetting } from '@pagerduty/backstage-plugin-common'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; @@ -52,6 +53,16 @@ export type PagerDutyCardServiceResponse = { /** @public */ export interface PagerDutyApi { + /** + * Fetches PagerDuty setting from store. + * + */ + getSetting(id: string): Promise; + /** + * Stores PagerDuty setting in the database. + * + */ + storeSettings(settings: PagerDutySetting[]): Promise; /** * Fetches all entity mappings. * diff --git a/src/components/PagerDutyPage/MappingTable.tsx b/src/components/PagerDutyPage/MappingTable.tsx index 0f276c2..97e95e6 100644 --- a/src/components/PagerDutyPage/MappingTable.tsx +++ b/src/components/PagerDutyPage/MappingTable.tsx @@ -97,6 +97,7 @@ export const MappingTable = ({ const columns = useMemo[]>( () => [ { + id: "serviceId", accessorKey: "serviceId", header: "Service ID", visibleInShowHideMenu: false, @@ -109,6 +110,7 @@ export const MappingTable = ({ ), }, { + id: "integrationKey", accessorKey: "integrationKey", header: "Integration Key", visibleInShowHideMenu: false, @@ -116,27 +118,32 @@ export const MappingTable = ({ Edit: () => null, }, { + id: "serviceName", accessorKey: "serviceName", header: "PagerDuty Service", enableEditing: false, }, { + id: "account", accessorKey: "account", header: "Account", enableEditing: false, Edit: () => null, }, { + id: "team", accessorKey: "team", header: "Team", enableEditing: false, }, { + id: "escalationPolicy", accessorKey: "escalationPolicy", header: "Escalation Policy", enableEditing: false, }, { + id: "entityRef", accessorKey: "entityRef", header: "Mapping", visibleInShowHideMenu: false, @@ -151,12 +158,14 @@ export const MappingTable = ({ }, }, { + id: "entityName", accessorKey: "entityName", header: "Mapped Entity Name", enableEditing: false, Edit: () => null, }, { + id: "status", accessorKey: "status", header: "Status", enableEditing: false, @@ -174,6 +183,7 @@ export const MappingTable = ({ ), }, { + id: "serviceUrl", accessorKey: "serviceUrl", header: "Service URL", visibleInShowHideMenu: false, @@ -190,9 +200,9 @@ export const MappingTable = ({ mutationFn: async (mapping: PagerDutyEntityMapping) => { return await pagerDutyApi.storeServiceMapping( mapping.serviceId, - mapping.integrationKey || "", + mapping.integrationKey ?? "", mapping.entityRef, - mapping.account || "" + mapping.account ?? "" ); }, }); @@ -297,6 +307,8 @@ export const MappingTable = ({ showAlertBanner: mappings === undefined || catalogEntities === undefined, showProgressBars: mappings.length === 0 || catalogEntities.length === 0, + }, + initialState: { columnVisibility: { serviceId: false, entityRef: false, diff --git a/src/components/PagerDutyPage/index.tsx b/src/components/PagerDutyPage/index.tsx index df7b0ee..f7c0b5b 100644 --- a/src/components/PagerDutyPage/index.tsx +++ b/src/components/PagerDutyPage/index.tsx @@ -1,28 +1,163 @@ -import React from "react"; -import { Grid, Typography } from "@material-ui/core"; -import { Header, Page, Content } from "@backstage/core-components"; +import React, { useEffect, useState } from "react"; +import { + Card, + FormControlLabel, + Grid, + Radio, + RadioGroup, + Typography, +} from "@material-ui/core"; +import { + Header, + Page, + Content, + TabbedLayout, +} from "@backstage/core-components"; import { ServiceMappingComponent } from "./ServiceMappingComponent"; +import { useApi } from "@backstage/core-plugin-api"; +import { pagerDutyApiRef } from "../../api"; +import { NotFoundError } from "@backstage/errors"; + +const SERVICE_DEPENDENCY_SYNC_STRATEGY = + "settings::service-dependency-sync-strategy"; /** @public */ export const PagerDutyPage = () => { + const pagerDutyApi = useApi(pagerDutyApiRef); + const [ + selectedServiceDependencyStrategy, + setSelectedServiceDependencyStrategy, + ] = useState("disabled"); + + useEffect(() => { + function fetchSetting() { + pagerDutyApi + .getSetting(SERVICE_DEPENDENCY_SYNC_STRATEGY) + .then((result) => { + if (result !== undefined) { + setSelectedServiceDependencyStrategy(result.value); + } + }) + .catch((error) => { + if (error instanceof NotFoundError) { + // If the setting is not found, set the default value to "disabled" + setSelectedServiceDependencyStrategy("disabled"); + } + }); + } + + fetchSetting(); + }, [pagerDutyApi]); + + const handleChange = (event: React.ChangeEvent) => { + const value = getSelectedValue((event.target as HTMLInputElement).value); + + setSelectedServiceDependencyStrategy(value); + + pagerDutyApi.storeSettings([ + { + id: SERVICE_DEPENDENCY_SYNC_STRATEGY, + value, + }, + ]); + }; + + function getSelectedValue( + value: string + ): "backstage" | "pagerduty" | "both" | "disabled" { + switch (value) { + case "backstage": + return "backstage"; + case "pagerduty": + return "pagerduty"; + case "both": + return "both"; + default: + return "disabled"; + } + } + return (
- - - Service to Entity mapping - - Easily map your existing PagerDuty services to entities in Backstage without the need to add anotations to all your projects. - - - Warning: Only 1:1 mapping is allowed at this time. - - - - - - + + + + + {/* Service to Entity mapping */} + + Easily map your existing PagerDuty services to entities in + Backstage without the need to add anotations to all your + projects. + + + Warning: Only 1:1 mapping is allowed at this time. + + + + + + + + + + + Plugin configuration + + Configure your PagerDuty plugin configuration here + + + + <> + + Service dependency synchronization strategy + + + Select the main source of truth for your service dependencies + + + } + label="Backstage" + /> + } + label="PagerDuty" + disabled + /> + } + label="Both" + /> + } + label="Disabled" + /> + + + +
+
+ + Warning: Changing this setting will affect how your + service dependencies are synchronized and may cause data loss. + Check the documentation for more information. + +
+
+
+
); diff --git a/yarn.lock b/yarn.lock index 402c68c..b017900 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4393,10 +4393,10 @@ __metadata: languageName: node linkType: hard -"@pagerduty/backstage-plugin-common@npm:0.2.0": - version: 0.2.0 - resolution: "@pagerduty/backstage-plugin-common@npm:0.2.0" - checksum: d7243ef9c11408eee046be351346455316dcd8984c33a61d773fec292843d3df9eb9906dfc5ce0ed4fc0c17a60b9b3ebbfe1a8f4a25b8ecc003ffa8d4304db77 +"@pagerduty/backstage-plugin-common@npm:0.2.1": + version: 0.2.1 + resolution: "@pagerduty/backstage-plugin-common@npm:0.2.1" + checksum: 76233c2162d8e7bd3479e13652042cf949911be065f0bf92c5823cbc03c122b8ff49938ca36ab8449da800de7dd9c85724f70e3ff5323e77ef879478394115c9 languageName: node linkType: hard @@ -4425,7 +4425,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.2.0 + "@pagerduty/backstage-plugin-common": 0.2.1 "@tanstack/react-query": ^5.40.1 "@testing-library/dom": ^8.0.0 "@testing-library/jest-dom": ^5.10.1