From 5539625f645704e97fabc72eadc9fcf5094cf6b4 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Mon, 19 Oct 2020 14:15:46 +0200 Subject: [PATCH 01/52] create pagerduty plugin --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + plugins/pagerduty/.eslintrc.js | 3 ++ plugins/pagerduty/README.md | 13 ++++++++ plugins/pagerduty/dev/index.tsx | 19 ++++++++++++ plugins/pagerduty/package.json | 46 ++++++++++++++++++++++++++++ plugins/pagerduty/src/index.ts | 16 ++++++++++ plugins/pagerduty/src/plugin.test.ts | 22 +++++++++++++ plugins/pagerduty/src/plugin.ts | 25 +++++++++++++++ plugins/pagerduty/src/setupTests.ts | 18 +++++++++++ 10 files changed, 164 insertions(+) create mode 100644 plugins/pagerduty/.eslintrc.js create mode 100644 plugins/pagerduty/README.md create mode 100644 plugins/pagerduty/dev/index.tsx create mode 100644 plugins/pagerduty/package.json create mode 100644 plugins/pagerduty/src/index.ts create mode 100644 plugins/pagerduty/src/plugin.test.ts create mode 100644 plugins/pagerduty/src/plugin.ts create mode 100644 plugins/pagerduty/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 65ba6140b2836..487e4ad0c568a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,6 +21,7 @@ "@backstage/plugin-kubernetes": "^0.1.1-alpha.25", "@backstage/plugin-lighthouse": "^0.1.1-alpha.25", "@backstage/plugin-newrelic": "^0.1.1-alpha.25", + "@backstage/plugin-pagerduty": "^0.1.1-alpha.25", "@backstage/plugin-register-component": "^0.1.1-alpha.25", "@backstage/plugin-rollbar": "^0.1.1-alpha.25", "@backstage/plugin-scaffolder": "^0.1.1-alpha.25", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index bdc38513186b7..3851556882379 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -38,3 +38,4 @@ export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights'; export { plugin as UserSettings } from '@backstage/plugin-user-settings'; +export { plugin as Pagerduty } from '@backstage/plugin-pagerduty'; diff --git a/plugins/pagerduty/.eslintrc.js b/plugins/pagerduty/.eslintrc.js new file mode 100644 index 0000000000000..13573efa9c466 --- /dev/null +++ b/plugins/pagerduty/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md new file mode 100644 index 0000000000000..0335cb5a9bbba --- /dev/null +++ b/plugins/pagerduty/README.md @@ -0,0 +1,13 @@ +# pagerduty + +Welcome to the pagerduty plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx new file mode 100644 index 0000000000000..264d6f801f5a2 --- /dev/null +++ b/plugins/pagerduty/dev/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json new file mode 100644 index 0000000000000..33054c6163aff --- /dev/null +++ b/plugins/pagerduty/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-pagerduty", + "version": "0.1.1-alpha.25", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.25", + "@backstage/theme": "^0.1.1-alpha.25", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.25", + "@backstage/dev-utils": "^0.1.1-alpha.25", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts new file mode 100644 index 0000000000000..224e29389089d --- /dev/null +++ b/plugins/pagerduty/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { plugin } from './plugin'; diff --git a/plugins/pagerduty/src/plugin.test.ts b/plugins/pagerduty/src/plugin.test.ts new file mode 100644 index 0000000000000..8d4545ac12fd4 --- /dev/null +++ b/plugins/pagerduty/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { plugin } from './plugin'; + +describe('pagerduty', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts new file mode 100644 index 0000000000000..8f8523b78bae1 --- /dev/null +++ b/plugins/pagerduty/src/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createPlugin, createRouteRef } from '@backstage/core'; + +export const rootRouteRef = createRouteRef({ + path: '/pagerduty', + title: 'pagerduty', +}); + +export const plugin = createPlugin({ + id: 'pagerduty', +}); diff --git a/plugins/pagerduty/src/setupTests.ts b/plugins/pagerduty/src/setupTests.ts new file mode 100644 index 0000000000000..d7857386de33a --- /dev/null +++ b/plugins/pagerduty/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 '@testing-library/jest-dom'; + +global.fetch = require('node-fetch'); From 47e9c6d56bd8fac1cb3d06cfefeb31e5646a9235 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 20 Oct 2020 09:55:32 +0200 Subject: [PATCH 02/52] add main functionality --- .../src/components/PagerDutyServiceCard.tsx | 294 ++++++++++++++++++ plugins/pagerduty/src/components/pd.svg | 13 + plugins/pagerduty/src/index.ts | 4 + 3 files changed, 311 insertions(+) create mode 100644 plugins/pagerduty/src/components/PagerDutyServiceCard.tsx create mode 100644 plugins/pagerduty/src/components/pd.svg diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx new file mode 100644 index 0000000000000..3dadacadfbf7e --- /dev/null +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -0,0 +1,294 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + InfoCard, + StatusError, + StatusWarning, + StatusOK, +} from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { + List, + ListSubheader, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + Tooltip, + ListItemText, + makeStyles, + IconButton, +} from '@material-ui/core'; +import moment from 'moment'; +import Pagerduty from './pd.svg'; +import UserIcon from '@material-ui/icons/Person'; +import EmailIcon from '@material-ui/icons/Email'; + +const useStyles = makeStyles({ + buttonContainer: {}, + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + svgButtonImage: { + height: '1em', + }, +}); + +type IncidentListProps = { + incidents: any; +}; + +const IncidentList = ({ incidents }: IncidentListProps) => { + const classes = useStyles(); + return incidents.map((incident: any) => ( + + + +
+ {incident.status === 'triggered' ? ( + + ) : ( + + )} +
+
+
+ + Created {moment(incident.createdAt).fromNow()}, assigned to{' '} + {(incident.assignees.length && incident.assignees[0].name) || + 'nobody'} + + } + /> + + + + View in PagerDuty + + + +
+ )); +}; + +const IncidentsEmptyState = () => { + const classes = useStyles(); + return ( + + +
+ +
+
+ +
+ ); +}; + +type IncidentsProps = { + incidents: Array; +}; +export const Incidents = ({ incidents }: IncidentsProps) => ( + Incidents}> + {incidents.length ? ( + + ) : ( + + )} + +); + +type EscalationProps = { + escalation: PagerDutyUserData[]; +}; + +const EscalationUsers = ({ escalation }: EscalationProps) => { + const classes = useStyles(); + const escalations = escalation.map((user, index) => ( + + + + + + + + + + + + + + View in PagerDuty + + + + + )); + return
{escalations}
; +}; + +const EscalationUsersEmptyState = () => { + const classes = useStyles(); + return ( + + +
+ +
+
+ +
+ ); +}; + +const EscalationPolicy = ({ escalation }: EscalationProps) => ( + Escalation Policy}> + {escalation.length ? ( + + ) : ( + + )} + +); + +type CardBodyProps = { + entity: Entity; + data: PagerDutyData; +}; + +const CardBody = ({ entity, data }: CardBodyProps) => { + const { pagerDutyServices } = data; + const classes = useStyles(); + + if (!pagerDutyServices.length) { + return ( +
+ 'The specified PagerDuty integration key does not match any PagerDuty + service.' +
+ ); + } + + const { activeIncidents, escalationPolicy } = pagerDutyServices[0]; + + return ( + <> + + +
+ {/* */} +
+ + ); +}; + +type PagerDutyIncident = { + id: string; + title: string; + status: string; + createdAt: string; + homepageUrl: string; + assignees: Partial[]; +}; + +type PagerDutyUserData = { + email: string; + name: string; + homepageUrl: string; + id: string; +}; + +type PagerDutyServicesData = { + activeIncidents: PagerDutyIncident[]; + escalationPolicy: PagerDutyUserData[]; + id: string; + name: string; + homepageUrl: string; +}; + +type PagerDutyData = { + pagerDutyServices: PagerDutyServicesData[]; +}; + +type Props = { + entity: Entity; +}; + +export const PagerDutyServiceCard = ({ entity }: Props) => { + // TODO: fetch this data + const mockData: PagerDutyData = { + pagerDutyServices: [ + { + activeIncidents: [ + { + id: 'id', + title: 'something happened', + status: 'triggered', + createdAt: '2020:01:01', + homepageUrl: 'url', + assignees: [ + { + name: 'name', + }, + ], + }, + ], + escalationPolicy: [ + { + email: 'user@example.com', + name: 'Name', + homepageUrl: 'https://spotify.pagerduty.com/users/FOO', + id: 'user id', + }, + ], + id: 'id', + name: 'name', + homepageUrl: 'https://spotify.pagety.com/service-directory/BAR', + }, + ], + }; + + return ( + + + + ); +}; + +export const isPluginApplicableToEntity = (entity: Entity) => true; diff --git a/plugins/pagerduty/src/components/pd.svg b/plugins/pagerduty/src/components/pd.svg new file mode 100644 index 0000000000000..237ffa2011254 --- /dev/null +++ b/plugins/pagerduty/src/components/pd.svg @@ -0,0 +1,13 @@ + + + + pd_icon + Created with Sketch. + + + + + + + + diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 224e29389089d..b24018887a8d2 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -14,3 +14,7 @@ * limitations under the License. */ export { plugin } from './plugin'; +export { + isPluginApplicableToEntity, + PagerDutyServiceCard, +} from './components/PagerDutyServiceCard'; From e8854140c3f8af226f3d2a1bf8acaadb562deab2 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 20 Oct 2020 09:56:55 +0200 Subject: [PATCH 03/52] add Pagerduty card to entity overview --- packages/app/src/components/catalog/EntityPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index f30e7ff34fc3b..51467fc2bbecc 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -66,6 +66,10 @@ import { isPluginApplicableToEntity as isPullRequestsAvailable, PullRequestsStatsCard, } from '@roadiehq/backstage-plugin-github-pull-requests'; +import { + isPluginApplicableToEntity as isPagerDutyAvailable, + PagerDutyServiceCard, +} from '@backstage/plugin-pagerduty'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -131,6 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( + {isPagerDutyAvailable(entity) && ( + + + + )} {isGitHubAvailable(entity) && ( <> From 82fbe62743b45cbab7d3bac590532f1baabdbb64 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 20 Oct 2020 11:21:18 +0200 Subject: [PATCH 04/52] splitted up component to separate files --- .../pagerduty/src/components/Escalation.tsx | 112 ++++++++ .../pagerduty/src/components/Incidents.tsx | 120 +++++++++ .../src/components/PagerDutyServiceCard.tsx | 253 ++---------------- plugins/pagerduty/src/components/types.tsx | 27 ++ 4 files changed, 280 insertions(+), 232 deletions(-) create mode 100644 plugins/pagerduty/src/components/Escalation.tsx create mode 100644 plugins/pagerduty/src/components/Incidents.tsx create mode 100644 plugins/pagerduty/src/components/types.tsx diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx new file mode 100644 index 0000000000000..217b93a400527 --- /dev/null +++ b/plugins/pagerduty/src/components/Escalation.tsx @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + List, + ListSubheader, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + Tooltip, + ListItemText, + makeStyles, + IconButton, +} from '@material-ui/core'; +import UserIcon from '@material-ui/icons/Person'; +import EmailIcon from '@material-ui/icons/Email'; +import { StatusWarning } from '@backstage/core'; +import Pagerduty from './pd.svg'; +import { PagerDutyUserData } from './types'; + +const useStyles = makeStyles({ + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + svgButtonImage: { + height: '1em', + }, +}); + +type EscalationUserProps = { + user: PagerDutyUserData; +}; + +const EscalationUser = ({ user }: EscalationUserProps) => { + const classes = useStyles(); + return ( + + + + + + + + + + + + + + View in PagerDuty + + + + + ); +}; + +const EscalationUsersEmptyState = () => { + const classes = useStyles(); + return ( + + +
+ +
+
+ +
+ ); +}; + +type EscalationPolicyProps = { + escalation: PagerDutyUserData[]; +}; + +export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => ( + Escalation Policy}> + {escalation.length ? ( + escalation.map((user, index) => ( + + )) + ) : ( + + )} + +); diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx new file mode 100644 index 0000000000000..6f6d194a77c8c --- /dev/null +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + Tooltip, + ListItemText, + makeStyles, + IconButton, + ListSubheader, +} from '@material-ui/core'; +import { StatusError, StatusWarning, StatusOK } from '@backstage/core'; +import Pagerduty from './pd.svg'; +import moment from 'moment'; + +const useStyles = makeStyles({ + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + svgButtonImage: { + height: '1em', + }, +}); + +const IncidentsEmptyState = () => { + const classes = useStyles(); + return ( + + +
+ +
+
+ +
+ ); +}; + +type IncidentListProps = { + incidents: any; +}; + +const IncidentList = ({ incidents }: IncidentListProps) => { + const classes = useStyles(); + return incidents.map((incident: any) => ( + + + +
+ {incident.status === 'triggered' ? ( + + ) : ( + + )} +
+
+
+ + Created {moment(incident.createdAt).fromNow()}, assigned to{' '} + {(incident.assignees.length && incident.assignees[0].name) || + 'nobody'} + + } + /> + + + + View in PagerDuty + + + +
+ )); +}; + +type IncidentsProps = { + incidents: Array; +}; + +export const Incidents = ({ incidents }: IncidentsProps) => ( + Incidents}> + {incidents.length ? ( + + ) : ( + + )} + +); diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index 3dadacadfbf7e..cd51c4bcee994 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -14,237 +14,17 @@ * limitations under the License. */ import React from 'react'; -import { - InfoCard, - StatusError, - StatusWarning, - StatusOK, -} from '@backstage/core'; +import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { - List, - ListSubheader, - ListItem, - ListItemIcon, - ListItemSecondaryAction, - Tooltip, - ListItemText, - makeStyles, - IconButton, -} from '@material-ui/core'; -import moment from 'moment'; -import Pagerduty from './pd.svg'; -import UserIcon from '@material-ui/icons/Person'; -import EmailIcon from '@material-ui/icons/Email'; +import { Grid, Button } from '@material-ui/core'; +import { Incidents } from './Incidents'; +import { EscalationPolicy } from './Escalation'; +import { PagerDutyData } from './types'; -const useStyles = makeStyles({ - buttonContainer: {}, - denseListIcon: { - marginRight: 0, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - }, - svgButtonImage: { - height: '1em', - }, -}); +const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; -type IncidentListProps = { - incidents: any; -}; - -const IncidentList = ({ incidents }: IncidentListProps) => { - const classes = useStyles(); - return incidents.map((incident: any) => ( - - - -
- {incident.status === 'triggered' ? ( - - ) : ( - - )} -
-
-
- - Created {moment(incident.createdAt).fromNow()}, assigned to{' '} - {(incident.assignees.length && incident.assignees[0].name) || - 'nobody'} - - } - /> - - - - View in PagerDuty - - - -
- )); -}; - -const IncidentsEmptyState = () => { - const classes = useStyles(); - return ( - - -
- -
-
- -
- ); -}; - -type IncidentsProps = { - incidents: Array; -}; -export const Incidents = ({ incidents }: IncidentsProps) => ( - Incidents}> - {incidents.length ? ( - - ) : ( - - )} - -); - -type EscalationProps = { - escalation: PagerDutyUserData[]; -}; - -const EscalationUsers = ({ escalation }: EscalationProps) => { - const classes = useStyles(); - const escalations = escalation.map((user, index) => ( - - - - - - - - - - - - - - View in PagerDuty - - - - - )); - return
{escalations}
; -}; - -const EscalationUsersEmptyState = () => { - const classes = useStyles(); - return ( - - -
- -
-
- -
- ); -}; - -const EscalationPolicy = ({ escalation }: EscalationProps) => ( - Escalation Policy}> - {escalation.length ? ( - - ) : ( - - )} - -); - -type CardBodyProps = { - entity: Entity; - data: PagerDutyData; -}; - -const CardBody = ({ entity, data }: CardBodyProps) => { - const { pagerDutyServices } = data; - const classes = useStyles(); - - if (!pagerDutyServices.length) { - return ( -
- 'The specified PagerDuty integration key does not match any PagerDuty - service.' -
- ); - } - - const { activeIncidents, escalationPolicy } = pagerDutyServices[0]; - - return ( - <> - - -
- {/* */} -
- - ); -}; - -type PagerDutyIncident = { - id: string; - title: string; - status: string; - createdAt: string; - homepageUrl: string; - assignees: Partial[]; -}; - -type PagerDutyUserData = { - email: string; - name: string; - homepageUrl: string; - id: string; -}; - -type PagerDutyServicesData = { - activeIncidents: PagerDutyIncident[]; - escalationPolicy: PagerDutyUserData[]; - id: string; - name: string; - homepageUrl: string; -}; - -type PagerDutyData = { - pagerDutyServices: PagerDutyServicesData[]; -}; +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); type Props = { entity: Entity; @@ -284,11 +64,20 @@ export const PagerDutyServiceCard = ({ entity }: Props) => { ], }; - return ( + const { activeIncidents, escalationPolicy } = mockData.pagerDutyServices[0]; + + return isPluginApplicableToEntity(entity) ? ( + + ) : ( - + + + + {/* */} + + ); }; - -export const isPluginApplicableToEntity = (entity: Entity) => true; diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx new file mode 100644 index 0000000000000..b3b94f02007ff --- /dev/null +++ b/plugins/pagerduty/src/components/types.tsx @@ -0,0 +1,27 @@ +export type PagerDutyIncident = { + id: string; + title: string; + status: string; + createdAt: string; + homepageUrl: string; + assignees: Partial[]; +}; + +export type PagerDutyUserData = { + email: string; + name: string; + homepageUrl: string; + id: string; +}; + +export type PagerDutyServicesData = { + activeIncidents: PagerDutyIncident[]; + escalationPolicy: any; // PagerDutyUserData[]; + id: string; + name: string; + homepageUrl: string; +}; + +export type PagerDutyData = { + pagerDutyServices: PagerDutyServicesData[]; +}; From 4dba9ea97ae30e1e5b83ee6dcf251c141b41519c Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 21 Oct 2020 10:04:05 +0200 Subject: [PATCH 05/52] add TriggerButton and TriggerDialog, refactor them --- .../src/components/PagerDutyServiceCard.tsx | 21 ++- .../src/components/TriggerButton.tsx | 68 +++++++++ .../src/components/TriggerDialog.tsx | 131 ++++++++++++++++++ 3 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 plugins/pagerduty/src/components/TriggerButton.tsx create mode 100644 plugins/pagerduty/src/components/TriggerDialog.tsx diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index cd51c4bcee994..e60575ee9beb0 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -16,10 +16,11 @@ import React from 'react'; import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { Grid, Button } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; import { Incidents } from './Incidents'; import { EscalationPolicy } from './Escalation'; import { PagerDutyData } from './types'; +import { TriggerButton } from './TriggerButton'; const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -64,19 +65,25 @@ export const PagerDutyServiceCard = ({ entity }: Props) => { ], }; - const { activeIncidents, escalationPolicy } = mockData.pagerDutyServices[0]; + const { + activeIncidents, + escalationPolicy, + homepageUrl, + } = mockData.pagerDutyServices[0]; + + const link = { + title: 'View in PagerDuty', + link: homepageUrl, + }; return isPluginApplicableToEntity(entity) ? ( ) : ( - + - {/* */} - + ); diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx new file mode 100644 index 0000000000000..578b86d50b282 --- /dev/null +++ b/plugins/pagerduty/src/components/TriggerButton.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { useState } from 'react'; +import { Button } from '@material-ui/core'; +import { TriggerDialog } from './TriggerDialog'; +import { Entity } from '@backstage/catalog-model'; + +type Props = { + entity: Entity; +}; + +export const TriggerButton = ({ entity }: Props) => { + const [showDialog, setShowDialog] = useState(false); + + const handleDialog = () => { + setShowDialog(!showDialog); + }; + + // const onTriggerAlarm = async (description: string) => { + // try { + // //TODO: call method from pagerduty client + // alertApi.post({ + // message: `Alarm successfully triggered by ${userId}`, + // }); + // } catch (error) { + // alertApi.post({ + // message: `Failed to trigger alarm, ${error.message}`, + // severity: 'error', + // }); + // throw error; + // } + // }; + + return ( + <> + + {showDialog && ( + + )} + + ); +}; diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx new file mode 100644 index 0000000000000..29378cce63fa5 --- /dev/null +++ b/plugins/pagerduty/src/components/TriggerDialog.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + TextField, + makeStyles, + DialogActions, + Button, + DialogContent, + FormControl, + FormHelperText, +} from '@material-ui/core'; +import { Progress, useApi, alertApiRef, identityApiRef } from '@backstage/core'; +import { useAsyncFn } from 'react-use'; + +const useStyles = makeStyles({ + warningText: { + border: '1px solid rgba(245, 155, 35, 0.5)', + backgroundColor: 'rgba(245, 155, 35, 0.2)', + padding: '0.5em 1em', + }, +}); + +type Props = { + name: string; + integrationKey: string; + onClose: () => void; +}; + +export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { + const classes = useStyles(); + const [description, setDescription] = useState(''); + const alertApi = useApi(alertApiRef); + const identityApi = useApi(identityApiRef); + const userId = identityApi.getUserId(); + + const descriptionChanged = (event: any) => { + setDescription(event.target.value); + }; + + const promiseFunc = () => + new Promise((resolve, reject) => { + setTimeout(() => { + reject('Inside test await'); + }, 1000); + }); + + const [{ value, loading, error }, triggerAlarm] = useAsyncFn(async () => { + return await promiseFunc(); + }); + + useEffect(() => { + if (value) { + alertApi.post({ + message: `Alarm successfully triggered by ${userId}`, + }); + onClose(); + } + + if (error) { + alertApi.post({ + message: `Failed to trigger alarm, ${error.message}`, + severity: 'error', + }); + } + }, [value, error]); + + return ( + + + This action will send PagerDuty alarms and notifications to on-call + people responsible for {name}. + + +

+ Note: If the issue you are seeing does not need urgent attention, + please get in touch with the responsible team using their preferred + communications channel. For most views, you can find links to support + and information channels by clicking the support button in the top + right corner of the page. If the issue is urgent, please don't + hesitate to trigger the alert. +

+

+ Please describe the problem you want to report. Be as descriptive as + possible. Your Spotify username and a reference to the current page + will automatically be sent amended to the alarm so that we can debug + the issue and reach out to you if necessary. +

+ +
+ + + + + {loading && } +
+ ); +}; From 4e93108ba4d94ea9818777604e02f9b3831aff08 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 21 Oct 2020 14:12:52 +0200 Subject: [PATCH 06/52] trigger pager alarm --- plugins/pagerduty/package.json | 4 +- plugins/pagerduty/src/api/pagerDutyClient.ts | 73 +++++++++++++++++++ .../src/{components => assets}/pd.svg | 0 .../pagerduty/src/components/Escalation.tsx | 2 +- .../pagerduty/src/components/Incidents.tsx | 2 +- .../src/components/PagerDutyServiceCard.tsx | 4 +- .../src/components/TriggerButton.tsx | 21 +----- .../src/components/TriggerDialog.tsx | 31 ++++---- 8 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 plugins/pagerduty/src/api/pagerDutyClient.ts rename plugins/pagerduty/src/{components => assets}/pd.svg (100%) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 33054c6163aff..e5d0264be3dda 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -22,12 +22,14 @@ "dependencies": { "@backstage/core": "^0.1.1-alpha.25", "@backstage/theme": "^0.1.1-alpha.25", + "@backstage/catalog-model": "^0.1.1-alpha.25", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^15.3.3", + "moment": "^2.27.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts new file mode 100644 index 0000000000000..d5db842dab999 --- /dev/null +++ b/plugins/pagerduty/src/api/pagerDutyClient.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +type Options = { + method: string; + headers: { + 'Content-Type': string; + Accept: string; + }; + body: string; +}; + +const request = async ( + url: string, + options: Options, +): Promise => { + const response = await fetch(url, options); + + if (!response.ok) { + const payload = await response.json(); + const errors = payload.errors.map((error: string) => error).join(' '); + const message = `Request failed with ${response.status}, ${errors}`; + + throw new Error(message); + } + + return await response.json(); +}; + +export function triggerPagerDutyAlarm( + integrationKey: string, + source: string, + description: string, + userName: string, +) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json, text/plain, */*', + }, + body: JSON.stringify({ + event_action: 'trigger', + routing_key: integrationKey, + client: 'Backstage', + client_url: source, + payload: { + summary: description, + source: source, + severity: 'error', + class: 'manual trigger', + custom_details: { + user: userName, + }, + }, + }), + }; + + return request('https://events.pagerduty.com/v2/enqueue', options); +} diff --git a/plugins/pagerduty/src/components/pd.svg b/plugins/pagerduty/src/assets/pd.svg similarity index 100% rename from plugins/pagerduty/src/components/pd.svg rename to plugins/pagerduty/src/assets/pd.svg diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx index 217b93a400527..74e253d51613e 100644 --- a/plugins/pagerduty/src/components/Escalation.tsx +++ b/plugins/pagerduty/src/components/Escalation.tsx @@ -29,7 +29,7 @@ import { import UserIcon from '@material-ui/icons/Person'; import EmailIcon from '@material-ui/icons/Email'; import { StatusWarning } from '@backstage/core'; -import Pagerduty from './pd.svg'; +import Pagerduty from '../assets/pd.svg'; import { PagerDutyUserData } from './types'; const useStyles = makeStyles({ diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx index 6f6d194a77c8c..98a26c66dbaec 100644 --- a/plugins/pagerduty/src/components/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -27,7 +27,7 @@ import { ListSubheader, } from '@material-ui/core'; import { StatusError, StatusWarning, StatusOK } from '@backstage/core'; -import Pagerduty from './pd.svg'; +import Pagerduty from '../assets/pd.svg'; import moment from 'moment'; const useStyles = makeStyles({ diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index e60575ee9beb0..e0564d76ce183 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -22,7 +22,7 @@ import { EscalationPolicy } from './Escalation'; import { PagerDutyData } from './types'; import { TriggerButton } from './TriggerButton'; -const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; +export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); @@ -76,7 +76,7 @@ export const PagerDutyServiceCard = ({ entity }: Props) => { link: homepageUrl, }; - return isPluginApplicableToEntity(entity) ? ( + return !isPluginApplicableToEntity(entity) ? ( ) : ( diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx index 578b86d50b282..eb051e68fa0d6 100644 --- a/plugins/pagerduty/src/components/TriggerButton.tsx +++ b/plugins/pagerduty/src/components/TriggerButton.tsx @@ -18,6 +18,7 @@ import React, { useState } from 'react'; import { Button } from '@material-ui/core'; import { TriggerDialog } from './TriggerDialog'; import { Entity } from '@backstage/catalog-model'; +import { PAGERDUTY_INTEGRATION_KEY } from './PagerDutyServiceCard'; type Props = { entity: Entity; @@ -30,21 +31,6 @@ export const TriggerButton = ({ entity }: Props) => { setShowDialog(!showDialog); }; - // const onTriggerAlarm = async (description: string) => { - // try { - // //TODO: call method from pagerduty client - // alertApi.post({ - // message: `Alarm successfully triggered by ${userId}`, - // }); - // } catch (error) { - // alertApi.post({ - // message: `Failed to trigger alarm, ${error.message}`, - // severity: 'error', - // }); - // throw error; - // } - // }; - return ( <> From fe2d8ac7152fc3a2d88df1dea66750f4dffa732a Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 22 Oct 2020 10:18:46 +0200 Subject: [PATCH 07/52] wip:fetching data from Api --- .../app/src/components/catalog/EntityPage.tsx | 10 +++---- plugins/pagerduty/src/api/pagerDutyClient.ts | 29 +++++++++++++++++-- .../src/components/PagerDutyServiceCard.tsx | 24 ++++++++++++++- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 51467fc2bbecc..7629c86a1e70a 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -135,11 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( - {isPagerDutyAvailable(entity) && ( - - - - )} + {/* {isPagerDutyAvailable(entity) && ( */} + + + + {/* )} */} {isGitHubAvailable(entity) && ( <> diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts index d5db842dab999..fa42c82f9eb54 100644 --- a/plugins/pagerduty/src/api/pagerDutyClient.ts +++ b/plugins/pagerduty/src/api/pagerDutyClient.ts @@ -14,18 +14,22 @@ * limitations under the License. */ +const API_URL = 'https://api.pagerduty.com'; +const EVENTS_API_URL = 'https://events.pagerduty.com/v2'; + type Options = { method: string; headers: { 'Content-Type': string; Accept: string; + Authorization?: string; }; - body: string; + body?: string; }; const request = async ( url: string, - options: Options, + options: any, //Options, ): Promise => { const response = await fetch(url, options); @@ -40,6 +44,25 @@ const request = async ( return await response.json(); }; +export const getServices = async (token: string, integrationKey: string) => { + const options = { + method: 'GET', + headers: { + Authorization: `Token token=${token}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }, + // query: { + // query: encodeURIComponent(`key:${integrationKey}`), + // }, + }; + + return request( + `${API_URL}/services/?query=key%253A238b701cc9d048f0bdd828355178eafe`, + options, + ); +}; + export function triggerPagerDutyAlarm( integrationKey: string, source: string, @@ -69,5 +92,5 @@ export function triggerPagerDutyAlarm( }), }; - return request('https://events.pagerduty.com/v2/enqueue', options); + return request(`${EVENTS_API_URL}/enqueue`, options); } diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index e0564d76ce183..917ae27090b03 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -14,13 +14,20 @@ * limitations under the License. */ import React from 'react'; -import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core'; +import { + InfoCard, + MissingAnnotationEmptyState, + useApi, + configApiRef, +} from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { Grid } from '@material-ui/core'; import { Incidents } from './Incidents'; import { EscalationPolicy } from './Escalation'; import { PagerDutyData } from './types'; import { TriggerButton } from './TriggerButton'; +import { useAsync } from 'react-use'; +import { getServices } from '../api/pagerDutyClient'; export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -32,6 +39,20 @@ type Props = { }; export const PagerDutyServiceCard = ({ entity }: Props) => { + const configApi = useApi(configApiRef); + const pagerDutyToken = + configApi.getOptionalString('pagerduty.api_token') ?? undefined; + + console.log({ pagerDutyToken }); + const { value, loading, error } = useAsync(async () => { + return await getServices( + pagerDutyToken!, + entity.metadata.annotations![PAGERDUTY_INTEGRATION_KEY], + ); + }); + if (value) console.log(value); + if (error) throw new Error(`Error in getting services, ${error}`); + // TODO: fetch this data const mockData: PagerDutyData = { pagerDutyServices: [ @@ -85,6 +106,7 @@ export const PagerDutyServiceCard = ({ entity }: Props) => { + {/* todo show something when we dont have token */} ); }; From 0609e726936e457c0407fa83ba8783409275f864 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 22 Oct 2020 15:12:38 +0200 Subject: [PATCH 08/52] add pagerduty backend --- packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 + packages/backend/src/plugins/pagerduty.ts | 27 ++++++++ plugins/pagerduty-backend/.eslintrc.js | 3 + plugins/pagerduty-backend/README.md | 13 ++++ plugins/pagerduty-backend/package.json | 40 ++++++++++++ plugins/pagerduty-backend/src/index.ts | 17 +++++ plugins/pagerduty-backend/src/run.ts | 33 ++++++++++ .../src/service/router.test.ts | 45 ++++++++++++++ .../pagerduty-backend/src/service/router.ts | 42 +++++++++++++ .../src/service/standaloneServer.ts | 62 +++++++++++++++++++ plugins/pagerduty-backend/src/setupTests.ts | 18 ++++++ 12 files changed, 304 insertions(+) create mode 100644 packages/backend/src/plugins/pagerduty.ts create mode 100644 plugins/pagerduty-backend/.eslintrc.js create mode 100644 plugins/pagerduty-backend/README.md create mode 100644 plugins/pagerduty-backend/package.json create mode 100644 plugins/pagerduty-backend/src/index.ts create mode 100644 plugins/pagerduty-backend/src/run.ts create mode 100644 plugins/pagerduty-backend/src/service/router.test.ts create mode 100644 plugins/pagerduty-backend/src/service/router.ts create mode 100644 plugins/pagerduty-backend/src/service/standaloneServer.ts create mode 100644 plugins/pagerduty-backend/src/setupTests.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index ef55b007a7b45..c31629e82e0e4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -31,6 +31,7 @@ "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.25", "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25", + "@backstage/plugin-pagerduty-backend": "^0.1.1-alpha.25", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 18fd44f0891b3..0ca17c7d7fb01 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -46,6 +46,7 @@ import techdocs from './plugins/techdocs'; import graphql from './plugins/graphql'; import app from './plugins/app'; import { PluginEnvironment } from './types'; +import pagerduty from './plugins/pagerduty'; function makeCreateEnv(config: ConfigReader) { const root = getRootLogger(); @@ -79,6 +80,7 @@ async function main() { const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); + const pagerdutyEnv = useHotMemoize(module, () => createEnv('pagerduty')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -90,6 +92,7 @@ async function main() { apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); + apiRouter.use('/pagerduty', await pagerduty(pagerdutyEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/backend/src/plugins/pagerduty.ts b/packages/backend/src/plugins/pagerduty.ts new file mode 100644 index 0000000000000..6dc3d8c9ba1cc --- /dev/null +++ b/packages/backend/src/plugins/pagerduty.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from '@backstage/plugin-pagerduty-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ + logger, + config, + }); +} diff --git a/plugins/pagerduty-backend/.eslintrc.js b/plugins/pagerduty-backend/.eslintrc.js new file mode 100644 index 0000000000000..16a033dbc6027 --- /dev/null +++ b/plugins/pagerduty-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/pagerduty-backend/README.md b/plugins/pagerduty-backend/README.md new file mode 100644 index 0000000000000..099a94a85f500 --- /dev/null +++ b/plugins/pagerduty-backend/README.md @@ -0,0 +1,13 @@ +# pagerduty + +Welcome to the pagerduty backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/pagerduty-backend/package.json b/plugins/pagerduty-backend/package.json new file mode 100644 index 0000000000000..cad4ba268ea59 --- /dev/null +++ b/plugins/pagerduty-backend/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-pagerduty-backend", + "version": "0.1.1-alpha.25", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.1.1-alpha.25", + "@backstage/config": "^0.1.1-alpha.25", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "winston": "^3.2.1", + "node-fetch": "^2.6.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.25", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.20.5" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/pagerduty-backend/src/index.ts b/plugins/pagerduty-backend/src/index.ts new file mode 100644 index 0000000000000..7612c392a2c33 --- /dev/null +++ b/plugins/pagerduty-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 * from './service/router'; diff --git a/plugins/pagerduty-backend/src/run.ts b/plugins/pagerduty-backend/src/run.ts new file mode 100644 index 0000000000000..b96989e4b85ab --- /dev/null +++ b/plugins/pagerduty-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/pagerduty-backend/src/service/router.test.ts b/plugins/pagerduty-backend/src/service/router.test.ts new file mode 100644 index 0000000000000..0aaeafa379667 --- /dev/null +++ b/plugins/pagerduty-backend/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts new file mode 100644 index 0000000000000..eb7cc734c2536 --- /dev/null +++ b/plugins/pagerduty-backend/src/service/router.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { errorHandler } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; + config: Config; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.use(errorHandler()); + return router; +} diff --git a/plugins/pagerduty-backend/src/service/standaloneServer.ts b/plugins/pagerduty-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000000..73f928bef4dee --- /dev/null +++ b/plugins/pagerduty-backend/src/service/standaloneServer.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ +/* + * Copyright 2020 Spotify AB + * + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'pagerduty-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/pagerduty', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/pagerduty-backend/src/setupTests.ts b/plugins/pagerduty-backend/src/setupTests.ts new file mode 100644 index 0000000000000..a5907fd52ff07 --- /dev/null +++ b/plugins/pagerduty-backend/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 {}; +global.fetch = require('node-fetch'); From 6d1f4a996d25f93a6b5a0c5f8f605f58048d3e2f Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 23 Oct 2020 13:14:08 +0200 Subject: [PATCH 09/52] fetching data from pagerduty endpoint in interval --- .../pagerduty-backend/src/service/router.ts | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts index eb7cc734c2536..192a22019a783 100644 --- a/plugins/pagerduty-backend/src/service/router.ts +++ b/plugins/pagerduty-backend/src/service/router.ts @@ -14,11 +14,79 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { errorHandler, useHotCleanup } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import fetch from 'node-fetch'; + +const router = Router(); +const API_URL = 'https://api.pagerduty.com'; +const EVENTS_API_URL = 'https://events.pagerduty.com/v2'; + +type Options = { + method: string; + headers: { + 'Content-Type': string; + Accept: string; + Authorization?: string; + }; + body?: string; +}; + +async function request( + url: string, + options: any, // Options, +): Promise { + // TODO: handle errors better + const response = await fetch(url, options); + + if (!response.ok) { + const payload = await response.json(); + const errors = payload.errors.map((error: string) => error).join(' '); + const message = `Request failed with ${response.status}, ${errors}`; + + throw new Error(message); + } + + return await response.json(); +} + +async function services(token?: string) { + // TODO: handle missing token differently + if (!token) { + return 'Missing Token'; + } + const options = { + method: 'GET', + headers: { + Authorization: `Token token=${token}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }, + }; + let offset = 0; + // TODO: Create type for this data + const data: any = []; + + async function fetchData() { + const result: any = await request( + `${API_URL}/services?offset=${offset}`, + options, + ); + data.push(...result.services); + + // TODO: remove offset when we are done + if (offset < 4 && result.more) { + ++offset; + await fetchData(); + } + } + + await fetchData(); + return data; +} export interface RouterOptions { logger: Logger; @@ -28,15 +96,36 @@ export interface RouterOptions { export async function createRouter( options: RouterOptions, ): Promise { - const { logger } = options; + const { logger, config } = options; + const token = config.getOptionalString('pagerduty.api_token') ?? undefined; + + let cachedData: any = []; + + let cancel = false; + const intervalId = setInterval(async () => { + if (!cancel) { + cancel = true; + const result = await services(token); + cachedData = result; + cancel = false; + } + }, 10000); + + useHotCleanup(module, () => clearInterval(intervalId)); - const router = Router(); router.use(express.json()); + router.get('/services', async (_, response) => { + logger.info('pinged'); + + response.send({ services: cachedData, size: cachedData.length }); + }); + router.get('/health', (_, response) => { logger.info('PONG!'); response.send({ status: 'ok' }); }); + router.use(errorHandler()); return router; } From c9c212e064a365f2cffcfc7eb6a10eef5699a0ae Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 23 Oct 2020 14:59:36 +0200 Subject: [PATCH 10/52] added error handling --- .../pagerduty-backend/src/service/router.ts | 100 ++++++++++++------ plugins/pagerduty/src/api/pagerDutyClient.ts | 20 ---- .../src/components/PagerDutyServiceCard.tsx | 10 +- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts index 192a22019a783..1fbab9b55169f 100644 --- a/plugins/pagerduty-backend/src/service/router.ts +++ b/plugins/pagerduty-backend/src/service/router.ts @@ -20,6 +20,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import fetch from 'node-fetch'; +import { Response } from 'express'; const router = Router(); const API_URL = 'https://api.pagerduty.com'; @@ -38,19 +39,21 @@ type Options = { async function request( url: string, options: any, // Options, -): Promise { +): Promise { // TODO: handle errors better - const response = await fetch(url, options); - - if (!response.ok) { - const payload = await response.json(); - const errors = payload.errors.map((error: string) => error).join(' '); - const message = `Request failed with ${response.status}, ${errors}`; - - throw new Error(message); + try { + const response = await fetch(url, options); + + if (!response.ok) { + const payload = await response.json(); + const errors = payload.errors.map((error: string) => error).join(' '); + const message = `Request failed with ${response.status}, ${errors}`; + throw new Error(message); + } + return await response.json(); + } catch (error) { + throw new Error(error); } - - return await response.json(); } async function services(token?: string) { @@ -58,6 +61,7 @@ async function services(token?: string) { if (!token) { return 'Missing Token'; } + const options = { method: 'GET', headers: { @@ -66,25 +70,38 @@ async function services(token?: string) { 'Content-Type': 'application/json', }, }; + let offset = 0; // TODO: Create type for this data - const data: any = []; + const data: PagerDutyService[] = []; - async function fetchData() { - const result: any = await request( + async function getData() { + const result = await request( `${API_URL}/services?offset=${offset}`, options, ); - data.push(...result.services); + + data.push( + ...result.services.map(service => ({ + id: service.id, + name: service.name, + homepageUrl: service.html_url, + })), + ); // TODO: remove offset when we are done - if (offset < 4 && result.more) { + if (offset < 2 && result.more) { ++offset; - await fetchData(); + await getData(); } } - await fetchData(); + try { + await getData(); + } catch (error) { + throw new Error(error); + } + return data; } @@ -93,6 +110,19 @@ export interface RouterOptions { config: Config; } +type PagerDutyService = { + // activeIncidents: any[]; + // escalationPolicy: any[]; + id: string; + name: string; + homepageUrl: string; +}; + +type PagerDutyResponse = { + services: any[]; + more: boolean; +}; + export async function createRouter( options: RouterOptions, ): Promise { @@ -100,14 +130,21 @@ export async function createRouter( const token = config.getOptionalString('pagerduty.api_token') ?? undefined; let cachedData: any = []; + let errorMessage: string; let cancel = false; const intervalId = setInterval(async () => { if (!cancel) { cancel = true; - const result = await services(token); - cachedData = result; - cancel = false; + try { + logger.info('Fetching data from PagerDuty API'); + cachedData = await services(token); + } catch (error) { + logger.error(`Failed to fetch data, ${error.message}`); + errorMessage = error.message; + } finally { + cancel = false; + } } }, 10000); @@ -115,17 +152,18 @@ export async function createRouter( router.use(express.json()); - router.get('/services', async (_, response) => { - logger.info('pinged'); - - response.send({ services: cachedData, size: cachedData.length }); - }); - - router.get('/health', (_, response) => { - logger.info('PONG!'); - response.send({ status: 'ok' }); - }); + router.get( + '/services', + async (_, response: Response) => { + if (errorMessage) { + response.status(500).send({ error: errorMessage }); + } else { + response.send({ services: cachedData }); + } + }, + ); router.use(errorHandler()); + return router; } diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts index fa42c82f9eb54..dc2176680df65 100644 --- a/plugins/pagerduty/src/api/pagerDutyClient.ts +++ b/plugins/pagerduty/src/api/pagerDutyClient.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -const API_URL = 'https://api.pagerduty.com'; const EVENTS_API_URL = 'https://events.pagerduty.com/v2'; type Options = { @@ -44,25 +43,6 @@ const request = async ( return await response.json(); }; -export const getServices = async (token: string, integrationKey: string) => { - const options = { - method: 'GET', - headers: { - Authorization: `Token token=${token}`, - Accept: 'application/vnd.pagerduty+json;version=2', - 'Content-Type': 'application/json', - }, - // query: { - // query: encodeURIComponent(`key:${integrationKey}`), - // }, - }; - - return request( - `${API_URL}/services/?query=key%253A238b701cc9d048f0bdd828355178eafe`, - options, - ); -}; - export function triggerPagerDutyAlarm( integrationKey: string, source: string, diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index 917ae27090b03..b7fde8eae3445 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -27,7 +27,6 @@ import { EscalationPolicy } from './Escalation'; import { PagerDutyData } from './types'; import { TriggerButton } from './TriggerButton'; import { useAsync } from 'react-use'; -import { getServices } from '../api/pagerDutyClient'; export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -40,15 +39,12 @@ type Props = { export const PagerDutyServiceCard = ({ entity }: Props) => { const configApi = useApi(configApiRef); - const pagerDutyToken = - configApi.getOptionalString('pagerduty.api_token') ?? undefined; - console.log({ pagerDutyToken }); const { value, loading, error } = useAsync(async () => { - return await getServices( - pagerDutyToken!, - entity.metadata.annotations![PAGERDUTY_INTEGRATION_KEY], + const response = await fetch( + `${configApi.getString('backend.baseUrl')}/api/pagerduty/services`, ); + return await response.json(); }); if (value) console.log(value); if (error) throw new Error(`Error in getting services, ${error}`); From 361049d9c2c7e8209b383db73c28bf6caee6b79a Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 3 Nov 2020 11:26:23 +0100 Subject: [PATCH 11/52] Refactor component to use pagerduty restApi directly --- .../app/src/components/catalog/EntityPage.tsx | 10 +- plugins/pagerduty/src/api/pagerDutyClient.ts | 44 +++++++++ .../pagerduty/src/components/Escalation.tsx | 4 +- .../pagerduty/src/components/Incidents.tsx | 5 +- .../src/components/PagerDutyServiceCard.tsx | 99 +++++++++---------- 5 files changed, 102 insertions(+), 60 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 7629c86a1e70a..ddc8ed4b1302c 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -135,11 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( - {/* {isPagerDutyAvailable(entity) && ( */} - - - - {/* )} */} + {isPagerDutyAvailable(entity) && ( + + + + )} {isGitHubAvailable(entity) && ( <> diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts index dc2176680df65..dbdc6622f4d76 100644 --- a/plugins/pagerduty/src/api/pagerDutyClient.ts +++ b/plugins/pagerduty/src/api/pagerDutyClient.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +const API_URL = 'https://api.pagerduty.com'; const EVENTS_API_URL = 'https://events.pagerduty.com/v2'; type Options = { @@ -43,6 +44,49 @@ const request = async ( return await response.json(); }; +const getByUrl = async (url: string, token: string) => { + const options = { + method: 'GET', + headers: { + Authorization: `Token token=${token}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }, + }; + return await request(url, options); +}; + +export const getServiceByIntegrationKey = async ( + integrationKey: string, + token: string, +) => { + const response = await getByUrl( + `${API_URL}/services?include[]=integrations&include[]=escalation_policies&query=${integrationKey}`, + token, + ); + if (response.services.length > 1) { + throw new Error('More than one service in response'); + } + return response.services[0]; +}; + +export const getIncidentsByServiceId = async ( + serviceId: string, + token: string, +) => { + return await getByUrl( + `${API_URL}/incidents?service_ids[]=${serviceId}`, + token, + ); +}; + +export const getOncallByPolicyId = async (policyId: string, token: string) => { + return await getByUrl( + `${API_URL}/oncalls?include[]=users&escalation_policy_ids[]=${policyId}`, + token, + ); +}; + export function triggerPagerDutyAlarm( integrationKey: string, source: string, diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx index 74e253d51613e..69a4543c4bbaf 100644 --- a/plugins/pagerduty/src/components/Escalation.tsx +++ b/plugins/pagerduty/src/components/Escalation.tsx @@ -102,8 +102,8 @@ type EscalationPolicyProps = { export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => ( Escalation Policy}> {escalation.length ? ( - escalation.map((user, index) => ( - + escalation.map((item, index) => ( + )) ) : ( diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx index 98a26c66dbaec..d2f6fc7128b7c 100644 --- a/plugins/pagerduty/src/components/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -80,8 +80,9 @@ const IncidentList = ({ incidents }: IncidentListProps) => { primary={incident.title} secondary={ - Created {moment(incident.createdAt).fromNow()}, assigned to{' '} - {(incident.assignees.length && incident.assignees[0].name) || + Created {moment(incident.created_at).fromNow()}, assigned to{' '} + {(incident?.assignments[0]?.assignee?.summary && + incident.assignments[0].assignee.summary) || 'nobody'} } diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index b7fde8eae3445..e17e35a99660a 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -14,19 +14,18 @@ * limitations under the License. */ import React from 'react'; -import { - InfoCard, - MissingAnnotationEmptyState, - useApi, - configApiRef, -} from '@backstage/core'; +import { InfoCard, useApi, configApiRef } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { Grid } from '@material-ui/core'; +import { Grid, LinearProgress } from '@material-ui/core'; import { Incidents } from './Incidents'; import { EscalationPolicy } from './Escalation'; -import { PagerDutyData } from './types'; import { TriggerButton } from './TriggerButton'; import { useAsync } from 'react-use'; +import { + getServiceByIntegrationKey, + getIncidentsByServiceId, + getOncallByPolicyId, +} from '../api/pagerDutyClient'; export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -40,65 +39,63 @@ type Props = { export const PagerDutyServiceCard = ({ entity }: Props) => { const configApi = useApi(configApiRef); + // TODO: handle missing token + const token = configApi.getOptionalString('pagerduty.api.token') ?? undefined; + console.log({ token }); + const { value, loading, error } = useAsync(async () => { - const response = await fetch( - `${configApi.getString('backend.baseUrl')}/api/pagerduty/services`, + const integrationKey = entity.metadata.annotations![ + PAGERDUTY_INTEGRATION_KEY + ]; + + const service = await getServiceByIntegrationKey(integrationKey, token); + const incidents = await getIncidentsByServiceId( + (service as any).id /*// TODO: fix type */, + token, ); - return await response.json(); + const oncalls = await getOncallByPolicyId( + (service as any).escalation_policy.id, // TODO: fix type + token, + ); + + return { + pagerDutyServices: [ + { + activeIncidents: incidents, + escalationPolicy: oncalls, + id: service.id, + name: service.name, + homepageUrl: service.html_url, + }, + ], + }; }); - if (value) console.log(value); - if (error) throw new Error(`Error in getting services, ${error}`); - // TODO: fetch this data - const mockData: PagerDutyData = { - pagerDutyServices: [ - { - activeIncidents: [ - { - id: 'id', - title: 'something happened', - status: 'triggered', - createdAt: '2020:01:01', - homepageUrl: 'url', - assignees: [ - { - name: 'name', - }, - ], - }, - ], - escalationPolicy: [ - { - email: 'user@example.com', - name: 'Name', - homepageUrl: 'https://spotify.pagerduty.com/users/FOO', - id: 'user id', - }, - ], - id: 'id', - name: 'name', - homepageUrl: 'https://spotify.pagety.com/service-directory/BAR', - }, - ], - }; + if (error) { + // TODO: use the errorApi + console.log(error); + throw new Error(`Error in getting data: ${error.message}`); + } + + if (loading) { + return ; + } const { activeIncidents, escalationPolicy, homepageUrl, - } = mockData.pagerDutyServices[0]; + } = value!.pagerDutyServices[0]!; const link = { title: 'View in PagerDuty', link: homepageUrl, }; - return !isPluginApplicableToEntity(entity) ? ( - - ) : ( + return ( - - + + From 122410f1ab388d0bd48480d9d486c2b4ecd89212 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 5 Nov 2020 10:49:11 +0100 Subject: [PATCH 12/52] fix typings --- .../pagerduty/src/components/Escalation.tsx | 12 +-- .../pagerduty/src/components/Incidents.tsx | 15 ++-- plugins/pagerduty/src/components/types.tsx | 84 ++++++++++++++++--- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx index 69a4543c4bbaf..19c6a15096d9c 100644 --- a/plugins/pagerduty/src/components/Escalation.tsx +++ b/plugins/pagerduty/src/components/Escalation.tsx @@ -30,7 +30,7 @@ import UserIcon from '@material-ui/icons/Person'; import EmailIcon from '@material-ui/icons/Email'; import { StatusWarning } from '@backstage/core'; import Pagerduty from '../assets/pd.svg'; -import { PagerDutyUserData } from './types'; +import { Oncall, PagerDutyEscalationPolicy } from './types'; const useStyles = makeStyles({ denseListIcon: { @@ -45,11 +45,7 @@ const useStyles = makeStyles({ }, }); -type EscalationUserProps = { - user: PagerDutyUserData; -}; - -const EscalationUser = ({ user }: EscalationUserProps) => { +const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => { const classes = useStyles(); return ( @@ -65,7 +61,7 @@ const EscalationUser = ({ user }: EscalationUserProps) => { @@ -96,7 +92,7 @@ const EscalationUsersEmptyState = () => { }; type EscalationPolicyProps = { - escalation: PagerDutyUserData[]; + escalation: Oncall[]; }; export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => ( diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx index d2f6fc7128b7c..0117281a0ed20 100644 --- a/plugins/pagerduty/src/components/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -29,6 +29,7 @@ import { import { StatusError, StatusWarning, StatusOK } from '@backstage/core'; import Pagerduty from '../assets/pd.svg'; import moment from 'moment'; +import { Incident } from '../components/types'; const useStyles = makeStyles({ denseListIcon: { @@ -57,13 +58,13 @@ const IncidentsEmptyState = () => { ); }; -type IncidentListProps = { - incidents: any; +type IncidentListItemProps = { + incident: Incident; }; -const IncidentList = ({ incidents }: IncidentListProps) => { +const IncidentListItem = ({ incident }: IncidentListItemProps) => { const classes = useStyles(); - return incidents.map((incident: any) => ( + return ( @@ -103,17 +104,17 @@ const IncidentList = ({ incidents }: IncidentListProps) => { - )); + ); }; type IncidentsProps = { - incidents: Array; + incidents: Incident[]; }; export const Incidents = ({ incidents }: IncidentsProps) => ( Incidents}> {incidents.length ? ( - + incidents.map(incident => ) ) : ( )} diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx index b3b94f02007ff..377ae6d0eaded 100644 --- a/plugins/pagerduty/src/components/types.tsx +++ b/plugins/pagerduty/src/components/types.tsx @@ -1,27 +1,87 @@ -export type PagerDutyIncident = { +/* + * Copyright 2020 Spotify AB + * + * 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 Incident = { id: string; title: string; status: string; - createdAt: string; homepageUrl: string; - assignees: Partial[]; + assignments: [ + { + assignee: User; + }, + ]; + serviceId: string; + created_at: string; }; -export type PagerDutyUserData = { - email: string; - name: string; - homepageUrl: string; +export type Service = { id: string; + name: string; + html_url: string; + integrationKey: string; + escalation_policy: PagerDutyEscalationPolicy; +}; + +export type ResponseService = { + services: Service[]; + more: boolean; }; -export type PagerDutyServicesData = { - activeIncidents: PagerDutyIncident[]; - escalationPolicy: any; // PagerDutyUserData[]; +export type ResponseOncall = { id: string; name: string; + email: string; homepageUrl: string; }; -export type PagerDutyData = { - pagerDutyServices: PagerDutyServicesData[]; +export type Options = { + method: string; + headers: HeadersInit; + body?: BodyInit; +}; + +export type PagerDutyClientConfig = { + token?: string; +}; + +export type ServicesResponse = { + services: Service[]; +}; + +export type IncidentResponse = { + incidents: Incident[]; +}; + +export type OncallsResponse = { + oncalls: Oncall[]; +}; + +export type PagerDutyEscalationPolicy = { + user: User; +}; + +export type User = { + id: string; + summary: string; + email: string; + html_url: string; + name: string; +}; + +export type Oncall = { + escalation_policy: PagerDutyEscalationPolicy; + user: User; }; From f7614ee6fce099c4e50403f979fdcbf38956193a Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 5 Nov 2020 11:17:19 +0100 Subject: [PATCH 13/52] add utility api and use it in pluign --- plugins/pagerduty/src/api/pagerDutyClient.ts | 196 ++++++++++-------- .../pagerduty/src/components/Incidents.tsx | 4 +- .../src/components/PagerDutyServiceCard.tsx | 66 +++--- .../src/components/TriggerDialog.tsx | 5 +- plugins/pagerduty/src/plugin.ts | 18 +- 5 files changed, 158 insertions(+), 131 deletions(-) diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts index dbdc6622f4d76..09abaf856c68b 100644 --- a/plugins/pagerduty/src/api/pagerDutyClient.ts +++ b/plugins/pagerduty/src/api/pagerDutyClient.ts @@ -14,92 +14,78 @@ * limitations under the License. */ -const API_URL = 'https://api.pagerduty.com'; -const EVENTS_API_URL = 'https://events.pagerduty.com/v2'; - -type Options = { - method: string; - headers: { - 'Content-Type': string; - Accept: string; - Authorization?: string; - }; - body?: string; -}; - -const request = async ( - url: string, - options: any, //Options, -): Promise => { - const response = await fetch(url, options); - - if (!response.ok) { - const payload = await response.json(); - const errors = payload.errors.map((error: string) => error).join(' '); - const message = `Request failed with ${response.status}, ${errors}`; - - throw new Error(message); +import { createApiRef } from '@backstage/core'; +import { + Service, + Incident, + Options, + PagerDutyClientConfig, + ServicesResponse, + IncidentResponse, + OncallsResponse, + Oncall, +} from '../components/types'; + +export const pagerDutyApiRef = createApiRef({ + id: 'plugin.pagerduty.api', + description: 'Used to fetch data from PagerDuty API', +}); + +interface PagerDutyClient { + getServiceByIntegrationKey(integrationKey: string): Promise; + getIncidentsByServiceId(serviceId: string): Promise; + getOncallByPolicyId(policyId: string): Promise; +} + +export class PagerDutyClientApi implements PagerDutyClient { + private API_URL = 'https://api.pagerduty.com'; + private EVENTS_API_URL = 'https://events.pagerduty.com/v2'; + + constructor(private readonly config?: PagerDutyClientConfig) {} + + async getServiceByIntegrationKey(integrationKey: string): Promise { + if (!this.config?.token) { + throw new Error('Missing token'); + } + + const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`; + const url = `${this.API_URL}/services?${params}`; + const { services } = await this.getByUrl(url); + + return services; + } + + async getIncidentsByServiceId(serviceId: string): Promise { + if (!this.config?.token) { + throw new Error('Missing token'); + } + + const params = `service_ids[]=${serviceId}`; + const url = `${this.API_URL}/incidents?${params}`; + const { incidents } = await this.getByUrl(url); + + return incidents; } - return await response.json(); -}; - -const getByUrl = async (url: string, token: string) => { - const options = { - method: 'GET', - headers: { - Authorization: `Token token=${token}`, - Accept: 'application/vnd.pagerduty+json;version=2', - 'Content-Type': 'application/json', - }, - }; - return await request(url, options); -}; - -export const getServiceByIntegrationKey = async ( - integrationKey: string, - token: string, -) => { - const response = await getByUrl( - `${API_URL}/services?include[]=integrations&include[]=escalation_policies&query=${integrationKey}`, - token, - ); - if (response.services.length > 1) { - throw new Error('More than one service in response'); + async getOncallByPolicyId(policyId: string): Promise { + if (!this.config?.token) { + throw new Error('Missing token'); + } + + const params = `include[]=users&escalation_policy_ids[]=${policyId}`; + const url = `${this.API_URL}/oncalls?${params}`; + const { oncalls } = await this.getByUrl(url); + + return oncalls; } - return response.services[0]; -}; - -export const getIncidentsByServiceId = async ( - serviceId: string, - token: string, -) => { - return await getByUrl( - `${API_URL}/incidents?service_ids[]=${serviceId}`, - token, - ); -}; - -export const getOncallByPolicyId = async (policyId: string, token: string) => { - return await getByUrl( - `${API_URL}/oncalls?include[]=users&escalation_policy_ids[]=${policyId}`, - token, - ); -}; - -export function triggerPagerDutyAlarm( - integrationKey: string, - source: string, - description: string, - userName: string, -) { - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Accept: 'application/json, text/plain, */*', - }, - body: JSON.stringify({ + + triggerPagerDutyAlarm( + integrationKey: string, + source: string, + description: string, + userName: string, + ) { + const body = JSON.stringify({ event_action: 'trigger', routing_key: integrationKey, client: 'Backstage', @@ -113,8 +99,46 @@ export function triggerPagerDutyAlarm( user: userName, }, }, - }), - }; + }); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json, text/plain, */*', + }, + body, + }; + + return this.request(`${this.EVENTS_API_URL}/enqueue`, options); + } + + private async getByUrl(url: string): Promise { + const options = { + method: 'GET', + headers: { + Authorization: `Token token=${this.config!.token}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }, + }; + const response = await this.request(url, options); - return request(`${EVENTS_API_URL}/enqueue`, options); + return response.json(); + } + + private async request(url: string, options: Options): Promise { + try { + const response = await fetch(url, options); + if (!response.ok) { + const payload = await response.json(); + const errors = payload.errors.map((error: string) => error).join(' '); + const message = `Request failed with ${response.status}, ${errors}`; + throw new Error(message); + } + return response; + } catch (error) { + throw new Error(error); + } + } } diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx index 0117281a0ed20..dd3301904db20 100644 --- a/plugins/pagerduty/src/components/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -114,7 +114,9 @@ type IncidentsProps = { export const Incidents = ({ incidents }: IncidentsProps) => ( Incidents}> {incidents.length ? ( - incidents.map(incident => ) + incidents.map((incident, index) => ( + + )) ) : ( )} diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index e17e35a99660a..54713b8ecd818 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -14,18 +14,15 @@ * limitations under the License. */ import React from 'react'; -import { InfoCard, useApi, configApiRef } from '@backstage/core'; +import { InfoCard, useApi } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { Grid, LinearProgress } from '@material-ui/core'; import { Incidents } from './Incidents'; import { EscalationPolicy } from './Escalation'; import { TriggerButton } from './TriggerButton'; import { useAsync } from 'react-use'; -import { - getServiceByIntegrationKey, - getIncidentsByServiceId, - getOncallByPolicyId, -} from '../api/pagerDutyClient'; +import { Alert } from '@material-ui/lab'; +import { pagerDutyApiRef } from '../api/pagerDutyClient'; export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -37,65 +34,52 @@ type Props = { }; export const PagerDutyServiceCard = ({ entity }: Props) => { - const configApi = useApi(configApiRef); - - // TODO: handle missing token - const token = configApi.getOptionalString('pagerduty.api.token') ?? undefined; - console.log({ token }); + const api = useApi(pagerDutyApiRef); const { value, loading, error } = useAsync(async () => { const integrationKey = entity.metadata.annotations![ PAGERDUTY_INTEGRATION_KEY ]; - const service = await getServiceByIntegrationKey(integrationKey, token); - const incidents = await getIncidentsByServiceId( - (service as any).id /*// TODO: fix type */, - token, - ); - const oncalls = await getOncallByPolicyId( - (service as any).escalation_policy.id, // TODO: fix type - token, - ); + const services = await api.getServiceByIntegrationKey(integrationKey); + // TODO check services length + const service = services[0]; + + const incidents = await api.getIncidentsByServiceId(service.id); + const oncalls = await api.getOncallByPolicyId(service.escalation_policy.id); return { - pagerDutyServices: [ - { - activeIncidents: incidents, - escalationPolicy: oncalls, - id: service.id, - name: service.name, - homepageUrl: service.html_url, - }, - ], + incidents, + oncalls, + id: service.id, + name: service.name, + homepageUrl: service.html_url, }; }); if (error) { - // TODO: use the errorApi - console.log(error); - throw new Error(`Error in getting data: ${error.message}`); + return ( +
+ + Error encountered while fetching information. {error.message} + +
+ ); } if (loading) { return ; } - const { - activeIncidents, - escalationPolicy, - homepageUrl, - } = value!.pagerDutyServices[0]!; - const link = { title: 'View in PagerDuty', - link: homepageUrl, + link: value!.homepageUrl, }; return ( - - + + diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx index 7dbb0be5f0d6c..13c2ba6aad819 100644 --- a/plugins/pagerduty/src/components/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog.tsx @@ -26,7 +26,7 @@ import { } from '@material-ui/core'; import { Progress, useApi, alertApiRef, identityApiRef } from '@backstage/core'; import { useAsyncFn } from 'react-use'; -import { triggerPagerDutyAlarm } from '../api/pagerDutyClient'; +import { pagerDutyApiRef } from '../api/pagerDutyClient'; const useStyles = makeStyles({ warningText: { @@ -48,6 +48,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); const userId = identityApi.getUserId(); + const api = useApi(pagerDutyApiRef); const descriptionChanged = (event: any) => { setDescription(event.target.value); @@ -55,7 +56,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { const [{ value, loading, error }, triggerAlarm] = useAsyncFn( async (desc: string) => { - return await triggerPagerDutyAlarm( + return await api.triggerPagerDutyAlarm( integrationKey, window.location.toString(), desc, diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 8f8523b78bae1..4073551dc8a06 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -13,7 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; +import { + createApiFactory, + createPlugin, + createRouteRef, + configApiRef, +} from '@backstage/core'; +import { pagerDutyApiRef, PagerDutyClientApi } from './api/pagerDutyClient'; export const rootRouteRef = createRouteRef({ path: '/pagerduty', @@ -22,4 +28,14 @@ export const rootRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'pagerduty', + apis: [ + createApiFactory({ + api: pagerDutyApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new PagerDutyClientApi({ + token: configApi.getOptionalString('pagerduty.api.token'), + }), + }), + ], }); From 2aed296e459dde1703a8376c6118b3c499c53686 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 11 Nov 2020 14:24:40 +0100 Subject: [PATCH 14/52] change UI bsed on UX --- .../pagerduty/src/components/Escalation.tsx | 15 +- .../pagerduty/src/components/Incidents.tsx | 18 +-- .../src/components/PagerDutyServiceCard.tsx | 32 +++-- .../src/components/PagerdutyCard.tsx | 132 ++++++++++++++++++ plugins/pagerduty/src/components/Pd.tsx | 27 ++++ .../src/components/TriggerButton.tsx | 24 +++- .../src/components/TriggerDialog.tsx | 2 + plugins/pagerduty/src/components/types.tsx | 7 +- 8 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 plugins/pagerduty/src/components/PagerdutyCard.tsx create mode 100644 plugins/pagerduty/src/components/Pd.tsx diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx index 19c6a15096d9c..f53f79af41a6d 100644 --- a/plugins/pagerduty/src/components/Escalation.tsx +++ b/plugins/pagerduty/src/components/Escalation.tsx @@ -29,8 +29,8 @@ import { import UserIcon from '@material-ui/icons/Person'; import EmailIcon from '@material-ui/icons/Email'; import { StatusWarning } from '@backstage/core'; -import Pagerduty from '../assets/pd.svg'; -import { Oncall, PagerDutyEscalationPolicy } from './types'; +import { Oncall } from './types'; +import PagerdutyIcon from './Pd'; const useStyles = makeStyles({ denseListIcon: { @@ -45,7 +45,7 @@ const useStyles = makeStyles({ }, }); -const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => { +const EscalationUser = ({ user }: Oncall) => { const classes = useStyles(); return ( @@ -56,7 +56,7 @@ const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => { - + @@ -64,12 +64,9 @@ const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => { href={user.html_url} target="_blank" rel="noopener noreferrer" + color="primary" > - View in PagerDuty +
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx index dd3301904db20..512836422c455 100644 --- a/plugins/pagerduty/src/components/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incidents.tsx @@ -25,11 +25,12 @@ import { makeStyles, IconButton, ListSubheader, + Link, } from '@material-ui/core'; import { StatusError, StatusWarning, StatusOK } from '@backstage/core'; -import Pagerduty from '../assets/pd.svg'; import moment from 'moment'; import { Incident } from '../components/types'; +import PagerdutyIcon from './Pd'; const useStyles = makeStyles({ denseListIcon: { @@ -64,6 +65,7 @@ type IncidentListItemProps = { const IncidentListItem = ({ incident }: IncidentListItemProps) => { const classes = useStyles(); + const user = incident.assignments[0].assignee; return ( @@ -82,8 +84,9 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => { secondary={ Created {moment(incident.created_at).fromNow()}, assigned to{' '} - {(incident?.assignments[0]?.assignee?.summary && - incident.assignments[0].assignee.summary) || + {(incident?.assignments[0]?.assignee?.summary && ( + {user.summary} + )) || 'nobody'} } @@ -91,15 +94,12 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => { - View in PagerDuty + diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx index 54713b8ecd818..4fd4691458903 100644 --- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx @@ -14,15 +14,16 @@ * limitations under the License. */ import React from 'react'; -import { InfoCard, useApi } from '@backstage/core'; +import { useApi } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { Grid, LinearProgress } from '@material-ui/core'; +import { LinearProgress } from '@material-ui/core'; import { Incidents } from './Incidents'; import { EscalationPolicy } from './Escalation'; import { TriggerButton } from './TriggerButton'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { pagerDutyApiRef } from '../api/pagerDutyClient'; +import { PagerdutyCard } from './PagerdutyCard'; export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -71,19 +72,26 @@ export const PagerDutyServiceCard = ({ entity }: Props) => { return ; } - const link = { + const pagerdutyLink = { title: 'View in PagerDuty', - link: value!.homepageUrl, + href: value!.homepageUrl, + }; + + const triggerAlarm = { + title: 'Trigger Alarm', + action: , }; return ( - - - - - - - {/* todo show something when we dont have token */} - + + + + + } + > ); }; diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx new file mode 100644 index 0000000000000..2af4d2e4bc0e6 --- /dev/null +++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { + Card, + CardHeader, + CardContent, + Divider, + Link, + makeStyles, +} from '@material-ui/core'; +import LinkIcon from '@material-ui/icons/Link'; +import ReportProblemIcon from '@material-ui/icons/ReportProblem'; +import classnames from 'classnames'; +import Pagerduty from '../assets/pd.svg'; +import PagerdutyIcon from './Pd'; + +// TODO: create a general component to use it in pagerduty and aboutCard + +const useStyles = makeStyles(theme => ({ + links: { + margin: theme.spacing(2, 0), + display: 'grid', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + gridGap: theme.spacing(3), + }, + link: { + display: 'grid', + justifyItems: 'center', + gridGap: 4, + textAlign: 'center', + }, + label: { + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + }, + trigger: { + color: theme.palette.secondary.main, + }, + svgButtonImage: { + height: '1.5em', + color: theme.palette.primary.main, + }, +})); + +type SubHeaderProps = { + ViewPagerduty: { title: string; href: string }; + TriggerAlarm: { title: string; action: React.ReactNode }; +}; + +type PagerdutyCardProps = { + title: string; + subheader: SubHeaderProps; + content: React.ReactNode; +}; + +type VerticalIconProps = { + label: string; + href?: string; + action?: React.ReactNode; + icon?: React.ReactNode; +}; + +const VerticalIcon = ({ + label, + href, + icon = , + action, +}: VerticalIconProps) => { + const classes = useStyles(); + if (action) { + return ( + <> + + {icon} + {action} + + + ); + } + return ( + <> + + {icon} + {label} + + + ); +}; +export const PagerdutyCard = ({ + title, + subheader, + content, +}: PagerdutyCardProps) => { + const classes = useStyles(); + const getSubheader = ({ ViewPagerduty, TriggerAlarm }: SubHeaderProps) => { + return ( + + ); + }; + + return ( + + + + {content} + + ); +}; diff --git a/plugins/pagerduty/src/components/Pd.tsx b/plugins/pagerduty/src/components/Pd.tsx new file mode 100644 index 0000000000000..3698d0a1c497b --- /dev/null +++ b/plugins/pagerduty/src/components/Pd.tsx @@ -0,0 +1,27 @@ +// import React from 'react'; +// import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + +// const SvgPd = (props: SvgIconProps) => +// React.createElement( +// SvgIcon, +// props, +// +// +// , +// ); + +// export default SvgPd; + +import React from 'react'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const SvgPd = (props: SvgIconProps) => + React.createElement( + SvgIcon, + props, + + + , + ); + +export default SvgPd; diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx index eb051e68fa0d6..48bed68e2e9de 100644 --- a/plugins/pagerduty/src/components/TriggerButton.tsx +++ b/plugins/pagerduty/src/components/TriggerButton.tsx @@ -15,17 +15,34 @@ */ import React, { useState } from 'react'; -import { Button } from '@material-ui/core'; +import { Button, makeStyles } from '@material-ui/core'; import { TriggerDialog } from './TriggerDialog'; import { Entity } from '@backstage/catalog-model'; import { PAGERDUTY_INTEGRATION_KEY } from './PagerDutyServiceCard'; +const useStyles = makeStyles({ + triggerAlarm: { + paddingTop: 0, + paddingBottom: 0, + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + lineHeight: 1.1, + '&:hover, &:focus, &.focus': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }, +}); + type Props = { entity: Entity; }; export const TriggerButton = ({ entity }: Props) => { const [showDialog, setShowDialog] = useState(false); + const classes = useStyles(); const handleDialog = () => { setShowDialog(!showDialog); @@ -34,12 +51,11 @@ export const TriggerButton = ({ entity }: Props) => { return ( <> {showDialog && ( { the issue and reach out to you if necessary.

{ - - {loading && } ); }; From bc161fba4ebeedac9b39774ee4aacf8fab488de4 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 20 Nov 2020 11:33:53 +0100 Subject: [PATCH 26/52] add readme --- plugins/pagerduty/README.md | 92 +++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 0335cb5a9bbba..9c5c0b24857e4 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -1,13 +1,89 @@ -# pagerduty +# PagerDuty -Welcome to the pagerduty plugin! +## Overview -_This plugin was created through the Backstage CLI_ +This plugin displays PagerDuty information about an entity such as if there are any active incidents and how the escalation policy looks like. -## Getting started +There is also an easy way to trigger an alarm directly to the person who is currently on-call. -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty). +This plugin requires that entities are annotated with an [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service). See more further down in this document. -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +This plugin provides: + +- A list of incidents +- A way to trigger an alarm to the person on-call +- Information details about the person on-call + +## Setup instructions + +Install the plugin: + +```bash +yarn add @backstage/plugin-pagerduty +``` + +Add it to the app in `plugins.ts`: + +```ts +export { plugin as Pagerduty } from '@backstage/plugin-pagerduty'; +``` + +Add it to the `EntityPage.ts`: + +```ts +import { + isPluginApplicableToEntity as isPagerDutyAvailable, + PagerDutyCard, +} from '@backstage/plugin-pagerduty'; +// add to code +{ + isPagerDutyAvailable(entity) && ( + + + + ); +} +``` + +## Client configuration + +The PagerDutyClient can be configured with the appropriate urls: + +In `apis.ts`: + +```ts +createApiFactory({ + api: pagerDutyApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new PagerDutyClientApi({ + api_url: `https://api.pagerduty.com`, + events_url: 'https://events.pagerduty.com/v2', + }), +}), +``` + +## Providing the API Token + +In order for the client to make requests to the [PagerDuty API](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/) it needs an [API Token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key). + +Then start the backend passing the token as an environment variable: + +```bash +$ PAGERDUTY_TOKEN='Token token=' yarn start +``` + +This will proxy the request by adding `Authorization` header with the provided token. + +## Integration Key + +The information displayed for each entity is based on the [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service). + +### Adding the integration key to the entity annotation + +If you want to use this plugin for an entity, you need to label it with the below annotation: + +```yml +annotations: + pagerduty.com/integration-key: [INTEGRATION_KEY] +``` From d14f125ed1769b2c8fee7c51f572eb304b4af43d Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 20 Nov 2020 12:03:27 +0100 Subject: [PATCH 27/52] update pagerduty after triggering new alarm --- .../Escalation/EscalationPolicy.tsx | 52 ++++++++--- .../components/Escalation/EscalationUser.tsx | 7 +- .../components/Incident/IncidentListItem.tsx | 43 +++++---- .../src/components/Incident/Incidents.tsx | 67 ++++++++++---- .../src/components/PagerdutyCard.tsx | 88 ++++++++++++++----- .../TriggerButton/TriggerButton.tsx | 71 --------------- .../src/components/TriggerButton/index.ts | 17 ---- .../TriggerDialog/TriggerDialog.tsx | 65 +++++++------- plugins/pagerduty/src/components/types.tsx | 10 ++- plugins/pagerduty/src/index.ts | 7 +- 10 files changed, 229 insertions(+), 198 deletions(-) delete mode 100644 plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx delete mode 100644 plugins/pagerduty/src/components/TriggerButton/index.ts diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx index cbce0ce4a3fb3..ccf70abb0b777 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx @@ -15,21 +15,47 @@ */ import React from 'react'; -import { List, ListSubheader } from '@material-ui/core'; -import { User } from '../types'; +import { List, ListSubheader, LinearProgress } from '@material-ui/core'; import { EscalationUsersEmptyState } from './EscalationUsersEmptyState'; import { EscalationUser } from './EscalationUser'; +import { useAsync } from 'react-use'; +import { pagerDutyApiRef } from '../../api'; +import { useApi } from '@backstage/core'; +import { Alert } from '@material-ui/lab'; -type EscalationPolicyProps = { - users: User[]; +type Props = { + policyId: string; }; -export const EscalationPolicy = ({ users }: EscalationPolicyProps) => ( - ON CALL}> - {users.length ? ( - users.map((user, index) => ) - ) : ( - - )} - -); +export const EscalationPolicy = ({ policyId }: Props) => { + const api = useApi(pagerDutyApiRef); + + const { value: users, loading, error } = useAsync(async () => { + const oncalls = await api.getOnCallByPolicyId(policyId); + const users = oncalls.map(oncall => oncall.user); + + return users; + }); + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + return users!.length ? ( + ON CALL}> + {users!.map((user, index) => ( + + ))} + + ) : ( + + ); +}; diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx index adc8d4e6d7196..642c9ecd96e93 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx @@ -36,8 +36,13 @@ const useStyles = makeStyles({ }, }); -export const EscalationUser = ({ user }: { user: User }) => { +type Props = { + user: User; +}; + +export const EscalationUser = ({ user }: Props) => { const classes = useStyles(); + return ( diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 6e22ccbbac106..7a97dad281ac7 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -31,10 +31,6 @@ import moment from 'moment'; import { Incident } from '../types'; import PagerdutyIcon from '../PagerDutyIcon'; -type IncidentListItemProps = { - incident: Incident; -}; - const useStyles = makeStyles({ denseListIcon: { marginRight: 0, @@ -51,9 +47,15 @@ const useStyles = makeStyles({ }, }); -export const IncidentListItem = ({ incident }: IncidentListItemProps) => { +type Props = { + incident: Incident; +}; + +export const IncidentListItem = ({ incident }: Props) => { const classes = useStyles(); - const user = incident.assignments[0].assignee; + const user = incident.assignments[0]?.assignee; + const createdAt = moment(incident.created_at).fromNow(); + return ( @@ -68,25 +70,28 @@ export const IncidentListItem = ({ incident }: IncidentListItemProps) => { - {incident.title} - - } + primary={incident.title} + primaryTypographyProps={{ + variant: 'body1', + className: classes.listItemPrimary, + }} secondary={ - - Created {moment(incident.created_at).fromNow()}, assigned to{' '} - {(incident?.assignments[0]?.assignee?.summary && ( - {user.summary} - )) || - 'nobody'} - + + Created {createdAt} and assigned to{' '} + + {user?.summary ?? 'nobody'} + + } /> void; }; -export const Incidents = ({ incidents }: IncidentsProps) => ( - {incidents.length && 'INCIDENTS'}} - > - {incidents.length ? ( - incidents.map((incident, index) => ( +export const Incidents = ({ + serviceId, + shouldRefreshIncidents, + setShouldRefreshIncidents, +}: Props) => { + const api = useApi(pagerDutyApiRef); + + const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( + async () => { + setShouldRefreshIncidents(false); + return await api.getIncidentsByServiceId(serviceId); + }, + ); + + useEffect(() => { + getIncidents(); + }, [shouldRefreshIncidents, getIncidents]); + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + return incidents?.length ? ( + INCIDENTS}> + {incidents!.map((incident, index) => ( - )) - ) : ( - - )} - -); + ))} + + ) : ( + + ); +}; diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx index 6b6d46685082d..f24e91a9e05a9 100644 --- a/plugins/pagerduty/src/components/PagerdutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { useApi, EmptyState } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { + Button, Card, CardContent, CardHeader, @@ -26,13 +27,13 @@ import { } from '@material-ui/core'; import { Incidents } from './Incident'; import { EscalationPolicy } from './Escalation'; -import { TriggerButton } from './TriggerButton'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { pagerDutyApiRef, UnauthorizedError } from '../api'; import { IconLinkVertical } from '@backstage/plugin-catalog'; import PagerDutyIcon from './PagerDutyIcon'; -import ReportProblemIcon from '@material-ui/icons/ReportProblem'; +import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; +import { TriggerDialog } from './TriggerDialog'; const useStyles = makeStyles(theme => ({ links: { @@ -42,6 +43,19 @@ const useStyles = makeStyles(theme => ({ gridAutoColumns: 'min-content', gridGap: theme.spacing(3), }, + triggerAlarm: { + paddingTop: 0, + paddingBottom: 0, + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + lineHeight: 1.5, + '&:hover, &:focus, &.focus': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }, })); export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -56,35 +70,45 @@ type Props = { export const PagerDutyCard = ({ entity }: Props) => { const classes = useStyles(); const api = useApi(pagerDutyApiRef); + const [showDialog, setShowDialog] = useState(false); + const [shouldRefreshIncidents, setShouldRefreshIncidents] = useState( + false, + ); + const integrationKey = entity.metadata.annotations![ + PAGERDUTY_INTEGRATION_KEY + ]; - const { value, loading, error } = useAsync(async () => { - const integrationKey = entity.metadata.annotations![ - PAGERDUTY_INTEGRATION_KEY - ]; - + const { value: service, loading, error } = useAsync(async () => { const services = await api.getServiceByIntegrationKey(integrationKey); - const incidents = await api.getIncidentsByServiceId(services[0].id); - const oncalls = await api.getOnCallByPolicyId( - services[0].escalation_policy.id, - ); - const users = oncalls.map(oncall => oncall.user); return { - incidents, - users, id: services[0].id, name: services[0].name, - homepageUrl: services[0].html_url, + url: services[0].html_url, + policyId: services[0].escalation_policy.id, }; }); + const handleDialog = () => { + setShowDialog(!showDialog); + }; + if (error) { if (error instanceof UnauthorizedError) { return ( + Read More + + } /> ); } @@ -102,12 +126,21 @@ export const PagerDutyCard = ({ entity }: Props) => { const pagerdutyLink = { title: 'View in PagerDuty', - href: value!.homepageUrl, + href: service!.url, }; const triggerAlarm = { title: 'Trigger Alarm', - action: , + action: ( + + ), }; return ( @@ -123,7 +156,7 @@ export const PagerDutyCard = ({ entity }: Props) => { /> } + icon={} action={triggerAlarm.action} /> @@ -131,8 +164,19 @@ export const PagerDutyCard = ({ entity }: Props) => { /> - - + + + ); diff --git a/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx deleted file mode 100644 index a4f012ac47bd7..0000000000000 --- a/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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, { useState } from 'react'; -import { Button, makeStyles } from '@material-ui/core'; -import { TriggerDialog } from '../TriggerDialog'; -import { Entity } from '@backstage/catalog-model'; -import { PAGERDUTY_INTEGRATION_KEY } from '../PagerDutyCard'; - -const useStyles = makeStyles({ - triggerAlarm: { - paddingTop: 0, - paddingBottom: 0, - fontSize: '0.7rem', - textTransform: 'uppercase', - fontWeight: 600, - letterSpacing: 1.2, - lineHeight: 1.5, - '&:hover, &:focus, &.focus': { - backgroundColor: 'transparent', - textDecoration: 'none', - }, - }, -}); - -type Props = { - entity: Entity; -}; - -export const TriggerButton = ({ entity }: Props) => { - const [showDialog, setShowDialog] = useState(false); - const classes = useStyles(); - - const handleDialog = () => { - setShowDialog(!showDialog); - }; - - return ( - <> - - {showDialog && ( - - )} - - ); -}; diff --git a/plugins/pagerduty/src/components/TriggerButton/index.ts b/plugins/pagerduty/src/components/TriggerButton/index.ts deleted file mode 100644 index 6a58a4f43f5e1..0000000000000 --- a/plugins/pagerduty/src/components/TriggerButton/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { TriggerButton } from './TriggerButton'; diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 4156074460066..e6bb6d107d6e3 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -19,67 +19,61 @@ import { Dialog, DialogTitle, TextField, - makeStyles, DialogActions, Button, DialogContent, Typography, CircularProgress, } from '@material-ui/core'; -import { - Progress, - useApi, - alertApiRef, - identityApiRef, - WarningPanel, -} from '@backstage/core'; +import { useApi, alertApiRef, identityApiRef } from '@backstage/core'; import { useAsyncFn } from 'react-use'; import { pagerDutyApiRef } from '../../api'; -import { Alert, AlertTitle } from '@material-ui/lab'; - -const useStyles = makeStyles({ - warningText: { - border: '1px solid rgba(245, 155, 35, 0.5)', - backgroundColor: 'rgba(245, 155, 35, 0.2)', - padding: '0.5em 1em', - }, -}); +import { Alert } from '@material-ui/lab'; type Props = { name: string; integrationKey: string; - onClose: () => void; + showDialog: boolean; + handleDialog: () => void; + setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void; }; -export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { - const classes = useStyles(); - const [description, setDescription] = useState(''); +export const TriggerDialog = ({ + name, + integrationKey, + showDialog, + handleDialog, + setShouldRefreshIncidents, +}: Props) => { const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); const userId = identityApi.getUserId(); const api = useApi(pagerDutyApiRef); - - const descriptionChanged = (event: any) => { - setDescription(event.target.value); - }; + const [description, setDescription] = useState(''); const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn( - async (desc: string) => { - return await api.triggerAlarm( + async (desc: string) => + await api.triggerAlarm( integrationKey, window.location.toString(), desc, userId, - ); - }, + ), ); + const descriptionChanged = ( + event: React.ChangeEvent, + ) => { + setDescription(event.target.value); + }; + useEffect(() => { if (value) { alertApi.post({ message: `Alarm successfully triggered by ${userId}`, }); - onClose(); + setShouldRefreshIncidents(true); + handleDialog(); } if (error) { @@ -88,16 +82,19 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { severity: 'error', }); } - }, [value, error, alertApi, onClose, userId]); + }, [value, error, alertApi, handleDialog, userId, setShouldRefreshIncidents]); + + if (!showDialog) { + return null; + } return ( - + This action will trigger an incident for "{name}". - {/* "Note" */} If the issue you are seeing does not need urgent attention, please get in touch with the responsible team using their preferred @@ -141,7 +138,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => { > Trigger Incident - diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx index 9c9e29cb019d9..e968656e65d6f 100644 --- a/plugins/pagerduty/src/components/types.tsx +++ b/plugins/pagerduty/src/components/types.tsx @@ -18,10 +18,10 @@ export type Incident = { id: string; title: string; status: string; - homepageUrl: string; + html_url: string; assignments: [ { - assignee: User; + assignee: Assignee; }, ]; serviceId: string; @@ -43,6 +43,12 @@ export type OnCall = { user: User; }; +export type Assignee = { + id: string; + summary: string; + html_url: string; +}; + export type User = { id: string; summary: string; diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 58dc39932dd9c..1e8e549ac92f2 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -17,4 +17,9 @@ export { plugin } from './plugin'; export { isPluginApplicableToEntity, PagerDutyCard, -} from './components/PagerDutyCard'; +} from './components/PagerdutyCard'; +export { + PagerDutyClientApi, + pagerDutyApiRef, + UnauthorizedError, +} from './api/client'; From 5519358df8ec1d4d2e39b24d991ec9f110ef92d9 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 20 Nov 2020 12:05:51 +0100 Subject: [PATCH 28/52] update tests --- .../components/Escalation/Escalation.test.tsx | 80 +++++++-- .../components/Incident/Incidents.test.tsx | 151 +++++++++++------ .../src/components/PagerDutyCard.test.tsx | 153 ++++++++++++++++++ .../TriggerDialog/TriggerDialog.test.tsx | 27 ++-- 4 files changed, 335 insertions(+), 76 deletions(-) create mode 100644 plugins/pagerduty/src/components/PagerDutyCard.test.tsx diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx index 4fa65eb999852..15ff3278a0cad 100644 --- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx +++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx @@ -14,34 +14,82 @@ * limitations under the License. */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; import { wrapInTestApp } from '@backstage/test-utils'; import { User } from '../types'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { pagerDutyApiRef } from '../../api'; -const escalations: User[] = [ - { - name: 'person1', - id: 'p1', - summary: 'person1', - email: 'person1@example.com', - html_url: 'http://a.com/id1', - }, -]; +const mockPagerDutyApi = { + getOnCallByPolicyId: () => [], +}; +const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); describe('Escalation', () => { - it('render emptyState', () => { - const { getByText } = render( - wrapInTestApp(), + it('Handles an empty response', async () => { + mockPagerDutyApi.getOnCallByPolicyId = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith('456'); }); - it('render escalation list', () => { - const { getByText } = render( - wrapInTestApp(), + 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, + }, + ]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('person1')).toBeInTheDocument(); expect(getByText('person1@example.com')).toBeInTheDocument(); + expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith('abc'); + }); + + it('Handles errors', async () => { + mockPagerDutyApi.getOnCallByPolicyId = jest + .fn() + .mockRejectedValueOnce(new Error('Error message')); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText('Error encountered while fetching information. Error message'), + ).toBeInTheDocument(); }); }); diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index fbe3783d3032e..6baa41002104f 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -14,65 +14,100 @@ * limitations under the License. */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; import { wrapInTestApp } from '@backstage/test-utils'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { pagerDutyApiRef } from '../../api'; import { Incident } from '../types'; -const incidents: Incident[] = [ - { - id: 'id1', - status: 'triggered', - title: 'title1', - created_at: '2020-11-06T00:00:00Z', - assignments: [ - { - assignee: { - name: 'person1', - id: 'p1', - summary: 'person1', - email: 'person1@example.com', - html_url: 'http://a.com/id1', - }, - }, - ], - homepageUrl: 'http://a.com/id1', - serviceId: 'sId1', - }, - { - id: 'id2', - status: 'acknowledged', - title: 'title2', - created_at: '2020-11-07T00:00:00Z', - assignments: [ - { - assignee: { - name: 'person2', - id: 'p2', - summary: 'person2', - email: 'person2@example.com', - html_url: 'http://a.com/id2', - }, - }, - ], - homepageUrl: 'http://a.com/id2', - serviceId: 'sId2', - }, -]; +const mockPagerDutyApi = { + getIncidentsByServiceId: () => [], +}; +const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); describe('Incidents', () => { - it('renders an empty state is there are no incidents', () => { - const { getByText } = render(wrapInTestApp()); + it('Renders an empty state when there are no incidents', async () => { + mockPagerDutyApi.getIncidentsByServiceId = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + {}} + /> + , + ), + ); + await waitFor(() => !queryByTestId('progress')); expect( getByText('Nice! No incidents are assigned to you!'), ).toBeInTheDocument(); }); - it('renders all incidents', () => { - const { getByText, getByTitle, getAllByTitle, getByLabelText } = render( - wrapInTestApp(), - ); + it('Renders all incidents', async () => { + mockPagerDutyApi.getIncidentsByServiceId = jest.fn().mockImplementationOnce( + async () => + [ + { + id: 'id1', + status: 'triggered', + title: 'title1', + created_at: '2020-11-06T00:00:00Z', + assignments: [ + { + assignee: { + id: 'p1', + summary: 'person1', + html_url: 'http://a.com/id1', + }, + }, + ], + html_url: 'http://a.com/id1', + serviceId: 'sId1', + }, + { + id: 'id2', + status: 'acknowledged', + title: 'title2', + created_at: '2020-11-07T00:00:00Z', + assignments: [ + { + assignee: { + id: 'p2', + summary: 'person2', + html_url: 'http://a.com/id2', + }, + }, + ], + html_url: 'http://a.com/id2', + serviceId: 'sId2', + }, + ] as Incident[], + ); + const { + getByText, + getByTitle, + getAllByTitle, + getByLabelText, + queryByTestId, + } = render( + wrapInTestApp( + + {}} + /> + , + ), + ); + await waitFor(() => !queryByTestId('progress')); expect(getByText('title1')).toBeInTheDocument(); expect(getByText('title2')).toBeInTheDocument(); expect(getByText('person1')).toBeInTheDocument(); @@ -85,4 +120,26 @@ describe('Incidents', () => { // assert links, mailto and hrefs, date calculation expect(getAllByTitle('View in PagerDuty').length).toEqual(2); }); + + it('Handle errors', async () => { + mockPagerDutyApi.getIncidentsByServiceId = jest + .fn() + .mockRejectedValueOnce(new Error('Error occurred')); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + {}} + /> + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('Error encountered while fetching information. Error occurred'), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx new file mode 100644 index 0000000000000..bca6f9f940fd6 --- /dev/null +++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { render, waitFor, fireEvent } from '@testing-library/react'; +import { PagerDutyCard } from './PagerdutyCard'; +import { Entity } from '@backstage/catalog-model'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { + alertApiRef, + ApiProvider, + ApiRegistry, + createApiRef, +} from '@backstage/core'; +import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api'; +import { Service } from './types'; +import { act } from 'react-dom/test-utils'; + +const mockPagerDutyApi: Partial = { + getServiceByIntegrationKey: async () => [], + getOnCallByPolicyId: async () => [], + getIncidentsByServiceId: async () => [], +}; + +const apis = ApiRegistry.from([ + [pagerDutyApiRef, mockPagerDutyApi], + [ + alertApiRef, + createApiRef({ + id: 'core.alert', + description: 'Used to report alerts and forward them to the app', + }), + ], +]); +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'pagerduty-test', + annotations: { + 'pagerduty.com/integration-key': 'abc123', + }, + }, +}; + +const service: Service = { + id: 'abc', + name: 'pagerduty-name', + html_url: 'www.example.com', + escalation_policy: { + id: 'def', + user: { + name: 'person1', + id: 'p1', + summary: 'person1', + email: 'person1@example.com', + html_url: 'http://a.com/id1', + }, + }, + integrationKey: 'abcd', +}; + +describe('PageDutyCard', () => { + it('Render pagerduty', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockImplementationOnce(async () => [service]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('View in PagerDuty')).toBeInTheDocument(); + expect(getByText('Trigger Alarm')).toBeInTheDocument(); + expect( + getByText('Nice! No incidents are assigned to you!'), + ).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + + it('Handles custom error for missing token', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument(); + }); + + it('handles general error', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockRejectedValueOnce(new Error('An error occurred')); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText( + 'Error encountered while fetching information. An error occurred', + ), + ).toBeInTheDocument(); + }); + it('opens the dialog when trigger button is clicked', async () => { + mockPagerDutyApi.getServiceByIntegrationKey = jest + .fn() + .mockImplementationOnce(async () => [service]); + + const { getByText, queryByTestId, getByTestId, getByRole } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('View in PagerDuty')).toBeInTheDocument(); + expect(getByText('Trigger Alarm')).toBeInTheDocument(); + const triggerButton = getByTestId('trigger-button'); + await act(async () => { + fireEvent.click(triggerButton); + }); + expect(getByRole('dialog')).toBeInTheDocument(); + }); +}); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 7e26985136e9b..440621dcf6f3c 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -25,9 +25,9 @@ import { identityApiRef, } from '@backstage/core'; import { pagerDutyApiRef } from '../../api'; -import { TriggerButton } from '../TriggerButton'; import { Entity } from '@backstage/catalog-model'; import { act } from 'react-dom/test-utils'; +import { TriggerDialog } from './TriggerDialog'; describe('TriggerDialog', () => { const mockIdentityApi: Partial = { @@ -36,7 +36,7 @@ describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); const mockPagerDutyApi = { - triggerPagerDutyAlarm: mockTriggerAlarmFn, + triggerAlarm: mockTriggerAlarmFn, }; const apis = ApiRegistry.from([ @@ -63,24 +63,25 @@ describe('TriggerDialog', () => { }, }; - const { getByText, getByRole, queryByRole, getByTestId } = render( + const { getByText, getByRole, getByTestId } = render( wrapInTestApp( - + {}} + name={entity.metadata.name} + integrationKey="abc123" + setShouldRefreshIncidents={() => {}} + /> , ), ); - expect(queryByRole('dialog')).toBeNull(); - const alarmButton = getByText('Trigger Alarm'); - fireEvent.click(alarmButton); + expect(getByRole('dialog')).toBeInTheDocument(); expect( - getByText( - 'This action will send PagerDuty alarms and notifications to on-call people responsible for', - { - exact: false, - }, - ), + getByText('This action will trigger an incident for ', { + exact: false, + }), ).toBeInTheDocument(); const input = getByTestId('trigger-input'); const description = 'Test Trigger Alarm'; From 6011b7d3e5476bf6c0e582cb132d282c434d872c Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 20 Nov 2020 12:07:43 +0100 Subject: [PATCH 29/52] add changeset --- .changeset/great-spiders-repair.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/great-spiders-repair.md diff --git a/.changeset/great-spiders-repair.md b/.changeset/great-spiders-repair.md new file mode 100644 index 0000000000000..0952612d846fe --- /dev/null +++ b/.changeset/great-spiders-repair.md @@ -0,0 +1,7 @@ +--- +'example-app': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-pagerduty': patch +--- + +Added pagerduty plugin to example app From 60b290cd0bcbe4e1fccf04897041836c3f051f0d Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 11:10:04 +0100 Subject: [PATCH 30/52] use discoveryApi and update readme --- plugins/pagerduty/README.md | 9 ++++----- plugins/pagerduty/src/api/client.ts | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 9c5c0b24857e4..da37282aa7aef 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -47,18 +47,17 @@ import { ## Client configuration -The PagerDutyClient can be configured with the appropriate urls: +The client takes a config object which contains an instance of the `discoveryApi` which is used to look up the `baseUrl` of the `proxy`, and a `eventUrl` which is defaulted to: "https://events.pagerduty.com/v2". In `apis.ts`: ```ts createApiFactory({ api: pagerDutyApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new PagerDutyClientApi({ - api_url: `https://api.pagerduty.com`, - events_url: 'https://events.pagerduty.com/v2', + discoveryApi: discoveryApi }), }), ``` diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index c9d40a16941cd..4ed994eec24be 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -39,7 +39,9 @@ export class PagerDutyClientApi implements PagerDutyClient { async getServiceByIntegrationKey(integrationKey: string): Promise { const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`; - const url = `${this.config.api_url}/services?${params}`; + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/services?${params}`; const { services } = await this.getByUrl(url); return services; @@ -47,7 +49,9 @@ export class PagerDutyClientApi implements PagerDutyClient { async getIncidentsByServiceId(serviceId: string): Promise { const params = `service_ids[]=${serviceId}`; - const url = `${this.config.api_url}/incidents?${params}`; + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/incidents?${params}`; const { incidents } = await this.getByUrl(url); return incidents; @@ -55,7 +59,9 @@ export class PagerDutyClientApi implements PagerDutyClient { async getOnCallByPolicyId(policyId: string): Promise { const params = `include[]=users&escalation_policy_ids[]=${policyId}`; - const url = `${this.config.api_url}/oncalls?${params}`; + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/pagerduty/oncalls?${params}`; const { oncalls } = await this.getByUrl(url); return oncalls; @@ -92,7 +98,10 @@ export class PagerDutyClientApi implements PagerDutyClient { body, }; - return this.request(`${this.config.events_url}/enqueue`, options); + return this.request( + `${this.config.eventsUrl ?? 'https://events.pagerduty.com/v2'}/enqueue`, + options, + ); } private async getByUrl(url: string): Promise { From aa88559488d9070f93471a716b235ab07c55eb3e Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 11:24:19 +0100 Subject: [PATCH 31/52] use discoveryApi --- plugins/pagerduty/src/components/types.tsx | 6 ++++-- plugins/pagerduty/src/plugin.ts | 11 ++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx index e968656e65d6f..5e5ee0a61be47 100644 --- a/plugins/pagerduty/src/components/types.tsx +++ b/plugins/pagerduty/src/components/types.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { DiscoveryApi } from '@backstage/core'; + export type Incident = { id: string; title: string; @@ -76,6 +78,6 @@ export type RequestOptions = { }; export type ClientApiConfig = { - api_url: string; - events_url: string; + eventsUrl?: string; + discoveryApi: DiscoveryApi; }; diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 2e07b5c430a18..02b11ed773106 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -17,7 +17,7 @@ import { createApiFactory, createPlugin, createRouteRef, - configApiRef, + discoveryApiRef, } from '@backstage/core'; import { pagerDutyApiRef, PagerDutyClientApi } from './api'; @@ -31,13 +31,10 @@ export const plugin = createPlugin({ apis: [ createApiFactory({ api: pagerDutyApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new PagerDutyClientApi({ - api_url: `${configApi.getString( - 'backend.baseUrl', - )}/api/proxy/pagerduty`, - events_url: 'https://events.pagerduty.com/v2', + discoveryApi: discoveryApi, }), }), ], From ac6e65aa1ae4bcb280e5a8d76f8cf00f76430bd4 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 14:10:17 +0100 Subject: [PATCH 32/52] create MissingTokenError and flatten error --- .../src/components/MissingTokenError.tsx | 35 +++++++++++++++++++ .../src/components/PagerdutyCard.tsx | 28 ++++----------- 2 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 plugins/pagerduty/src/components/MissingTokenError.tsx diff --git a/plugins/pagerduty/src/components/MissingTokenError.tsx b/plugins/pagerduty/src/components/MissingTokenError.tsx new file mode 100644 index 0000000000000..c22552b7c6b7c --- /dev/null +++ b/plugins/pagerduty/src/components/MissingTokenError.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { EmptyState } from '@backstage/core'; +import { Button } from '@material-ui/core'; + +export const MissingTokenError = () => ( + + Read More + + } + /> +); diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx index f24e91a9e05a9..22a471c031db9 100644 --- a/plugins/pagerduty/src/components/PagerdutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { useState } from 'react'; -import { useApi, EmptyState } from '@backstage/core'; +import { useApi } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { Button, @@ -31,9 +31,10 @@ import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { pagerDutyApiRef, UnauthorizedError } from '../api'; import { IconLinkVertical } from '@backstage/plugin-catalog'; -import PagerDutyIcon from './PagerDutyIcon'; +import { PagerDutyIcon } from './PagerDutyIcon'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { TriggerDialog } from './TriggerDialog'; +import { MissingTokenError } from './MissingTokenError'; const useStyles = makeStyles(theme => ({ links: { @@ -93,26 +94,11 @@ export const PagerDutyCard = ({ entity }: Props) => { setShowDialog(!showDialog); }; - if (error) { - if (error instanceof UnauthorizedError) { - return ( - - Read More - - } - /> - ); - } + if (error instanceof UnauthorizedError) { + return ; + } + if (error) { return ( Error encountered while fetching information. {error.message} From a57ed028f85b4beda64830a2f17b14254be9b7e2 Mon Sep 17 00:00:00 2001 From: samiramkr Date: Fri, 27 Nov 2020 14:49:51 +0100 Subject: [PATCH 33/52] Update plugins/pagerduty/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- plugins/pagerduty/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index da37282aa7aef..b3e0b9f4e642b 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -2,7 +2,7 @@ ## Overview -This plugin displays PagerDuty information about an entity such as if there are any active incidents and how the escalation policy looks like. +This plugin displays PagerDuty information about an entity such as if there are any active incidents and what the escalation policy is. There is also an easy way to trigger an alarm directly to the person who is currently on-call. From 43d0967db89e2cf32808d0ae4a02ed442a27c755 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 16:10:55 +0100 Subject: [PATCH 34/52] refactor code --- .../AboutCard/IconLinkVertical/IconLinkVertical.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx index 7e1dc61fa72e4..4a8bf2a0abc6c 100644 --- a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx +++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx @@ -75,11 +75,11 @@ export function IconLinkVertical({ if (action) { return ( From b2013cc40b33ec4d43138c4d02e553c5b142b981 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 16:35:53 +0100 Subject: [PATCH 35/52] refactor type --- plugins/pagerduty/src/api/client.ts | 14 +++++++------- plugins/pagerduty/src/api/types.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 4ed994eec24be..7b453b7298bc4 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -25,7 +25,7 @@ import { OnCallsResponse, OnCall, } from '../components/types'; -import { PagerDutyClient } from './types'; +import { PagerDutyClient, TriggerAlarmRequest } from './types'; export class UnauthorizedError extends Error {} @@ -67,12 +67,12 @@ export class PagerDutyClientApi implements PagerDutyClient { return oncalls; } - triggerAlarm( - integrationKey: string, - source: string, - description: string, - userName: string, - ) { + triggerAlarm({ + integrationKey, + source, + description, + userName, + }: TriggerAlarmRequest): Promise { const body = JSON.stringify({ event_action: 'trigger', routing_key: integrationKey, diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index c07834a213701..17c2067818c38 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -16,6 +16,13 @@ import { Incident, OnCall, Service } from '../components/types'; +export type TriggerAlarmRequest = { + integrationKey: string; + source: string; + description: string; + userName: string; +}; + export interface PagerDutyClient { /** * Fetches a list of services, filtered by the provided integration key. @@ -38,10 +45,5 @@ export interface PagerDutyClient { /** * Triggers an incident to whoever is on-call. */ - triggerAlarm( - integrationKey: string, - source: string, - description: string, - userName: string, - ): Promise; + triggerAlarm(request: TriggerAlarmRequest): Promise; } From 6a02802d728031f19550b3cf26203e29b3e7008b Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 27 Nov 2020 16:59:16 +0100 Subject: [PATCH 36/52] use Progress --- .../src/components/Escalation/EscalationPolicy.tsx | 14 ++++++++------ .../src/components/Incident/Incidents.tsx | 14 ++++++++------ plugins/pagerduty/src/components/PagerdutyCard.tsx | 4 ++-- plugins/pagerduty/src/setupTests.ts | 2 -- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx index ccf70abb0b777..5e67d5217221b 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; -import { List, ListSubheader, LinearProgress } from '@material-ui/core'; +import { List, ListSubheader } from '@material-ui/core'; import { EscalationUsersEmptyState } from './EscalationUsersEmptyState'; import { EscalationUser } from './EscalationUser'; import { useAsync } from 'react-use'; import { pagerDutyApiRef } from '../../api'; -import { useApi } from '@backstage/core'; +import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; type Props = { @@ -46,16 +46,18 @@ export const EscalationPolicy = ({ policyId }: Props) => { } if (loading) { - return ; + return ; } - return users!.length ? ( + if (!users?.length) { + return ; + } + + return ( ON CALL}> {users!.map((user, index) => ( ))} - ) : ( - ); }; diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx index 2d2e5b01fd6bd..1d4ec3a1a638f 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx @@ -15,12 +15,12 @@ */ import React, { useEffect } from 'react'; -import { List, ListSubheader, LinearProgress } from '@material-ui/core'; +import { List, ListSubheader } from '@material-ui/core'; import { IncidentListItem } from './IncidentListItem'; import { IncidentsEmptyState } from './IncidentEmptyState'; import { useAsyncFn } from 'react-use'; import { pagerDutyApiRef } from '../../api'; -import { useApi } from '@backstage/core'; +import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; type Props = { @@ -56,16 +56,18 @@ export const Incidents = ({ } if (loading) { - return ; + return ; } - return incidents?.length ? ( + if (!incidents?.length) { + return ; + } + + return ( INCIDENTS}> {incidents!.map((incident, index) => ( ))} - ) : ( - ); }; diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx index 22a471c031db9..333e042f82f6e 100644 --- a/plugins/pagerduty/src/components/PagerdutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx @@ -22,7 +22,6 @@ import { CardContent, CardHeader, Divider, - LinearProgress, makeStyles, } from '@material-ui/core'; import { Incidents } from './Incident'; @@ -35,6 +34,7 @@ import { PagerDutyIcon } from './PagerDutyIcon'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { TriggerDialog } from './TriggerDialog'; import { MissingTokenError } from './MissingTokenError'; +import { Progress } from '@backstage/core'; const useStyles = makeStyles(theme => ({ links: { @@ -107,7 +107,7 @@ export const PagerDutyCard = ({ entity }: Props) => { } if (loading) { - return ; + return ; } const pagerdutyLink = { diff --git a/plugins/pagerduty/src/setupTests.ts b/plugins/pagerduty/src/setupTests.ts index d7857386de33a..0bfa67b49a755 100644 --- a/plugins/pagerduty/src/setupTests.ts +++ b/plugins/pagerduty/src/setupTests.ts @@ -14,5 +14,3 @@ * limitations under the License. */ import '@testing-library/jest-dom'; - -global.fetch = require('node-fetch'); From d2f716d64083ad4769153bb6dd833d5e9b093604 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Mon, 30 Nov 2020 10:57:16 +0100 Subject: [PATCH 37/52] fix changeset --- .changeset/great-spiders-repair.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/great-spiders-repair.md b/.changeset/great-spiders-repair.md index 0952612d846fe..2cf1ddd8e8551 100644 --- a/.changeset/great-spiders-repair.md +++ b/.changeset/great-spiders-repair.md @@ -1,5 +1,4 @@ --- -'example-app': patch '@backstage/plugin-catalog': patch '@backstage/plugin-pagerduty': patch --- From 49938595bcc8b54a0074bc8d34a8c8b5b4f94382 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Mon, 30 Nov 2020 18:31:35 +0100 Subject: [PATCH 38/52] improve updating incident list, refactor code, update tests, move types, rename types file --- plugins/pagerduty/README.md | 3 ++- plugins/pagerduty/src/api/client.ts | 13 +++++----- plugins/pagerduty/src/api/types.ts | 24 ++++++++++++++++++ .../components/Escalation/EscalationUser.tsx | 4 +-- .../components/Incident/IncidentListItem.tsx | 4 +-- .../components/Incident/Incidents.test.tsx | 6 ++--- .../src/components/Incident/Incidents.tsx | 12 +++------ .../src/components/PagerDutyIcon.tsx | 4 +-- .../src/components/PagerdutyCard.tsx | 16 ++++++------ .../TriggerDialog/TriggerDialog.test.tsx | 14 ++++++----- .../TriggerDialog/TriggerDialog.tsx | 24 +++++++++--------- .../src/components/{types.tsx => types.ts} | 25 ------------------- 12 files changed, 71 insertions(+), 78 deletions(-) rename plugins/pagerduty/src/components/{types.tsx => types.ts} (74%) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index b3e0b9f4e642b..7a886627e307f 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -57,7 +57,8 @@ createApiFactory({ deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => new PagerDutyClientApi({ - discoveryApi: discoveryApi + discoveryApi: discoveryApi, + eventUrl: "https://events.pagerduty.com/v2" //to override the default value }), }), ``` diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 7b453b7298bc4..ab65514466cae 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -15,17 +15,16 @@ */ import { createApiRef } from '@backstage/core'; +import { Service, Incident, OnCall } from '../components/types'; import { - Service, - Incident, - RequestOptions, - ClientApiConfig, + PagerDutyClient, + TriggerAlarmRequest, ServicesResponse, IncidentsResponse, OnCallsResponse, - OnCall, -} from '../components/types'; -import { PagerDutyClient, TriggerAlarmRequest } from './types'; + ClientApiConfig, + RequestOptions, +} from './types'; export class UnauthorizedError extends Error {} diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 17c2067818c38..2231cc6991e9d 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -15,6 +15,7 @@ */ import { Incident, OnCall, Service } from '../components/types'; +import { DiscoveryApi } from '@backstage/core'; export type TriggerAlarmRequest = { integrationKey: string; @@ -47,3 +48,26 @@ export interface PagerDutyClient { */ triggerAlarm(request: TriggerAlarmRequest): Promise; } + +export type ServicesResponse = { + services: Service[]; +}; + +export type IncidentsResponse = { + incidents: Incident[]; +}; + +export type OnCallsResponse = { + oncalls: OnCall[]; +}; + +export type ClientApiConfig = { + eventsUrl?: string; + discoveryApi: DiscoveryApi; +}; + +export type RequestOptions = { + method: string; + headers: HeadersInit; + body?: BodyInit; +}; diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx index 642c9ecd96e93..3847896e89fe7 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx @@ -27,7 +27,7 @@ import { } from '@material-ui/core'; import Avatar from '@material-ui/core/Avatar'; import EmailIcon from '@material-ui/icons/Email'; -import PagerdutyIcon from '../PagerDutyIcon'; +import { PagerDutyIcon } from '../PagerDutyIcon'; import { User } from '../types'; const useStyles = makeStyles({ @@ -69,7 +69,7 @@ export const EscalationUser = ({ user }: Props) => { rel="noopener noreferrer" color="primary" > - + diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 7a97dad281ac7..488631b834418 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -29,7 +29,7 @@ import { import { StatusError, StatusWarning } from '@backstage/core'; import moment from 'moment'; import { Incident } from '../types'; -import PagerdutyIcon from '../PagerDutyIcon'; +import { PagerDutyIcon } from '../PagerDutyIcon'; const useStyles = makeStyles({ denseListIcon: { @@ -96,7 +96,7 @@ export const IncidentListItem = ({ incident }: Props) => { rel="noopener noreferrer" color="primary" > - + diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index 6baa41002104f..3eef24cc6ae8c 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -37,7 +37,7 @@ describe('Incidents', () => { {}} /> , @@ -101,7 +101,7 @@ describe('Incidents', () => { {}} /> , @@ -131,7 +131,7 @@ describe('Incidents', () => { {}} /> , diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx index 1d4ec3a1a638f..4b229fb4bc699 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx @@ -25,27 +25,21 @@ import { Alert } from '@material-ui/lab'; type Props = { serviceId: string; - shouldRefreshIncidents: boolean; - setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void; + refreshIncidents: Boolean; }; -export const Incidents = ({ - serviceId, - shouldRefreshIncidents, - setShouldRefreshIncidents, -}: Props) => { +export const Incidents = ({ serviceId, refreshIncidents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( async () => { - setShouldRefreshIncidents(false); return await api.getIncidentsByServiceId(serviceId); }, ); useEffect(() => { getIncidents(); - }, [shouldRefreshIncidents, getIncidents]); + }, [refreshIncidents, getIncidents]); if (error) { return ( diff --git a/plugins/pagerduty/src/components/PagerDutyIcon.tsx b/plugins/pagerduty/src/components/PagerDutyIcon.tsx index 36f6e1989d4ff..67cfc0036ae73 100644 --- a/plugins/pagerduty/src/components/PagerDutyIcon.tsx +++ b/plugins/pagerduty/src/components/PagerDutyIcon.tsx @@ -17,7 +17,7 @@ import React from 'react'; import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; -const SvgPd = (props: SvgIconProps) => +export const PagerDutyIcon = (props: SvgIconProps) => React.createElement( SvgIcon, props, @@ -25,5 +25,3 @@ const SvgPd = (props: SvgIconProps) => , ); - -export default SvgPd; diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx index 333e042f82f6e..ac309e8a95de2 100644 --- a/plugins/pagerduty/src/components/PagerdutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { useState } from 'react'; -import { useApi } from '@backstage/core'; +import { useApi, Progress } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { Button, @@ -34,7 +34,6 @@ import { PagerDutyIcon } from './PagerDutyIcon'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { TriggerDialog } from './TriggerDialog'; import { MissingTokenError } from './MissingTokenError'; -import { Progress } from '@backstage/core'; const useStyles = makeStyles(theme => ({ links: { @@ -72,9 +71,7 @@ export const PagerDutyCard = ({ entity }: Props) => { const classes = useStyles(); const api = useApi(pagerDutyApiRef); const [showDialog, setShowDialog] = useState(false); - const [shouldRefreshIncidents, setShouldRefreshIncidents] = useState( - false, - ); + const [refreshIncidents, setRefreshIncidents] = useState(false); const integrationKey = entity.metadata.annotations![ PAGERDUTY_INTEGRATION_KEY ]; @@ -129,6 +126,10 @@ export const PagerDutyCard = ({ entity }: Props) => { ), }; + const onTriggerRefresh = () => { + setRefreshIncidents(true); + }; + return ( { { handleDialog={handleDialog} name={entity.metadata.name} integrationKey={integrationKey} - setShouldRefreshIncidents={setShouldRefreshIncidents} + onTriggerRefresh={onTriggerRefresh} /> diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 440621dcf6f3c..3d2a48b419021 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -71,7 +71,7 @@ describe('TriggerDialog', () => { handleDialog={() => {}} name={entity.metadata.name} integrationKey="abc123" - setShouldRefreshIncidents={() => {}} + onTriggerRefresh={() => {}} /> , ), @@ -93,11 +93,13 @@ describe('TriggerDialog', () => { fireEvent.click(triggerButton); }); expect(mockTriggerAlarmFn).toHaveBeenCalled(); - expect(mockTriggerAlarmFn).toHaveBeenCalledWith( - entity!.metadata!.annotations!['pagerduty.com/integration-key'], - window.location.toString(), + expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ + integrationKey: entity!.metadata!.annotations![ + 'pagerduty.com/integration-key' + ], + source: window.location.toString(), description, - 'guest@example.com', - ); + userName: 'guest@example.com', + }); }); }); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index e6bb6d107d6e3..4717b8fece059 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -35,7 +35,7 @@ type Props = { integrationKey: string; showDialog: boolean; handleDialog: () => void; - setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void; + onTriggerRefresh: () => void; }; export const TriggerDialog = ({ @@ -43,22 +43,22 @@ export const TriggerDialog = ({ integrationKey, showDialog, handleDialog, - setShouldRefreshIncidents, + onTriggerRefresh, }: Props) => { const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); - const userId = identityApi.getUserId(); + const userName = identityApi.getUserId(); const api = useApi(pagerDutyApiRef); const [description, setDescription] = useState(''); const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn( - async (desc: string) => - await api.triggerAlarm( + async (description: string) => + await api.triggerAlarm({ integrationKey, - window.location.toString(), - desc, - userId, - ), + source: window.location.toString(), + description, + userName, + }), ); const descriptionChanged = ( @@ -70,9 +70,9 @@ export const TriggerDialog = ({ useEffect(() => { if (value) { alertApi.post({ - message: `Alarm successfully triggered by ${userId}`, + message: `Alarm successfully triggered by ${userName}`, }); - setShouldRefreshIncidents(true); + onTriggerRefresh(); handleDialog(); } @@ -82,7 +82,7 @@ export const TriggerDialog = ({ severity: 'error', }); } - }, [value, error, alertApi, handleDialog, userId, setShouldRefreshIncidents]); + }, [value, error, alertApi, handleDialog, userName, onTriggerRefresh]); if (!showDialog) { return null; diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.ts similarity index 74% rename from plugins/pagerduty/src/components/types.tsx rename to plugins/pagerduty/src/components/types.ts index 5e5ee0a61be47..a93791fe8e050 100644 --- a/plugins/pagerduty/src/components/types.tsx +++ b/plugins/pagerduty/src/components/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core'; - export type Incident = { id: string; title: string; @@ -58,26 +56,3 @@ export type User = { html_url: string; name: string; }; - -export type ServicesResponse = { - services: Service[]; -}; - -export type IncidentsResponse = { - incidents: Incident[]; -}; - -export type OnCallsResponse = { - oncalls: OnCall[]; -}; - -export type RequestOptions = { - method: string; - headers: HeadersInit; - body?: BodyInit; -}; - -export type ClientApiConfig = { - eventsUrl?: string; - discoveryApi: DiscoveryApi; -}; From 04a53de52fdd9424d35bd398904ff21989853153 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 1 Dec 2020 13:46:46 +0100 Subject: [PATCH 39/52] rename type and class, fix test --- plugins/pagerduty/README.md | 2 +- plugins/pagerduty/src/api/client.ts | 6 +++--- plugins/pagerduty/src/api/index.ts | 8 ++------ plugins/pagerduty/src/api/types.ts | 2 +- .../src/components/Incident/Incidents.test.tsx | 18 +++--------------- .../src/components/PagerDutyCard.test.tsx | 2 +- plugins/pagerduty/src/index.ts | 4 ++-- plugins/pagerduty/src/plugin.ts | 4 ++-- 8 files changed, 15 insertions(+), 31 deletions(-) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 7a886627e307f..b925e9d7a767f 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -56,7 +56,7 @@ createApiFactory({ api: pagerDutyApiRef, deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => - new PagerDutyClientApi({ + new PagerDutyClient({ discoveryApi: discoveryApi, eventUrl: "https://events.pagerduty.com/v2" //to override the default value }), diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index ab65514466cae..214e7d57a74ef 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -17,7 +17,7 @@ import { createApiRef } from '@backstage/core'; import { Service, Incident, OnCall } from '../components/types'; import { - PagerDutyClient, + PagerDutyApi, TriggerAlarmRequest, ServicesResponse, IncidentsResponse, @@ -28,12 +28,12 @@ import { export class UnauthorizedError extends Error {} -export const pagerDutyApiRef = createApiRef({ +export const pagerDutyApiRef = createApiRef({ id: 'plugin.pagerduty.api', description: 'Used to fetch data from PagerDuty API', }); -export class PagerDutyClientApi implements PagerDutyClient { +export class PagerDutyClient implements PagerDutyApi { constructor(private readonly config: ClientApiConfig) {} async getServiceByIntegrationKey(integrationKey: string): Promise { diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts index 5ac7470475b49..90604c40126be 100644 --- a/plugins/pagerduty/src/api/index.ts +++ b/plugins/pagerduty/src/api/index.ts @@ -14,9 +14,5 @@ * limitations under the License. */ -export { - PagerDutyClientApi, - pagerDutyApiRef, - UnauthorizedError, -} from './client'; -export type { PagerDutyClient } from './types'; +export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client'; +export type { PagerDutyApi } from './types'; diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 2231cc6991e9d..4b3ae74cc18e8 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -24,7 +24,7 @@ export type TriggerAlarmRequest = { userName: string; }; -export interface PagerDutyClient { +export interface PagerDutyApi { /** * Fetches a list of services, filtered by the provided integration key. * diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index 3eef24cc6ae8c..3c5e30e0ccfa8 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -35,11 +35,7 @@ describe('Incidents', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - {}} - /> + , ), ); @@ -99,11 +95,7 @@ describe('Incidents', () => { } = render( wrapInTestApp( - {}} - /> + , ), ); @@ -129,11 +121,7 @@ describe('Incidents', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - {}} - /> + , ), ); diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx index bca6f9f940fd6..26d67a55e4065 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { render, waitFor, fireEvent } from '@testing-library/react'; -import { PagerDutyCard } from './PagerdutyCard'; +import { PagerDutyCard } from './PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { wrapInTestApp } from '@backstage/test-utils'; import { diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 1e8e549ac92f2..4ecd4edcc63a2 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -17,9 +17,9 @@ export { plugin } from './plugin'; export { isPluginApplicableToEntity, PagerDutyCard, -} from './components/PagerdutyCard'; +} from './components/PagerDutyCard'; export { - PagerDutyClientApi, + PagerDutyClient, pagerDutyApiRef, UnauthorizedError, } from './api/client'; diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 02b11ed773106..4e8f429b059d0 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -19,7 +19,7 @@ import { createRouteRef, discoveryApiRef, } from '@backstage/core'; -import { pagerDutyApiRef, PagerDutyClientApi } from './api'; +import { pagerDutyApiRef, PagerDutyClient } from './api'; export const rootRouteRef = createRouteRef({ path: '/pagerduty', @@ -33,7 +33,7 @@ export const plugin = createPlugin({ api: pagerDutyApiRef, deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => - new PagerDutyClientApi({ + new PagerDutyClient({ discoveryApi: discoveryApi, }), }), From 3d392a0b6acfe0852d8c7eb83200dc8d276e5a48 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 1 Dec 2020 16:38:40 +0100 Subject: [PATCH 40/52] address comments --- app-config.yaml | 2 ++ packages/app/package.json | 6 +++--- packages/app/src/plugins.ts | 2 +- plugins/pagerduty/README.md | 17 +++++------------ plugins/pagerduty/src/api/client.ts | 14 ++++++++++++-- plugins/pagerduty/src/api/types.ts | 2 +- .../src/components/PagerDutyCard.test.tsx | 3 +-- .../TriggerDialog/TriggerDialog.test.tsx | 3 +-- plugins/pagerduty/src/plugin.ts | 9 ++++----- 9 files changed, 30 insertions(+), 28 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 18eeb24400845..e80b5df0213ac 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -312,3 +312,5 @@ homepage: timezone: 'Europe/Stockholm' - label: TYO timezone: 'Asia/Tokyo' +pagerduty: + eventsBaseUrl: 'https://events.pagerduty.com/v2' diff --git a/packages/app/package.json b/packages/app/package.json index b0499c8431ab9..f78a3474ff07b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,25 +21,25 @@ "@backstage/plugin-kubernetes": "^0.3.0", "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-newrelic": "^0.2.1", + "@backstage/plugin-pagerduty": "0.2.1", "@backstage/plugin-register-component": "^0.2.2", "@backstage/plugin-rollbar": "^0.2.3", "@backstage/plugin-scaffolder": "^0.3.1", - "@backstage/plugin-sentry": "^0.2.3", "@backstage/plugin-search": "^0.2.1", + "@backstage/plugin-sentry": "^0.2.3", "@backstage/plugin-tech-radar": "^0.3.0", "@backstage/plugin-techdocs": "^0.2.3", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/plugin-welcome": "^0.2.1", "@backstage/test-utils": "^0.1.3", - "@backstage/plugin-pagerduty": "^0.2.1", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", + "@roadiehq/backstage-plugin-buildkite": "^0.1.3", "@roadiehq/backstage-plugin-github-insights": "^0.2.15", "@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3", "@roadiehq/backstage-plugin-travis-ci": "^0.2.8", - "@roadiehq/backstage-plugin-buildkite": "^0.1.3", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 3fdf236e8d686..d70694969ee3b 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -38,6 +38,6 @@ export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights'; export { plugin as UserSettings } from '@backstage/plugin-user-settings'; -export { plugin as Pagerduty } from '@backstage/plugin-pagerduty'; +export { plugin as PagerDuty } from '@backstage/plugin-pagerduty'; export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index b925e9d7a767f..1c22053b889b5 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -47,20 +47,13 @@ import { ## Client configuration -The client takes a config object which contains an instance of the `discoveryApi` which is used to look up the `baseUrl` of the `proxy`, and a `eventUrl` which is defaulted to: "https://events.pagerduty.com/v2". +If you want to override the default URL for events, you can add it to `app-config.yaml`. -In `apis.ts`: +In `app-config.yaml`: -```ts -createApiFactory({ - api: pagerDutyApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new PagerDutyClient({ - discoveryApi: discoveryApi, - eventUrl: "https://events.pagerduty.com/v2" //to override the default value - }), -}), +```yaml +pagerduty: + eventsBaseUrl: 'https://events.pagerduty.com/v2' ``` ## Providing the API Token diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 214e7d57a74ef..99e21ef6d366a 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core'; +import { createApiRef, DiscoveryApi, ConfigApi } from '@backstage/core'; import { Service, Incident, OnCall } from '../components/types'; import { PagerDutyApi, @@ -34,6 +34,15 @@ export const pagerDutyApiRef = createApiRef({ }); export class PagerDutyClient implements PagerDutyApi { + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { + const eventsBaseUrl: string = + configApi.getOptionalString('pagerDuty.eventsBaseUrl') ?? + 'https://events.pagerduty.com/v2'; + return new PagerDutyClient({ + eventsBaseUrl, + discoveryApi, + }); + } constructor(private readonly config: ClientApiConfig) {} async getServiceByIntegrationKey(integrationKey: string): Promise { @@ -98,7 +107,8 @@ export class PagerDutyClient implements PagerDutyApi { }; return this.request( - `${this.config.eventsUrl ?? 'https://events.pagerduty.com/v2'}/enqueue`, + `${this.config.eventsBaseUrl ?? + 'https://events.pagerduty.com/v2'}/enqueue`, options, ); } diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts index 4b3ae74cc18e8..733f171489bef 100644 --- a/plugins/pagerduty/src/api/types.ts +++ b/plugins/pagerduty/src/api/types.ts @@ -62,7 +62,7 @@ export type OnCallsResponse = { }; export type ClientApiConfig = { - eventsUrl?: string; + eventsBaseUrl?: string; discoveryApi: DiscoveryApi; }; diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx index 26d67a55e4065..34ddb63f71386 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { render, waitFor, fireEvent } from '@testing-library/react'; +import { render, waitFor, fireEvent, act } from '@testing-library/react'; import { PagerDutyCard } from './PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { wrapInTestApp } from '@backstage/test-utils'; @@ -26,7 +26,6 @@ import { } from '@backstage/core'; import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api'; import { Service } from './types'; -import { act } from 'react-dom/test-utils'; const mockPagerDutyApi: Partial = { getServiceByIntegrationKey: async () => [], diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 3d2a48b419021..7ef57d67b474c 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, act } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { ApiRegistry, @@ -26,7 +26,6 @@ import { } from '@backstage/core'; import { pagerDutyApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; -import { act } from 'react-dom/test-utils'; import { TriggerDialog } from './TriggerDialog'; describe('TriggerDialog', () => { diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 4e8f429b059d0..34796f491ec0c 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -18,6 +18,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + configApiRef, } from '@backstage/core'; import { pagerDutyApiRef, PagerDutyClient } from './api'; @@ -31,11 +32,9 @@ export const plugin = createPlugin({ apis: [ createApiFactory({ api: pagerDutyApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new PagerDutyClient({ - discoveryApi: discoveryApi, - }), + deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, + factory: ({ configApi, discoveryApi }) => + PagerDutyClient.fromConfig(configApi, discoveryApi), }), ], }); From fa25d9f448934593c32ee43c80fe41f9252ed1c0 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 1 Dec 2020 16:58:35 +0100 Subject: [PATCH 41/52] rename file --- .../src/components/PagerDutyCard.tsx | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 plugins/pagerduty/src/components/PagerDutyCard.tsx diff --git a/plugins/pagerduty/src/components/PagerDutyCard.tsx b/plugins/pagerduty/src/components/PagerDutyCard.tsx new file mode 100644 index 0000000000000..ac309e8a95de2 --- /dev/null +++ b/plugins/pagerduty/src/components/PagerDutyCard.tsx @@ -0,0 +1,169 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { useState } from 'react'; +import { useApi, Progress } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { + Button, + Card, + CardContent, + CardHeader, + Divider, + makeStyles, +} from '@material-ui/core'; +import { Incidents } from './Incident'; +import { EscalationPolicy } from './Escalation'; +import { useAsync } from 'react-use'; +import { Alert } from '@material-ui/lab'; +import { pagerDutyApiRef, UnauthorizedError } from '../api'; +import { IconLinkVertical } from '@backstage/plugin-catalog'; +import { PagerDutyIcon } from './PagerDutyIcon'; +import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; +import { TriggerDialog } from './TriggerDialog'; +import { MissingTokenError } from './MissingTokenError'; + +const useStyles = makeStyles(theme => ({ + links: { + margin: theme.spacing(2, 0), + display: 'grid', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + gridGap: theme.spacing(3), + }, + triggerAlarm: { + paddingTop: 0, + paddingBottom: 0, + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + lineHeight: 1.5, + '&:hover, &:focus, &.focus': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }, +})); + +export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); + +type Props = { + entity: Entity; +}; + +export const PagerDutyCard = ({ entity }: Props) => { + const classes = useStyles(); + const api = useApi(pagerDutyApiRef); + const [showDialog, setShowDialog] = useState(false); + const [refreshIncidents, setRefreshIncidents] = useState(false); + const integrationKey = entity.metadata.annotations![ + PAGERDUTY_INTEGRATION_KEY + ]; + + const { value: service, loading, error } = useAsync(async () => { + const services = await api.getServiceByIntegrationKey(integrationKey); + + return { + id: services[0].id, + name: services[0].name, + url: services[0].html_url, + policyId: services[0].escalation_policy.id, + }; + }); + + const handleDialog = () => { + setShowDialog(!showDialog); + }; + + if (error instanceof UnauthorizedError) { + return ; + } + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + const pagerdutyLink = { + title: 'View in PagerDuty', + href: service!.url, + }; + + const triggerAlarm = { + title: 'Trigger Alarm', + action: ( + + ), + }; + + const onTriggerRefresh = () => { + setRefreshIncidents(true); + }; + + return ( + + + } + /> + } + action={triggerAlarm.action} + /> + + } + /> + + + + + + + + ); +}; From e7807c4c79522491ef3705aa26845aad3dc20444 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 1 Dec 2020 18:23:39 +0100 Subject: [PATCH 42/52] remove moment --- plugins/pagerduty/package.json | 12 ++++++------ .../src/components/Incident/IncidentListItem.tsx | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 2fce2ebef321a..64cc5a089362a 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -20,21 +20,21 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.0", - "@backstage/theme": "^0.2.1", "@backstage/catalog-model": "^0.2.0", - "@backstage/test-utils": "^0.1.2", + "@backstage/core": "^0.3.0", "@backstage/plugin-catalog": "^0.2.1", + "@backstage/test-utils": "^0.1.2", + "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "date-fns": "^2.16.1", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3", - "moment": "^2.27.0" + "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.2.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index 488631b834418..7a34501325d36 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -27,7 +27,7 @@ import { Typography, } from '@material-ui/core'; import { StatusError, StatusWarning } from '@backstage/core'; -import moment from 'moment'; +import { formatDistanceToNowStrict } from 'date-fns'; import { Incident } from '../types'; import { PagerDutyIcon } from '../PagerDutyIcon'; @@ -54,7 +54,7 @@ type Props = { export const IncidentListItem = ({ incident }: Props) => { const classes = useStyles(); const user = incident.assignments[0]?.assignee; - const createdAt = moment(incident.created_at).fromNow(); + const createdAt = formatDistanceToNowStrict(new Date(incident.created_at)); return ( @@ -77,7 +77,7 @@ export const IncidentListItem = ({ incident }: Props) => { }} secondary={ - Created {createdAt} and assigned to{' '} + Created {createdAt} ago and assigned to{' '} Date: Thu, 3 Dec 2020 14:25:58 +0100 Subject: [PATCH 43/52] revert changes --- .../IconLinkVertical/IconLinkVertical.tsx | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx index 4a8bf2a0abc6c..dd267dde35353 100644 --- a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx +++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx @@ -25,10 +25,9 @@ export type IconLinkVerticalProps = { disabled?: boolean; title?: string; label: string; - action?: React.ReactNode; }; -const useIconStyles = makeStyles(theme => ({ +const useIconStyles = makeStyles({ link: { display: 'grid', justifyItems: 'center', @@ -44,16 +43,12 @@ const useIconStyles = makeStyles(theme => ({ fontWeight: 600, letterSpacing: 1.2, }, - linkStyle: { - color: theme.palette.secondary.main, - }, -})); +}); export function IconLinkVertical({ icon = , href = '#', disabled = false, - action, ...props }: IconLinkVerticalProps) { const classes = useIconStyles(); @@ -72,23 +67,6 @@ export function IconLinkVertical({ ); } - if (action) { - return ( - - {icon} - {action} - - ); - } - // Absolute links should not be using RouterLink if (href?.startsWith('//') || href?.includes('://')) { return ( From 3dee3093dee7ab0eb696997a9d35ff60c5b562ea Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 09:28:48 +0100 Subject: [PATCH 44/52] fix updating incidents and create AboutCard and use it --- .../src/components/About/AboutCard.tsx | 33 ++++++ .../src/components/About/SubHeader.tsx | 50 ++++++++ .../src/components/About/VerticalIcon.tsx | 85 ++++++++++++++ .../pagerduty/src/components/About/index.ts | 19 +++ .../src/components/Incident/Incidents.tsx | 6 +- .../src/components/PagerDutyCard.test.tsx | 12 +- .../src/components/PagerDutyCard.tsx | 110 +++++++----------- .../TriggerDialog/TriggerDialog.test.tsx | 2 +- .../TriggerDialog/TriggerDialog.tsx | 24 ++-- plugins/pagerduty/src/components/types.ts | 7 ++ 10 files changed, 254 insertions(+), 94 deletions(-) create mode 100644 plugins/pagerduty/src/components/About/AboutCard.tsx create mode 100644 plugins/pagerduty/src/components/About/SubHeader.tsx create mode 100644 plugins/pagerduty/src/components/About/VerticalIcon.tsx create mode 100644 plugins/pagerduty/src/components/About/index.ts diff --git a/plugins/pagerduty/src/components/About/AboutCard.tsx b/plugins/pagerduty/src/components/About/AboutCard.tsx new file mode 100644 index 0000000000000..7d5419908a02d --- /dev/null +++ b/plugins/pagerduty/src/components/About/AboutCard.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Card, CardContent, CardHeader, Divider } from '@material-ui/core'; +import { SubHeader } from './SubHeader'; +import { SubHeaderLink } from '../types'; + +type Props = { + title: string; + links: SubHeaderLink[]; + content: React.ReactNode; +}; + +export const AboutCard = ({ title, links, content }: Props) => ( + + } /> + + {content} + +); diff --git a/plugins/pagerduty/src/components/About/SubHeader.tsx b/plugins/pagerduty/src/components/About/SubHeader.tsx new file mode 100644 index 0000000000000..5e4ea96fe0b44 --- /dev/null +++ b/plugins/pagerduty/src/components/About/SubHeader.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { VerticalIcon } from './VerticalIcon'; +import { SubHeaderLink } from '../types'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + links: { + margin: theme.spacing(2, 0), + display: 'grid', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + gridGap: theme.spacing(3), + }, +})); + +type Props = { + links: SubHeaderLink[]; +}; + +export const SubHeader = ({ links }: Props) => { + const classes = useStyles(); + return ( + + ); +}; diff --git a/plugins/pagerduty/src/components/About/VerticalIcon.tsx b/plugins/pagerduty/src/components/About/VerticalIcon.tsx new file mode 100644 index 0000000000000..5463372ac0bae --- /dev/null +++ b/plugins/pagerduty/src/components/About/VerticalIcon.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 classnames from 'classnames'; +import { makeStyles, Link } from '@material-ui/core'; +import LinkIcon from '@material-ui/icons/Link'; +import { Link as RouterLink } from 'react-router-dom'; + +export type VerticalIconProps = { + icon?: React.ReactNode; + href?: string; + title?: string; + label: string; + action?: React.ReactNode; +}; + +const useIconStyles = makeStyles(theme => ({ + link: { + display: 'grid', + justifyItems: 'center', + gridGap: 4, + textAlign: 'center', + }, + label: { + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + }, + linkStyle: { + color: theme.palette.secondary.main, + }, +})); + +export function VerticalIcon({ + icon = , + href = '#', + action, + ...props +}: VerticalIconProps) { + const classes = useIconStyles(); + + if (action) { + return ( + + {icon} + {action} + + ); + } + + // Absolute links should not be using RouterLink + if (href?.startsWith('//') || href?.includes('://')) { + return ( + + {icon} + {props.label} + + ); + } + + return ( + + {icon} + {props.label} + + ); +} diff --git a/plugins/pagerduty/src/components/About/index.ts b/plugins/pagerduty/src/components/About/index.ts new file mode 100644 index 0000000000000..4e495daa541c2 --- /dev/null +++ b/plugins/pagerduty/src/components/About/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { AboutCard } from './AboutCard'; +// export { SubHeader } from './SubHeader'; +// export { VerticalIcon } from './VerticalIcon'; diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx index 4b229fb4bc699..732a582cdb1f1 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx @@ -25,16 +25,14 @@ import { Alert } from '@material-ui/lab'; type Props = { serviceId: string; - refreshIncidents: Boolean; + refreshIncidents: boolean; }; export const Incidents = ({ serviceId, refreshIncidents }: Props) => { const api = useApi(pagerDutyApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( - async () => { - return await api.getIncidentsByServiceId(serviceId); - }, + async () => await api.getIncidentsByServiceId(serviceId), ); useEffect(() => { diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx index 34ddb63f71386..a6615aee82aa1 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx @@ -85,11 +85,9 @@ describe('PageDutyCard', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(getByText('View in PagerDuty')).toBeInTheDocument(); - expect(getByText('Trigger Alarm')).toBeInTheDocument(); - expect( - getByText('Nice! No incidents are assigned to you!'), - ).toBeInTheDocument(); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); @@ -141,8 +139,8 @@ describe('PageDutyCard', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(getByText('View in PagerDuty')).toBeInTheDocument(); - expect(getByText('Trigger Alarm')).toBeInTheDocument(); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); const triggerButton = getByTestId('trigger-button'); await act(async () => { fireEvent.click(triggerButton); diff --git a/plugins/pagerduty/src/components/PagerDutyCard.tsx b/plugins/pagerduty/src/components/PagerDutyCard.tsx index ac309e8a95de2..8fd20f37e8511 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.tsx @@ -13,36 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { useApi, Progress } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; -import { - Button, - Card, - CardContent, - CardHeader, - Divider, - makeStyles, -} from '@material-ui/core'; +import { Button, makeStyles } from '@material-ui/core'; import { Incidents } from './Incident'; import { EscalationPolicy } from './Escalation'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { pagerDutyApiRef, UnauthorizedError } from '../api'; -import { IconLinkVertical } from '@backstage/plugin-catalog'; -import { PagerDutyIcon } from './PagerDutyIcon'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import { TriggerDialog } from './TriggerDialog'; -import { MissingTokenError } from './MissingTokenError'; +import { MissingTokenError } from './Errors/MissingTokenError'; +import WebIcon from '@material-ui/icons/Web'; +import { AboutCard } from './About/AboutCard'; -const useStyles = makeStyles(theme => ({ - links: { - margin: theme.spacing(2, 0), - display: 'grid', - gridAutoFlow: 'column', - gridAutoColumns: 'min-content', - gridGap: theme.spacing(3), - }, +const useStyles = makeStyles({ triggerAlarm: { paddingTop: 0, paddingBottom: 0, @@ -56,7 +42,7 @@ const useStyles = makeStyles(theme => ({ textDecoration: 'none', }, }, -})); +}); export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; @@ -76,6 +62,14 @@ export const PagerDutyCard = ({ entity }: Props) => { PAGERDUTY_INTEGRATION_KEY ]; + const handleRefresh = useCallback(() => { + setRefreshIncidents(x => !x); + }, []); + + const handleDialog = useCallback(() => { + setShowDialog(x => !x); + }, []); + const { value: service, loading, error } = useAsync(async () => { const services = await api.getServiceByIntegrationKey(integrationKey); @@ -87,10 +81,6 @@ export const PagerDutyCard = ({ entity }: Props) => { }; }); - const handleDialog = () => { - setShowDialog(!showDialog); - }; - if (error instanceof UnauthorizedError) { return ; } @@ -107,13 +97,14 @@ export const PagerDutyCard = ({ entity }: Props) => { return ; } - const pagerdutyLink = { - title: 'View in PagerDuty', + const serviceLink = { + title: 'Service Directory', href: service!.url, + icon: , }; - const triggerAlarm = { - title: 'Trigger Alarm', + const triggerLink = { + title: 'Create Incident', action: ( ), - }; - - const onTriggerRefresh = () => { - setRefreshIncidents(true); + icon: , }; return ( - - - } - /> - } - action={triggerAlarm.action} - /> - - } - /> - - - - - - - + + + + + + } + /> ); }; diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 7ef57d67b474c..8073ab44b5a72 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -70,7 +70,7 @@ describe('TriggerDialog', () => { handleDialog={() => {}} name={entity.metadata.name} integrationKey="abc123" - onTriggerRefresh={() => {}} + onIncidentCreated={() => {}} /> , ), diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 4717b8fece059..4fa88a1b9a35c 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -35,7 +35,7 @@ type Props = { integrationKey: string; showDialog: boolean; handleDialog: () => void; - onTriggerRefresh: () => void; + onIncidentCreated: () => void; }; export const TriggerDialog = ({ @@ -43,7 +43,7 @@ export const TriggerDialog = ({ integrationKey, showDialog, handleDialog, - onTriggerRefresh, + onIncidentCreated: onIncidentCreated, }: Props) => { const alertApi = useApi(alertApiRef); const identityApi = useApi(identityApiRef); @@ -72,24 +72,20 @@ export const TriggerDialog = ({ alertApi.post({ message: `Alarm successfully triggered by ${userName}`, }); - onTriggerRefresh(); + onIncidentCreated(); handleDialog(); } + }, [value, alertApi, handleDialog, userName, onIncidentCreated]); - if (error) { - alertApi.post({ - message: `Failed to trigger alarm. ${error.message}`, - severity: 'error', - }); - } - }, [value, error, alertApi, handleDialog, userName, onTriggerRefresh]); - - if (!showDialog) { - return null; + if (error) { + alertApi.post({ + message: `Failed to trigger alarm. ${error.message}`, + severity: 'error', + }); } return ( - + This action will trigger an incident for "{name}". diff --git a/plugins/pagerduty/src/components/types.ts b/plugins/pagerduty/src/components/types.ts index a93791fe8e050..371f4eea6d0ce 100644 --- a/plugins/pagerduty/src/components/types.ts +++ b/plugins/pagerduty/src/components/types.ts @@ -56,3 +56,10 @@ export type User = { html_url: string; name: string; }; + +export type SubHeaderLink = { + title: string; + href?: string; + icon: React.ReactNode; + action?: React.ReactNode; +}; From 18c2dcb368f4dfcefdb0fcd211dbf5eb1c772c32 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 09:58:45 +0100 Subject: [PATCH 45/52] remove pagerduty logo --- .../components/Escalation/EscalationUser.tsx | 4 +-- .../Incident/IncidentEmptyState.tsx | 4 +-- .../components/Incident/IncidentListItem.tsx | 4 +-- .../components/Incident/Incidents.test.tsx | 4 +-- .../src/components/PagerDutyIcon.tsx | 27 ------------------- 5 files changed, 7 insertions(+), 36 deletions(-) delete mode 100644 plugins/pagerduty/src/components/PagerDutyIcon.tsx diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx index 3847896e89fe7..41995c86f5835 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx @@ -27,7 +27,7 @@ import { } from '@material-ui/core'; import Avatar from '@material-ui/core/Avatar'; import EmailIcon from '@material-ui/icons/Email'; -import { PagerDutyIcon } from '../PagerDutyIcon'; +import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { User } from '../types'; const useStyles = makeStyles({ @@ -69,7 +69,7 @@ export const EscalationUser = ({ user }: Props) => { rel="noopener noreferrer" color="primary" > - + diff --git a/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx index ded161261ed9c..c140620bb3fbe 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx @@ -15,14 +15,14 @@ */ import React from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, Typography } from '@material-ui/core'; import EmptyStateImage from '../../assets/emptyState.svg'; export const IncidentsEmptyState = () => { return ( -

Nice! No incidents are assigned to you!

+ Nice! No incidents found!
{ rel="noopener noreferrer" color="primary" > - + diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index 3c5e30e0ccfa8..9da9f9f11b4b8 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -40,9 +40,7 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect( - getByText('Nice! No incidents are assigned to you!'), - ).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); }); it('Renders all incidents', async () => { diff --git a/plugins/pagerduty/src/components/PagerDutyIcon.tsx b/plugins/pagerduty/src/components/PagerDutyIcon.tsx deleted file mode 100644 index 67cfc0036ae73..0000000000000 --- a/plugins/pagerduty/src/components/PagerDutyIcon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; - -export const PagerDutyIcon = (props: SvgIconProps) => - React.createElement( - SvgIcon, - props, - - - , - ); From 6f23e968303fb6a5ac91bd3021527d3cfcfa79d5 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 10:03:26 +0100 Subject: [PATCH 46/52] refactor structure --- .../{ => Errors}/MissingTokenError.tsx | 0 .../pagerduty/src/components/Errors/index.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) rename plugins/pagerduty/src/components/{ => Errors}/MissingTokenError.tsx (100%) create mode 100644 plugins/pagerduty/src/components/Errors/index.ts diff --git a/plugins/pagerduty/src/components/MissingTokenError.tsx b/plugins/pagerduty/src/components/Errors/MissingTokenError.tsx similarity index 100% rename from plugins/pagerduty/src/components/MissingTokenError.tsx rename to plugins/pagerduty/src/components/Errors/MissingTokenError.tsx diff --git a/plugins/pagerduty/src/components/Errors/index.ts b/plugins/pagerduty/src/components/Errors/index.ts new file mode 100644 index 0000000000000..3c2dfa65f2fcc --- /dev/null +++ b/plugins/pagerduty/src/components/Errors/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { MissingTokenError } from './MissingTokenError'; From 1c6f8417d111e8a69cfba997f74ff6128f4d7a8f Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 10:14:05 +0100 Subject: [PATCH 47/52] fix getIncident query --- plugins/pagerduty/src/api/client.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 99e21ef6d366a..527a56b2af4ab 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -56,7 +56,7 @@ export class PagerDutyClient implements PagerDutyApi { } async getIncidentsByServiceId(serviceId: string): Promise { - const params = `service_ids[]=${serviceId}`; + const params = `statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`; const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/incidents?${params}`; @@ -106,11 +106,9 @@ export class PagerDutyClient implements PagerDutyApi { body, }; - return this.request( - `${this.config.eventsBaseUrl ?? - 'https://events.pagerduty.com/v2'}/enqueue`, - options, - ); + const url = this.config.eventsBaseUrl ?? 'https://events.pagerduty.com/v2'; + + return this.request(`${url}/enqueue`, options); } private async getByUrl(url: string): Promise { From afb65b2f886dbce5112a862cc27b0bdbcf1e47a1 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 10:14:50 +0100 Subject: [PATCH 48/52] remove plugin-catalog dep --- plugins/pagerduty/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 64cc5a089362a..a7b5c3b58f71d 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -22,15 +22,16 @@ "dependencies": { "@backstage/catalog-model": "^0.2.0", "@backstage/core": "^0.3.0", - "@backstage/plugin-catalog": "^0.2.1", "@backstage/test-utils": "^0.1.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "classnames": "^2.2.6", "date-fns": "^2.16.1", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { From 02be8ca4f1a7095ea88a95642a4c4ee256c8b6b6 Mon Sep 17 00:00:00 2001 From: samiramkr Date: Fri, 4 Dec 2020 11:41:30 +0100 Subject: [PATCH 49/52] Delete PagerdutyCard.tsx --- .../src/components/PagerdutyCard.tsx | 169 ------------------ 1 file changed, 169 deletions(-) delete mode 100644 plugins/pagerduty/src/components/PagerdutyCard.tsx diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx deleted file mode 100644 index ac309e8a95de2..0000000000000 --- a/plugins/pagerduty/src/components/PagerdutyCard.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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, { useState } from 'react'; -import { useApi, Progress } from '@backstage/core'; -import { Entity } from '@backstage/catalog-model'; -import { - Button, - Card, - CardContent, - CardHeader, - Divider, - makeStyles, -} from '@material-ui/core'; -import { Incidents } from './Incident'; -import { EscalationPolicy } from './Escalation'; -import { useAsync } from 'react-use'; -import { Alert } from '@material-ui/lab'; -import { pagerDutyApiRef, UnauthorizedError } from '../api'; -import { IconLinkVertical } from '@backstage/plugin-catalog'; -import { PagerDutyIcon } from './PagerDutyIcon'; -import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; -import { TriggerDialog } from './TriggerDialog'; -import { MissingTokenError } from './MissingTokenError'; - -const useStyles = makeStyles(theme => ({ - links: { - margin: theme.spacing(2, 0), - display: 'grid', - gridAutoFlow: 'column', - gridAutoColumns: 'min-content', - gridGap: theme.spacing(3), - }, - triggerAlarm: { - paddingTop: 0, - paddingBottom: 0, - fontSize: '0.7rem', - textTransform: 'uppercase', - fontWeight: 600, - letterSpacing: 1.2, - lineHeight: 1.5, - '&:hover, &:focus, &.focus': { - backgroundColor: 'transparent', - textDecoration: 'none', - }, - }, -})); - -export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; - -export const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); - -type Props = { - entity: Entity; -}; - -export const PagerDutyCard = ({ entity }: Props) => { - const classes = useStyles(); - const api = useApi(pagerDutyApiRef); - const [showDialog, setShowDialog] = useState(false); - const [refreshIncidents, setRefreshIncidents] = useState(false); - const integrationKey = entity.metadata.annotations![ - PAGERDUTY_INTEGRATION_KEY - ]; - - const { value: service, loading, error } = useAsync(async () => { - const services = await api.getServiceByIntegrationKey(integrationKey); - - return { - id: services[0].id, - name: services[0].name, - url: services[0].html_url, - policyId: services[0].escalation_policy.id, - }; - }); - - const handleDialog = () => { - setShowDialog(!showDialog); - }; - - if (error instanceof UnauthorizedError) { - return ; - } - - if (error) { - return ( - - Error encountered while fetching information. {error.message} - - ); - } - - if (loading) { - return ; - } - - const pagerdutyLink = { - title: 'View in PagerDuty', - href: service!.url, - }; - - const triggerAlarm = { - title: 'Trigger Alarm', - action: ( - - ), - }; - - const onTriggerRefresh = () => { - setRefreshIncidents(true); - }; - - return ( - - - } - /> - } - action={triggerAlarm.action} - /> - - } - /> - - - - - - - - ); -}; From fa9d634350425d44287f604729a4786f1ccf7a2f Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 13:42:20 +0100 Subject: [PATCH 50/52] update dependencies version --- plugins/pagerduty/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index a7b5c3b58f71d..4d9ec49d025b4 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -20,23 +20,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.0", - "@backstage/test-utils": "^0.1.2", + "@backstage/catalog-model": "^0.4.0", + "@backstage/core": "^0.3.2", + "@backstage/test-utils": "^0.1.4", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "classnames": "^2.2.6", - "date-fns": "^2.16.1", + "date-fns": "^2.15.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.1", - "@backstage/dev-utils": "^0.1.3", + "@backstage/cli": "^0.4.0", + "@backstage/dev-utils": "^0.1.5", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From b7a5d90c83f3266d64e00ab3dac34933be4d7617 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 14:56:53 +0100 Subject: [PATCH 51/52] fix import --- .../pagerduty/src/components/Incident/IncidentEmptyState.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx index c140620bb3fbe..b697422b0f91d 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { Grid, Typography } from '@material-ui/core'; -import EmptyStateImage from '../../assets/emptyState.svg'; +import EmptyStateImage from '../../assets/emptystate.svg'; export const IncidentsEmptyState = () => { return ( From 7c856e33174b1c675e619d229e60817b4c94c320 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 4 Dec 2020 16:44:01 +0100 Subject: [PATCH 52/52] update dep version --- plugins/pagerduty/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 4d9ec49d025b4..4d58a1da42ffa 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -22,7 +22,6 @@ "dependencies": { "@backstage/catalog-model": "^0.4.0", "@backstage/core": "^0.3.2", - "@backstage/test-utils": "^0.1.4", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,13 +36,15 @@ "devDependencies": { "@backstage/cli": "^0.4.0", "@backstage/dev-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "msw": "^0.20.5", - "node-fetch": "^2.6.1" + "msw": "^0.21.2", + "node-fetch": "^2.6.1", + "cross-fetch": "^3.0.6" }, "files": [ "dist"