Skip to content

Commit

Permalink
feat: service dependency sync (#125)
Browse files Browse the repository at this point in the history
### Description

This PR updates the `PagerDutyPage` component to add a new
configurations tab where users can configure how they want to sync
service dependencies between Backstage and PagerDuty.


![image](https://github.com/user-attachments/assets/8d5028b9-8c31-4843-b6da-7324e43cd1a9)

**By default, service dependency syncing is disabled.** It's an opt-in
feature and admins need to be aware of what it does because you might
end up deleting existing service dependencies on Backstage or PagerDuty.

**Issue number:** N/A

### Type of change

- [x] New feature (non-breaking change which adds functionality)
- [ ] Fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)

### Checklist

- [x] I have performed a self-review of this change
- [x] Changes have been tested
- [ ] Changes are documented
- [x] Changes generate *no new warnings*
- [x] PR title follows [conventional commit
semantics](https://www.conventionalcommits.org/en/v1.0.0/)

If this is a breaking change 👇

- [ ] I have documented the migration process
- [ ] I have implemented necessary warnings (if it can live side by
side)

## Acknowledgement

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

**Disclaimer:** We value your time and bandwidth. As such, any pull
requests created on non-triaged issues might not be successful.
  • Loading branch information
t1agob authored Jul 24, 2024
2 parents bfd34bb + ea73457 commit d814afb
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 28 deletions.
9 changes: 9 additions & 0 deletions dev/mockPagerDutyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
80 changes: 79 additions & 1 deletion src/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
);
});
});

});
30 changes: 29 additions & 1 deletion src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,6 +107,33 @@ export class PagerDutyClient implements PagerDutyApi {
return response;
}

async getSetting(id: string): Promise<PagerDutySetting> {
const url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
)}/settings/${id}`;

return await this.findByUrl<PagerDutySetting>(url);
}

async storeSettings(settings: PagerDutySetting[]): Promise<Response> {
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<PagerDutyEntityMappingsResponse> {
const url = `${await this.config.discoveryApi.getBaseUrl(
'pagerduty',
Expand Down
13 changes: 12 additions & 1 deletion src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +53,16 @@ export type PagerDutyCardServiceResponse = {

/** @public */
export interface PagerDutyApi {
/**
* Fetches PagerDuty setting from store.
*
*/
getSetting(id: string): Promise<PagerDutySetting>;
/**
* Stores PagerDuty setting in the database.
*
*/
storeSettings(settings: PagerDutySetting[]): Promise<Response>;
/**
* Fetches all entity mappings.
*
Expand Down
4 changes: 2 additions & 2 deletions src/components/PagerDutyPage/MappingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ export const MappingTable = ({
mutationFn: async (mapping: PagerDutyEntityMapping) => {
return await pagerDutyApi.storeServiceMapping(
mapping.serviceId,
mapping.integrationKey || "",
mapping.integrationKey ?? "",
mapping.entityRef,
mapping.account || ""
mapping.account ?? ""
);
},
});
Expand Down
168 changes: 151 additions & 17 deletions src/components/PagerDutyPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,162 @@
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<HTMLInputElement>) => {
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 (
<Page themeId="home">
<Header title="PagerDuty" subtitle="Advanced configurations" />
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<Typography variant="h4">Service to Entity mapping</Typography>
<Typography variant="body1">
Easily map your existing PagerDuty services to entities in Backstage without the need to add anotations to all your projects.
</Typography>
<Typography variant="body1">
<b>Warning: </b>Only 1:1 mapping is allowed at this time.
</Typography>
</Grid>
<Grid item>
<ServiceMappingComponent />
</Grid>
</Grid>
<TabbedLayout>
<TabbedLayout.Route path="/service-mapping" title="Service Mapping">
<Grid container spacing={3} direction="column">
<Grid item>
{/* <Typography variant="h4">Service to Entity mapping</Typography> */}
<Typography variant="body1">
Easily map your existing PagerDuty services to entities in
Backstage without the need to add anotations to all your
projects.
</Typography>
<Typography variant="body1">
<b>Warning: </b>Only 1:1 mapping is allowed at this time.
</Typography>
</Grid>
<Grid item>
<ServiceMappingComponent />
</Grid>
</Grid>
</TabbedLayout.Route>
<TabbedLayout.Route path="/settings" title="Configuration">
<Grid container spacing={3} direction="column">
<Grid item>
<Typography variant="h4">Plugin configuration</Typography>
<Typography variant="body1">
Configure your PagerDuty plugin configuration here
</Typography>
</Grid>
<Card
title="Service dependency synchronization preferences"
style={{ padding: "10px", paddingLeft: "15px", width: "50%" }}
>
<>
<Typography variant="h6">
Service dependency synchronization strategy
</Typography>
<Typography variant="body1">
Select the main source of truth for your service dependencies
</Typography>
<RadioGroup
value={selectedServiceDependencyStrategy}
onChange={handleChange}
>
<FormControlLabel
value="backstage"
control={<Radio />}
label="Backstage"
/>
<FormControlLabel
value="pagerduty"
control={<Radio />}
label="PagerDuty"
/>
<FormControlLabel
value="both"
control={<Radio />}
label="Both"
/>
<FormControlLabel
value="disabled"
control={<Radio />}
label="Disabled"
/>
</RadioGroup>
</>

<br />
<br />
<Typography variant="body1">
<b>Warning: </b>Changing this setting will affect how your
service dependencies are synchronized and may cause data loss.
Check the <a style={{color: "cadetblue"}} href="https://pagerduty.github.io/backstage-plugin-docs/index.html"> documentation </a> for more information.
</Typography>
</Card>
</Grid>
</TabbedLayout.Route>
</TabbedLayout>
</Content>
</Page>
);
Expand Down
Loading

0 comments on commit d814afb

Please sign in to comment.