From 5539625f645704e97fabc72eadc9fcf5094cf6b4 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 19 Oct 2020 14:15:46 +0200
Subject: [PATCH 01/77] 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 65ba6140b2..487e4ad0c5 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 bdc3851318..3851556882 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 0000000000..13573efa9c
--- /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 0000000000..0335cb5a9b
--- /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 0000000000..264d6f801f
--- /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 0000000000..33054c6163
--- /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 0000000000..224e293890
--- /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 0000000000..8d4545ac12
--- /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 0000000000..8f8523b78b
--- /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 0000000000..d7857386de
--- /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/77] 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 0000000000..3dadacadfb
--- /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'}
+
+ }
+ />
+
+
+
+
+
+
+
+
+ ));
+};
+
+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) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ));
+ 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 0000000000..237ffa2011
--- /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 224e293890..b24018887a 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/77] 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 f30e7ff34f..51467fc2bb 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/77] 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 0000000000..217b93a400
--- /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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 0000000000..6f6d194a77
--- /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'}
+
+ }
+ />
+
+
+
+
+
+
+
+
+ ));
+};
+
+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 3dadacadfb..cd51c4bcee 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'}
-
- }
- />
-
-
-
-
-
-
-
-
- ));
-};
-
-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) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ));
- 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) ? (
+
+ ) : (
-
+
+
+
+ {/* */}
+
+ Trigger Alarm
+
+
);
};
-
-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 0000000000..b3b94f0200
--- /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/77] 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 cd51c4bcee..e60575ee9b 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) ? (
) : (
-
+
- {/* */}
-
- Trigger Alarm
-
+
);
diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx
new file mode 100644
index 0000000000..578b86d50b
--- /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 (
+ <>
+
+ Trigger alarm
+
+ {showDialog && (
+
+ )}
+ >
+ );
+};
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx
new file mode 100644
index 0000000000..29378cce63
--- /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.
+
+
+
+
+
+ Trigger
+
+
+ Close
+
+
+ {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/77] 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 33054c6163..e5d0264be3 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 0000000000..d5db842dab
--- /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 217b93a400..74e253d516 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 6f6d194a77..98a26c66db 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 e60575ee9b..e0564d76ce 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 578b86d50b..eb051e68fa 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 (
<>
{
{showDialog && (
)}
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx
index 29378cce63..7dbb0be5f0 100644
--- a/plugins/pagerduty/src/components/TriggerDialog.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog.tsx
@@ -23,11 +23,10 @@ import {
DialogActions,
Button,
DialogContent,
- FormControl,
- FormHelperText,
} from '@material-ui/core';
import { Progress, useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
+import { triggerPagerDutyAlarm } from '../api/pagerDutyClient';
const useStyles = makeStyles({
warningText: {
@@ -54,16 +53,16 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
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();
- });
+ const [{ value, loading, error }, triggerAlarm] = useAsyncFn(
+ async (desc: string) => {
+ return await triggerPagerDutyAlarm(
+ integrationKey,
+ window.location.toString(),
+ desc,
+ userId,
+ );
+ },
+ );
useEffect(() => {
if (value) {
@@ -75,14 +74,14 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
if (error) {
alertApi.post({
- message: `Failed to trigger alarm, ${error.message}`,
+ message: `Failed to trigger alarm. ${error.message}`,
severity: 'error',
});
}
- }, [value, error]);
+ }, [value, error, alertApi, onClose, userId]);
return (
-
+
This action will send PagerDuty alarms and notifications to on-call
people responsible for {name}.
@@ -117,7 +116,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
id="trigger"
color="secondary"
disabled={!description || loading}
- onClick={triggerAlarm}
+ onClick={() => triggerAlarm(description)}
>
Trigger
From fe2d8ac7152fc3a2d88df1dea66750f4dffa732a Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Thu, 22 Oct 2020 10:18:46 +0200
Subject: [PATCH 07/77] 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 51467fc2bb..7629c86a1e 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 d5db842dab..fa42c82f9e 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 e0564d76ce..917ae27090 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/77] 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 ef55b007a7..c31629e82e 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 18fd44f089..0ca17c7d7f 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 0000000000..6dc3d8c9ba
--- /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 0000000000..16a033dbc6
--- /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 0000000000..099a94a85f
--- /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 0000000000..cad4ba268e
--- /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 0000000000..7612c392a2
--- /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 0000000000..b96989e4b8
--- /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 0000000000..0aaeafa379
--- /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 0000000000..eb7cc734c2
--- /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 0000000000..73f928bef4
--- /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 0000000000..a5907fd52f
--- /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/77] 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 eb7cc734c2..192a22019a 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/77] added error handling
---
.../pagerduty-backend/src/service/router.ts | 98 +++++++++++++------
plugins/pagerduty/src/api/pagerDutyClient.ts | 20 ----
.../src/components/PagerDutyServiceCard.tsx | 10 +-
3 files changed, 71 insertions(+), 57 deletions(-)
diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts
index 192a22019a..1fbab9b551 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);
+ 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);
+ 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 fa42c82f9e..dc2176680d 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 917ae27090..b7fde8eae3 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/77] 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 | 105 +++++++++---------
5 files changed, 105 insertions(+), 63 deletions(-)
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 7629c86a1e..ddc8ed4b13 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 dc2176680d..dbdc6622f4 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 74e253d516..69a4543c4b 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 98a26c66db..d2f6fc7128 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 b7fde8eae3..e17e35a996 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);
- const { value, loading, error } = useAsync(async () => {
- 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}`);
+ // TODO: handle missing token
+ const token = configApi.getOptionalString('pagerduty.api.token') ?? undefined;
+ console.log({ token });
- // 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',
- },
- ],
- };
+ 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,
+ );
+
+ return {
+ pagerDutyServices: [
+ {
+ activeIncidents: incidents,
+ escalationPolicy: 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}`);
+ }
+
+ 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/77] fix typings
---
.../pagerduty/src/components/Escalation.tsx | 12 +--
.../pagerduty/src/components/Incidents.tsx | 15 +--
plugins/pagerduty/src/components/types.tsx | 92 +++++++++++++++----
3 files changed, 88 insertions(+), 31 deletions(-)
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx
index 69a4543c4b..19c6a15096 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 d2f6fc7128..0117281a0e 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 b3b94f0200..377ae6d0ea 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 = {
+export type Service = {
+ id: string;
+ name: string;
+ html_url: string;
+ integrationKey: string;
+ escalation_policy: PagerDutyEscalationPolicy;
+};
+
+export type ResponseService = {
+ services: Service[];
+ more: boolean;
+};
+
+export type ResponseOncall = {
+ id: string;
+ name: string;
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[];
+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/77] add utility api and use it in pluign
---
plugins/pagerduty/src/api/pagerDutyClient.ts | 178 ++++++++++--------
.../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, 149 insertions(+), 122 deletions(-)
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index dbdc6622f4..09abaf856c 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';
+import { createApiRef } from '@backstage/core';
+import {
+ Service,
+ Incident,
+ Options,
+ PagerDutyClientConfig,
+ ServicesResponse,
+ IncidentResponse,
+ OncallsResponse,
+ Oncall,
+} from '../components/types';
-type Options = {
- method: string;
- headers: {
- 'Content-Type': string;
- Accept: string;
- Authorization?: string;
- };
- body?: string;
-};
+export const pagerDutyApiRef = createApiRef({
+ id: 'plugin.pagerduty.api',
+ description: 'Used to fetch data from PagerDuty API',
+});
-const request = async (
- url: string,
- options: any, //Options,
-): Promise => {
- const response = await fetch(url, options);
+interface PagerDutyClient {
+ getServiceByIntegrationKey(integrationKey: string): Promise;
+ getIncidentsByServiceId(serviceId: string): Promise;
+ getOncallByPolicyId(policyId: string): Promise;
+}
- 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}`;
+export class PagerDutyClientApi implements PagerDutyClient {
+ private API_URL = 'https://api.pagerduty.com';
+ private EVENTS_API_URL = 'https://events.pagerduty.com/v2';
- throw new Error(message);
+ 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;
}
- return await response.json();
-};
+ async getIncidentsByServiceId(serviceId: string): Promise {
+ if (!this.config?.token) {
+ throw new Error('Missing token');
+ }
-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);
-};
+ const params = `service_ids[]=${serviceId}`;
+ const url = `${this.API_URL}/incidents?${params}`;
+ const { incidents } = await this.getByUrl(url);
-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 incidents;
}
- return response.services[0];
-};
-export const getIncidentsByServiceId = async (
- serviceId: string,
- token: string,
-) => {
- return await getByUrl(
- `${API_URL}/incidents?service_ids[]=${serviceId}`,
- token,
- );
-};
+ async getOncallByPolicyId(policyId: string): Promise {
+ if (!this.config?.token) {
+ throw new Error('Missing token');
+ }
-export const getOncallByPolicyId = async (policyId: string, token: string) => {
- return await getByUrl(
- `${API_URL}/oncalls?include[]=users&escalation_policy_ids[]=${policyId}`,
- token,
- );
-};
+ const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
+ const url = `${this.API_URL}/oncalls?${params}`;
+ const { oncalls } = await this.getByUrl(url);
-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({
+ return oncalls;
+ }
+
+ 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,
},
},
- }),
- };
+ });
- return request(`${EVENTS_API_URL}/enqueue`, options);
+ 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 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 0117281a0e..dd3301904d 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 e17e35a996..54713b8ecd 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 7dbb0be5f0..13c2ba6aad 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 8f8523b78b..4073551dc8 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/77] 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 19c6a15096..f53f79af41 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"
>
-
+
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index dd3301904d..512836422c 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) => {
-
+
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 54713b8ecd..4fd4691458 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 0000000000..2af4d2e4bc
--- /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 (
+
+
+ //
+
+ }
+ >
+ }
+ action={TriggerAlarm.action}
+ >
+
+ );
+ };
+
+ return (
+
+
+
+ {content}
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Pd.tsx b/plugins/pagerduty/src/components/Pd.tsx
new file mode 100644
index 0000000000..3698d0a1c4
--- /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 eb051e68fa..48bed68e2e 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 (
<>
- Trigger alarm
+ Trigger Alarm
{showDialog && (
{
the issue and reach out to you if necessary.
{
Date: Wed, 11 Nov 2020 14:25:59 +0100
Subject: [PATCH 15/77] add test
---
.../src/components/Escalation.test.tsx | 34 ++++++
.../src/components/Incidents.test.tsx | 71 ++++++++++++
.../src/components/TriggerDialog.test.tsx | 101 ++++++++++++++++++
3 files changed, 206 insertions(+)
create mode 100644 plugins/pagerduty/src/components/Escalation.test.tsx
create mode 100644 plugins/pagerduty/src/components/Incidents.test.tsx
create mode 100644 plugins/pagerduty/src/components/TriggerDialog.test.tsx
diff --git a/plugins/pagerduty/src/components/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation.test.tsx
new file mode 100644
index 0000000000..db52e36f4a
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation.test.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { EscalationPolicy } from './Escalation';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { Oncall } from './types';
+
+export const escalations: Oncall[] = [
+ {
+ user: {
+ name: 'person1',
+ id: 'p1',
+ summary: 'person1',
+ email: 'person1@example.com',
+ html_url: 'http://a.com/id1',
+ },
+ },
+];
+
+describe('Escalation', () => {
+ it('render emptyState', () => {
+ const { getByText } = render(
+ wrapInTestApp( ),
+ );
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
+ it('render Escalation list', () => {
+ const { getByText } = render(
+ wrapInTestApp( ),
+ );
+ expect(getByText('person1')).toBeInTheDocument();
+ expect(getByText('person1@example.com')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/pagerduty/src/components/Incidents.test.tsx b/plugins/pagerduty/src/components/Incidents.test.tsx
new file mode 100644
index 0000000000..62c8151af2
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incidents.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { Incidents } from './Incidents';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { Incident } from './types';
+
+export 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',
+ },
+];
+
+describe('Incidents', () => {
+ it('renders an empty state is there are no incidents', () => {
+ const { getByText } = render(wrapInTestApp( ));
+ expect(getByText('No incidents')).toBeInTheDocument();
+ });
+
+ it('renders all incidents', () => {
+ const { getByText, getByTitle, getAllByTitle, getByLabelText } = render(
+ wrapInTestApp( ),
+ );
+
+ expect(getByText('title1')).toBeInTheDocument();
+ expect(getByText('title2')).toBeInTheDocument();
+ expect(getByText('person1')).toBeInTheDocument();
+ expect(getByText('person2')).toBeInTheDocument();
+ expect(getByTitle('triggered')).toBeInTheDocument();
+ expect(getByTitle('acknowledged')).toBeInTheDocument();
+ expect(getByLabelText('Status error')).toBeInTheDocument();
+ expect(getByLabelText('Status warning')).toBeInTheDocument();
+
+ // assert links, mailto and hrefs, date calculation
+ expect(getAllByTitle('View in PagerDuty').length).toEqual(2);
+ });
+});
diff --git a/plugins/pagerduty/src/components/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog.test.tsx
new file mode 100644
index 0000000000..a99e2c03f4
--- /dev/null
+++ b/plugins/pagerduty/src/components/TriggerDialog.test.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import { render, fireEvent, getByTestId } from '@testing-library/react';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { TriggerDialog } from './TriggerDialog';
+import {
+ ApiRegistry,
+ alertApiRef,
+ createApiRef,
+ ApiProvider,
+ IdentityApi,
+ identityApiRef,
+} from '@backstage/core';
+import { pagerDutyApiRef } from '../api/pagerDutyClient';
+import { TriggerButton } from './TriggerButton';
+import { Entity } from '@backstage/catalog-model';
+import { act } from 'react-dom/test-utils';
+
+describe('TriggerDialog', () => {
+ const mockIdentityApi: Partial = {
+ getUserId: () => 'guest@example.com',
+ };
+
+ const mockTriggerAlarmFn = jest.fn();
+ const mockPagerDutyApi = {
+ triggerPagerDutyAlarm: mockTriggerAlarmFn,
+ };
+
+ const apis = ApiRegistry.from([
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+ [identityApiRef, mockIdentityApi],
+ [pagerDutyApiRef, mockPagerDutyApi],
+ ]);
+
+ it('open the dialog and trigger an alarm', async () => {
+ const onClick = jest.fn(async () => {});
+
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'pagerduty-test',
+ annotations: {
+ 'pagerduty.com/integration-key': 'abc123',
+ },
+ },
+ };
+
+ const {
+ getByText,
+ getByRole,
+ queryByText,
+ queryByRole,
+ getByTestId,
+ } = render(
+ wrapInTestApp(
+
+
+ {/* */}
+ ,
+ ),
+ );
+ 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,
+ },
+ ),
+ ).toBeInTheDocument();
+ const input = getByTestId('trigger-input');
+ const description = 'Test Trigger Alarm';
+ await act(async () => {
+ fireEvent.change(input, { target: { value: description } });
+ });
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(mockTriggerAlarmFn).toHaveBeenCalled();
+ expect(mockTriggerAlarmFn).toHaveBeenCalledWith(
+ entity!.metadata!.annotations!['pagerduty.com/integration-key'],
+ window.location.toString(),
+ description,
+ 'guest@example.com',
+ );
+ });
+});
From 2be408a226e6ba7924057c0294b29fae4cb97406 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Wed, 11 Nov 2020 16:48:44 +0100
Subject: [PATCH 16/77] recator UI and remove comments
---
plugins/pagerduty/src/api/pagerDutyClient.ts | 12 ++++-----
plugins/pagerduty/src/assets/emptystate.svg | 26 +++++++++++++++++++
.../src/components/Escalation.test.tsx | 4 +--
.../pagerduty/src/components/Escalation.tsx | 25 +++++++++++++-----
.../pagerduty/src/components/Incidents.tsx | 19 +++++++++++---
.../src/components/PagerDutyServiceCard.tsx | 2 +-
.../src/components/PagerdutyCard.tsx | 11 +-------
plugins/pagerduty/src/components/Pd.tsx | 14 ----------
plugins/pagerduty/src/components/types.tsx | 10 +++----
9 files changed, 74 insertions(+), 49 deletions(-)
create mode 100644 plugins/pagerduty/src/assets/emptystate.svg
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index 09abaf856c..d0cb3719d6 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -22,8 +22,8 @@ import {
PagerDutyClientConfig,
ServicesResponse,
IncidentResponse,
- OncallsResponse,
- Oncall,
+ OnCallsResponse,
+ OnCall,
} from '../components/types';
export const pagerDutyApiRef = createApiRef({
@@ -34,7 +34,7 @@ export const pagerDutyApiRef = createApiRef({
interface PagerDutyClient {
getServiceByIntegrationKey(integrationKey: string): Promise;
getIncidentsByServiceId(serviceId: string): Promise;
- getOncallByPolicyId(policyId: string): Promise;
+ getOnCallByPolicyId(policyId: string): Promise;
}
export class PagerDutyClientApi implements PagerDutyClient {
@@ -67,16 +67,16 @@ export class PagerDutyClientApi implements PagerDutyClient {
return incidents;
}
- async getOncallByPolicyId(policyId: string): Promise {
+ 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);
+ const { onCalls } = await this.getByUrl(url);
- return oncalls;
+ return onCalls;
}
triggerPagerDutyAlarm(
diff --git a/plugins/pagerduty/src/assets/emptystate.svg b/plugins/pagerduty/src/assets/emptystate.svg
new file mode 100644
index 0000000000..57b35238dd
--- /dev/null
+++ b/plugins/pagerduty/src/assets/emptystate.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/pagerduty/src/components/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation.test.tsx
index db52e36f4a..13e2687b04 100644
--- a/plugins/pagerduty/src/components/Escalation.test.tsx
+++ b/plugins/pagerduty/src/components/Escalation.test.tsx
@@ -2,9 +2,9 @@ import React from 'react';
import { render } from '@testing-library/react';
import { EscalationPolicy } from './Escalation';
import { wrapInTestApp } from '@backstage/test-utils';
-import { Oncall } from './types';
+import { OnCall } from './types';
-export const escalations: Oncall[] = [
+export const escalations: OnCall[] = [
{
user: {
name: 'person1',
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx
index f53f79af41..56c2c27a03 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -25,11 +25,12 @@ import {
ListItemText,
makeStyles,
IconButton,
+ Typography,
} from '@material-ui/core';
-import UserIcon from '@material-ui/icons/Person';
+import Avatar from '@material-ui/core/Avatar';
import EmailIcon from '@material-ui/icons/Email';
import { StatusWarning } from '@backstage/core';
-import { Oncall } from './types';
+import { OnCall } from './types';
import PagerdutyIcon from './Pd';
const useStyles = makeStyles({
@@ -43,16 +44,26 @@ const useStyles = makeStyles({
svgButtonImage: {
height: '1em',
},
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
});
-const EscalationUser = ({ user }: Oncall) => {
+const EscalationUser = ({ user }: OnCall) => {
const classes = useStyles();
return (
-
+
-
+
+ {user.name}
+
+ }
+ secondary={user.email}
+ />
@@ -89,11 +100,11 @@ const EscalationUsersEmptyState = () => {
};
type EscalationPolicyProps = {
- escalation: Oncall[];
+ escalation: OnCall[];
};
export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => (
- Escalation Policy}>
+ ON CALL}>
{escalation.length ? (
escalation.map((item, index) => (
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index 512836422c..52247b8f06 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -26,6 +26,7 @@ import {
IconButton,
ListSubheader,
Link,
+ Typography,
} from '@material-ui/core';
import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
import moment from 'moment';
@@ -43,6 +44,12 @@ const useStyles = makeStyles({
svgButtonImage: {
height: '1em',
},
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
+ listItemIcon: {
+ minWidth: '1em',
+ },
});
const IncidentsEmptyState = () => {
@@ -67,8 +74,8 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => {
const classes = useStyles();
const user = incident.assignments[0].assignee;
return (
-
-
+
+
{incident.status === 'triggered' ? (
@@ -80,7 +87,11 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => {
+ {incident.title}
+
+ }
secondary={
Created {moment(incident.created_at).fromNow()}, assigned to{' '}
@@ -112,7 +123,7 @@ type IncidentsProps = {
};
export const Incidents = ({ incidents }: IncidentsProps) => (
- Incidents}>
+ INCIDENTS}>
{incidents.length ? (
incidents.map((incident, index) => (
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 4fd4691458..5bb7c9b5a9 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -47,7 +47,7 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
const service = services[0];
const incidents = await api.getIncidentsByServiceId(service.id);
- const oncalls = await api.getOncallByPolicyId(service.escalation_policy.id);
+ const oncalls = await api.getOnCallByPolicyId(service.escalation_policy.id);
return {
incidents,
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index 2af4d2e4bc..539bf69540 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -10,7 +10,6 @@ import {
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
@@ -100,15 +99,7 @@ export const PagerdutyCard = ({
- //
-
- }
+ icon={ }
>
-// React.createElement(
-// SvgIcon,
-// props,
-//
-//
-// ,
-// );
-
-// export default SvgPd;
-
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index ca06e79510..dec8f18739 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -32,7 +32,7 @@ export type Service = {
name: string;
html_url: string;
integrationKey: string;
- escalation_policy: Oncall;
+ escalation_policy: OnCall;
};
export type ResponseService = {
@@ -40,7 +40,7 @@ export type ResponseService = {
more: boolean;
};
-export type ResponseOncall = {
+export type ResponseOnCall = {
id: string;
name: string;
email: string;
@@ -65,8 +65,8 @@ export type IncidentResponse = {
incidents: Incident[];
};
-export type OncallsResponse = {
- oncalls: Oncall[];
+export type OnCallsResponse = {
+ onCalls: OnCall[];
};
export type User = {
@@ -77,6 +77,6 @@ export type User = {
name: string;
};
-export type Oncall = {
+export type OnCall = {
user: User;
};
From ab0cceaa08777febd7cd1ae54c614b7e050ba40c Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 13 Nov 2020 11:41:06 +0100
Subject: [PATCH 17/77] refactor types
---
plugins/pagerduty/src/api/pagerDutyClient.ts | 19 +++---
.../src/components/PagerDutyServiceCard.tsx | 8 +--
plugins/pagerduty/src/components/Pd.tsx | 16 +++++
plugins/pagerduty/src/components/types.tsx | 62 ++++++++-----------
4 files changed, 58 insertions(+), 47 deletions(-)
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index d0cb3719d6..7b1aa442c3 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -18,10 +18,10 @@ import { createApiRef } from '@backstage/core';
import {
Service,
Incident,
- Options,
- PagerDutyClientConfig,
+ RequestOptions,
+ ClientApiConfig,
ServicesResponse,
- IncidentResponse,
+ IncidentsResponse,
OnCallsResponse,
OnCall,
} from '../components/types';
@@ -41,7 +41,7 @@ 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) {}
+ constructor(private readonly config?: ClientApiConfig) {}
async getServiceByIntegrationKey(integrationKey: string): Promise {
if (!this.config?.token) {
@@ -62,7 +62,7 @@ export class PagerDutyClientApi implements PagerDutyClient {
const params = `service_ids[]=${serviceId}`;
const url = `${this.API_URL}/incidents?${params}`;
- const { incidents } = await this.getByUrl(url);
+ const { incidents } = await this.getByUrl(url);
return incidents;
}
@@ -74,9 +74,9 @@ export class PagerDutyClientApi implements PagerDutyClient {
const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
const url = `${this.API_URL}/oncalls?${params}`;
- const { onCalls } = await this.getByUrl(url);
+ const { oncalls } = await this.getByUrl(url);
- return onCalls;
+ return oncalls;
}
triggerPagerDutyAlarm(
@@ -127,7 +127,10 @@ export class PagerDutyClientApi implements PagerDutyClient {
return response.json();
}
- private async request(url: string, options: Options): Promise {
+ private async request(
+ url: string,
+ options: RequestOptions,
+ ): Promise {
try {
const response = await fetch(url, options);
if (!response.ok) {
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 5bb7c9b5a9..d343a9fea1 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -45,13 +45,13 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
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);
+ const users = oncalls.map(item => item.user);
return {
incidents,
- oncalls,
+ users,
id: service.id,
name: service.name,
homepageUrl: service.html_url,
@@ -89,9 +89,9 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
content={
<>
-
+
>
}
- >
+ />
);
};
diff --git a/plugins/pagerduty/src/components/Pd.tsx b/plugins/pagerduty/src/components/Pd.tsx
index eeaea87a7f..36f6e1989d 100644
--- a/plugins/pagerduty/src/components/Pd.tsx
+++ b/plugins/pagerduty/src/components/Pd.tsx
@@ -1,3 +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 React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index dec8f18739..b229949cb4 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
export type Incident = {
id: string;
title: string;
@@ -32,41 +33,14 @@ export type Service = {
name: string;
html_url: string;
integrationKey: string;
- escalation_policy: OnCall;
+ escalation_policy: {
+ id: string;
+ user: User;
+ };
};
-export type ResponseService = {
- services: Service[];
- more: boolean;
-};
-
-export type ResponseOnCall = {
- id: string;
- name: string;
- email: string;
- homepageUrl: string;
-};
-
-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 OnCall = {
+ user: User;
};
export type User = {
@@ -77,6 +51,24 @@ export type User = {
name: string;
};
-export type OnCall = {
- user: User;
+export type ClientApiConfig = {
+ token?: 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;
};
From f9c971b3f7a8ee85e994aeb5ef79b0d5a097e3f5 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 13 Nov 2020 13:41:46 +0100
Subject: [PATCH 18/77] refactor code
---
plugins/pagerduty/package.json | 1 +
.../src/components/Escalation.test.tsx | 34 -------------
.../components/Escalation/Escalation.test.tsx | 47 +++++++++++++++++
.../Escalation/EscalationPolicy.tsx | 36 +++++++++++++
.../EscalationUser.tsx} | 49 ++----------------
.../Escalation/EscalationUsersEmptyState.tsx | 48 +++++++++++++++++
.../Incident/IncidentEmptyState.tsx | 36 +++++++++++++
.../IncidentListItem.tsx} | 51 +++----------------
.../{ => Incident}/Incidents.test.tsx | 23 +++++++--
.../src/components/Incident/Incidents.tsx | 40 +++++++++++++++
.../src/components/PagerDutyServiceCard.tsx | 4 +-
.../src/components/TriggerDialog.test.tsx | 33 ++++++------
12 files changed, 258 insertions(+), 144 deletions(-)
delete mode 100644 plugins/pagerduty/src/components/Escalation.test.tsx
create mode 100644 plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
create mode 100644 plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
rename plugins/pagerduty/src/components/{Escalation.tsx => Escalation/EscalationUser.tsx} (63%)
create mode 100644 plugins/pagerduty/src/components/Escalation/EscalationUsersEmptyState.tsx
create mode 100644 plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx
rename plugins/pagerduty/src/components/{Incidents.tsx => Incident/IncidentListItem.tsx} (72%)
rename plugins/pagerduty/src/components/{ => Incident}/Incidents.test.tsx (71%)
create mode 100644 plugins/pagerduty/src/components/Incident/Incidents.tsx
diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json
index e5d0264be3..43aad8579a 100644
--- a/plugins/pagerduty/package.json
+++ b/plugins/pagerduty/package.json
@@ -23,6 +23,7 @@
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
+ "@backstage/test-utils": "^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",
diff --git a/plugins/pagerduty/src/components/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation.test.tsx
deleted file mode 100644
index 13e2687b04..0000000000
--- a/plugins/pagerduty/src/components/Escalation.test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { render } from '@testing-library/react';
-import { EscalationPolicy } from './Escalation';
-import { wrapInTestApp } from '@backstage/test-utils';
-import { OnCall } from './types';
-
-export const escalations: OnCall[] = [
- {
- user: {
- name: 'person1',
- id: 'p1',
- summary: 'person1',
- email: 'person1@example.com',
- html_url: 'http://a.com/id1',
- },
- },
-];
-
-describe('Escalation', () => {
- it('render emptyState', () => {
- const { getByText } = render(
- wrapInTestApp( ),
- );
- expect(getByText('Empty escalation policy')).toBeInTheDocument();
- });
-
- it('render Escalation list', () => {
- const { getByText } = render(
- wrapInTestApp( ),
- );
- expect(getByText('person1')).toBeInTheDocument();
- expect(getByText('person1@example.com')).toBeInTheDocument();
- });
-});
diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
new file mode 100644
index 0000000000..3e4a59d15a
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 } from '@testing-library/react';
+import { EscalationPolicy } from './EscalationPolicy';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { User } from '../types';
+
+const escalations: User[] = [
+ {
+ name: 'person1',
+ id: 'p1',
+ summary: 'person1',
+ email: 'person1@example.com',
+ html_url: 'http://a.com/id1',
+ },
+];
+
+describe('Escalation', () => {
+ it('render emptyState', () => {
+ const { getByText } = render(
+ wrapInTestApp( ),
+ );
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
+ it('render Escalation list', () => {
+ const { getByText } = render(
+ wrapInTestApp( ),
+ );
+ expect(getByText('person1')).toBeInTheDocument();
+ expect(getByText('person1@example.com')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
new file mode 100644
index 0000000000..8a2aacead9
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 } from '@material-ui/core';
+import { User } from '../types';
+import { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
+import { EscalationUser } from './EscalationUser';
+
+type EscalationPolicyProps = {
+ users: User[];
+};
+
+export const EscalationPolicy = ({ users }: EscalationPolicyProps) => (
+ ON CALL}>
+ {users.length ? (
+ users.map((user, index) => )
+ ) : (
+ // TODO: how does it look if we used an EmptyState component here
+
+ )}
+
+);
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
similarity index 63%
rename from plugins/pagerduty/src/components/Escalation.tsx
rename to plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
index 56c2c27a03..3fa8ef8d65 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
@@ -16,8 +16,6 @@
import React from 'react';
import {
- List,
- ListSubheader,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
@@ -29,27 +27,16 @@ import {
} from '@material-ui/core';
import Avatar from '@material-ui/core/Avatar';
import EmailIcon from '@material-ui/icons/Email';
-import { StatusWarning } from '@backstage/core';
-import { OnCall } from './types';
-import PagerdutyIcon from './Pd';
+import PagerdutyIcon from '../Pd';
+import { User } from '../types';
const useStyles = makeStyles({
- denseListIcon: {
- marginRight: 0,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- },
- svgButtonImage: {
- height: '1em',
- },
listItemPrimary: {
fontWeight: 'bold',
},
});
-const EscalationUser = ({ user }: OnCall) => {
+export const EscalationUser = ({ user }: { user: User }) => {
const classes = useStyles();
return (
@@ -84,33 +71,3 @@ const EscalationUser = ({ user }: OnCall) => {
);
};
-
-const EscalationUsersEmptyState = () => {
- const classes = useStyles();
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-type EscalationPolicyProps = {
- escalation: OnCall[];
-};
-
-export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => (
- ON CALL}>
- {escalation.length ? (
- escalation.map((item, index) => (
-
- ))
- ) : (
-
- )}
-
-);
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUsersEmptyState.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUsersEmptyState.tsx
new file mode 100644
index 0000000000..d587011601
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUsersEmptyState.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 {
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ makeStyles,
+} from '@material-ui/core';
+import { StatusWarning } from '@backstage/core';
+
+const useStyles = makeStyles({
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export const EscalationUsersEmptyState = () => {
+ const classes = useStyles();
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx
new file mode 100644
index 0000000000..ded161261e
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 { Grid } from '@material-ui/core';
+import EmptyStateImage from '../../assets/emptyState.svg';
+
+export const IncidentsEmptyState = () => {
+ return (
+
+
+ Nice! No incidents are assigned to you!
+
+
+
+
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
similarity index 72%
rename from plugins/pagerduty/src/components/Incidents.tsx
rename to plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
index 52247b8f06..f01c99fc8c 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
@@ -16,7 +16,6 @@
import React from 'react';
import {
- List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
@@ -24,14 +23,17 @@ import {
ListItemText,
makeStyles,
IconButton,
- ListSubheader,
Link,
Typography,
} from '@material-ui/core';
-import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
+import { StatusError, StatusWarning } from '@backstage/core';
import moment from 'moment';
-import { Incident } from '../components/types';
-import PagerdutyIcon from './Pd';
+import { Incident } from '../types';
+import PagerdutyIcon from '../Pd';
+
+type IncidentListItemProps = {
+ incident: Incident;
+};
const useStyles = makeStyles({
denseListIcon: {
@@ -41,9 +43,6 @@ const useStyles = makeStyles({
alignItems: 'center',
justifyContent: 'center',
},
- svgButtonImage: {
- height: '1em',
- },
listItemPrimary: {
fontWeight: 'bold',
},
@@ -52,25 +51,7 @@ const useStyles = makeStyles({
},
});
-const IncidentsEmptyState = () => {
- const classes = useStyles();
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-type IncidentListItemProps = {
- incident: Incident;
-};
-
-const IncidentListItem = ({ incident }: IncidentListItemProps) => {
+export const IncidentListItem = ({ incident }: IncidentListItemProps) => {
const classes = useStyles();
const user = incident.assignments[0].assignee;
return (
@@ -117,19 +98,3 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => {
);
};
-
-type IncidentsProps = {
- incidents: Incident[];
-};
-
-export const Incidents = ({ incidents }: IncidentsProps) => (
- INCIDENTS}>
- {incidents.length ? (
- incidents.map((incident, index) => (
-
- ))
- ) : (
-
- )}
-
-);
diff --git a/plugins/pagerduty/src/components/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
similarity index 71%
rename from plugins/pagerduty/src/components/Incidents.test.tsx
rename to plugins/pagerduty/src/components/Incident/Incidents.test.tsx
index 62c8151af2..fbe3783d30 100644
--- a/plugins/pagerduty/src/components/Incidents.test.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
@@ -1,10 +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 React from 'react';
import { render } from '@testing-library/react';
import { Incidents } from './Incidents';
import { wrapInTestApp } from '@backstage/test-utils';
-import { Incident } from './types';
+import { Incident } from '../types';
-export const incidents: Incident[] = [
+const incidents: Incident[] = [
{
id: 'id1',
status: 'triggered',
@@ -48,7 +63,9 @@ export const incidents: Incident[] = [
describe('Incidents', () => {
it('renders an empty state is there are no incidents', () => {
const { getByText } = render(wrapInTestApp( ));
- expect(getByText('No incidents')).toBeInTheDocument();
+ expect(
+ getByText('Nice! No incidents are assigned to you!'),
+ ).toBeInTheDocument();
});
it('renders all incidents', () => {
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx
new file mode 100644
index 0000000000..a49d913c80
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 } from '@material-ui/core';
+import { Incident } from '../types';
+import { IncidentListItem } from './IncidentListItem';
+import { IncidentsEmptyState } from './IncidentEmptyState';
+
+type IncidentsProps = {
+ incidents: Incident[];
+};
+
+export const Incidents = ({ incidents }: IncidentsProps) => (
+ {incidents.length && 'INCIDENTS'}}
+ >
+ {incidents.length ? (
+ incidents.map((incident, index) => (
+
+ ))
+ ) : (
+
+ )}
+
+);
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index d343a9fea1..480e7cdbd6 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -17,8 +17,8 @@ import React from 'react';
import { useApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { LinearProgress } from '@material-ui/core';
-import { Incidents } from './Incidents';
-import { EscalationPolicy } from './Escalation';
+import { Incidents } from './Incident/Incidents';
+import { EscalationPolicy } from './Escalation/EscalationPolicy';
import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
diff --git a/plugins/pagerduty/src/components/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog.test.tsx
index a99e2c03f4..387b168e05 100644
--- a/plugins/pagerduty/src/components/TriggerDialog.test.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog.test.tsx
@@ -1,7 +1,21 @@
+/*
+ * 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, fireEvent, getByTestId } from '@testing-library/react';
+import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
-import { TriggerDialog } from './TriggerDialog';
import {
ApiRegistry,
alertApiRef,
@@ -38,8 +52,6 @@ describe('TriggerDialog', () => {
]);
it('open the dialog and trigger an alarm', async () => {
- const onClick = jest.fn(async () => {});
-
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -51,21 +63,10 @@ describe('TriggerDialog', () => {
},
};
- const {
- getByText,
- getByRole,
- queryByText,
- queryByRole,
- getByTestId,
- } = render(
+ const { getByText, getByRole, queryByRole, getByTestId } = render(
wrapInTestApp(
- {/* */}
,
),
);
From 00a2ead383598e17bf5f5a9d833656520daf4a57 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 16 Nov 2020 16:09:45 +0100
Subject: [PATCH 19/77] refactor code an structure
---
.../components/Escalation/Escalation.test.tsx | 2 +-
.../Escalation/EscalationPolicy.tsx | 1 -
.../components/Escalation/EscalationUser.tsx | 2 +-
.../src/components/Escalation/index.ts | 17 ++
.../components/Incident/IncidentListItem.tsx | 2 +-
.../src/components/Incident/index.ts | 17 ++
.../components/{Pd.tsx => PagerDutyIcon.tsx} | 0
.../src/components/PagerDutyServiceCard.tsx | 97 ---------
.../src/components/PagerdutyCard.tsx | 194 ++++++++++--------
.../{ => TriggerButton}/TriggerButton.tsx | 4 +-
.../src/components/TriggerButton/index.ts | 17 ++
.../TriggerDialog.test.tsx | 4 +-
.../{ => TriggerDialog}/TriggerDialog.tsx | 2 +-
.../src/components/TriggerDialog/index.ts | 17 ++
plugins/pagerduty/src/components/types.tsx | 9 +-
15 files changed, 186 insertions(+), 199 deletions(-)
create mode 100644 plugins/pagerduty/src/components/Escalation/index.ts
create mode 100644 plugins/pagerduty/src/components/Incident/index.ts
rename plugins/pagerduty/src/components/{Pd.tsx => PagerDutyIcon.tsx} (100%)
delete mode 100644 plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
rename plugins/pagerduty/src/components/{ => TriggerButton}/TriggerButton.tsx (93%)
create mode 100644 plugins/pagerduty/src/components/TriggerButton/index.ts
rename plugins/pagerduty/src/components/{ => TriggerDialog}/TriggerDialog.test.tsx (96%)
rename plugins/pagerduty/src/components/{ => TriggerDialog}/TriggerDialog.tsx (98%)
create mode 100644 plugins/pagerduty/src/components/TriggerDialog/index.ts
diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
index 3e4a59d15a..4fa65eb999 100644
--- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
+++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
@@ -37,7 +37,7 @@ describe('Escalation', () => {
expect(getByText('Empty escalation policy')).toBeInTheDocument();
});
- it('render Escalation list', () => {
+ it('render escalation list', () => {
const { getByText } = render(
wrapInTestApp( ),
);
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
index 8a2aacead9..cbce0ce4a3 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
@@ -29,7 +29,6 @@ export const EscalationPolicy = ({ users }: EscalationPolicyProps) => (
{users.length ? (
users.map((user, index) => )
) : (
- // TODO: how does it look if we used an EmptyState component here
)}
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
index 3fa8ef8d65..e20a5f7567 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 '../Pd';
+import PagerdutyIcon from '../PagerDutyIcon';
import { User } from '../types';
const useStyles = makeStyles({
diff --git a/plugins/pagerduty/src/components/Escalation/index.ts b/plugins/pagerduty/src/components/Escalation/index.ts
new file mode 100644
index 0000000000..ac2db62cd9
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/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 { EscalationPolicy } from './EscalationPolicy';
diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
index f01c99fc8c..4700583a8b 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 '../Pd';
+import PagerdutyIcon from '../PagerDutyIcon';
type IncidentListItemProps = {
incident: Incident;
diff --git a/plugins/pagerduty/src/components/Incident/index.ts b/plugins/pagerduty/src/components/Incident/index.ts
new file mode 100644
index 0000000000..fb2702602b
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incident/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 { Incidents } from './Incidents';
diff --git a/plugins/pagerduty/src/components/Pd.tsx b/plugins/pagerduty/src/components/PagerDutyIcon.tsx
similarity index 100%
rename from plugins/pagerduty/src/components/Pd.tsx
rename to plugins/pagerduty/src/components/PagerDutyIcon.tsx
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
deleted file mode 100644
index 480e7cdbd6..0000000000
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ /dev/null
@@ -1,97 +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 { useApi } from '@backstage/core';
-import { Entity } from '@backstage/catalog-model';
-import { LinearProgress } from '@material-ui/core';
-import { Incidents } from './Incident/Incidents';
-import { EscalationPolicy } from './Escalation/EscalationPolicy';
-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';
-
-export const isPluginApplicableToEntity = (entity: Entity) =>
- Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
-
-type Props = {
- entity: Entity;
-};
-
-export const PagerDutyServiceCard = ({ entity }: Props) => {
- const api = useApi(pagerDutyApiRef);
-
- const { value, loading, error } = useAsync(async () => {
- const integrationKey = entity.metadata.annotations![
- PAGERDUTY_INTEGRATION_KEY
- ];
-
- 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);
- const users = oncalls.map(item => item.user);
-
- return {
- incidents,
- users,
- id: service.id,
- name: service.name,
- homepageUrl: service.html_url,
- };
- });
-
- if (error) {
- return (
-
-
- Error encountered while fetching information. {error.message}
-
-
- );
- }
-
- if (loading) {
- return ;
- }
-
- const pagerdutyLink = {
- title: 'View in PagerDuty',
- href: value!.homepageUrl,
- };
-
- const triggerAlarm = {
- title: 'Trigger Alarm',
- action: ,
- };
-
- return (
-
-
-
- >
- }
- />
- );
-};
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index 539bf69540..6b6d466850 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -1,18 +1,38 @@
+/*
+ * 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 { useApi, EmptyState } from '@backstage/core';
+import { Entity } from '@backstage/catalog-model';
import {
Card,
- CardHeader,
CardContent,
+ CardHeader,
Divider,
- Link,
+ LinearProgress,
makeStyles,
} from '@material-ui/core';
-import LinkIcon from '@material-ui/icons/Link';
+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 classnames from 'classnames';
-import PagerdutyIcon from './Pd';
-
-// TODO: create a general component to use it in pagerduty and aboutCard
const useStyles = makeStyles(theme => ({
links: {
@@ -22,102 +42,98 @@ const useStyles = makeStyles(theme => ({
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 };
+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;
};
-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) => {
+export const PagerDutyCard = ({ entity }: Props) => {
const classes = useStyles();
- if (action) {
+ const api = useApi(pagerDutyApiRef);
+
+ const { value, loading, error } = useAsync(async () => {
+ const integrationKey = entity.metadata.annotations![
+ PAGERDUTY_INTEGRATION_KEY
+ ];
+
+ 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,
+ };
+ });
+
+ if (error) {
+ if (error instanceof UnauthorizedError) {
+ return (
+
+ );
+ }
+
return (
- <>
-
- {icon}
- {action}
-
- >
+
+ Error encountered while fetching information. {error.message}
+
);
}
- return (
- <>
-
- {icon}
- {label}
-
- >
- );
-};
-export const PagerdutyCard = ({
- title,
- subheader,
- content,
-}: PagerdutyCardProps) => {
- const classes = useStyles();
- const getSubheader = ({ ViewPagerduty, TriggerAlarm }: SubHeaderProps) => {
- return (
-
- }
- >
- }
- action={TriggerAlarm.action}
- >
-
- );
+
+ if (loading) {
+ return ;
+ }
+
+ const pagerdutyLink = {
+ title: 'View in PagerDuty',
+ href: value!.homepageUrl,
+ };
+
+ const triggerAlarm = {
+ title: 'Trigger Alarm',
+ action: ,
};
return (
+ title="PagerDuty"
+ subheader={
+
+ }
+ />
+ }
+ action={triggerAlarm.action}
+ />
+
+ }
+ />
- {content}
+
+
+
+
);
};
diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx
similarity index 93%
rename from plugins/pagerduty/src/components/TriggerButton.tsx
rename to plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx
index 48bed68e2e..8feebf9e89 100644
--- a/plugins/pagerduty/src/components/TriggerButton.tsx
+++ b/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx
@@ -16,9 +16,9 @@
import React, { useState } from 'react';
import { Button, makeStyles } from '@material-ui/core';
-import { TriggerDialog } from './TriggerDialog';
+import { TriggerDialog } from '../TriggerDialog';
import { Entity } from '@backstage/catalog-model';
-import { PAGERDUTY_INTEGRATION_KEY } from './PagerDutyServiceCard';
+import { PAGERDUTY_INTEGRATION_KEY } from '../PagerDutyCard';
const useStyles = makeStyles({
triggerAlarm: {
diff --git a/plugins/pagerduty/src/components/TriggerButton/index.ts b/plugins/pagerduty/src/components/TriggerButton/index.ts
new file mode 100644
index 0000000000..6a58a4f43f
--- /dev/null
+++ b/plugins/pagerduty/src/components/TriggerButton/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 { TriggerButton } from './TriggerButton';
diff --git a/plugins/pagerduty/src/components/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
similarity index 96%
rename from plugins/pagerduty/src/components/TriggerDialog.test.tsx
rename to plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
index 387b168e05..7e26985136 100644
--- a/plugins/pagerduty/src/components/TriggerDialog.test.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -24,8 +24,8 @@ import {
IdentityApi,
identityApiRef,
} from '@backstage/core';
-import { pagerDutyApiRef } from '../api/pagerDutyClient';
-import { TriggerButton } from './TriggerButton';
+import { pagerDutyApiRef } from '../../api';
+import { TriggerButton } from '../TriggerButton';
import { Entity } from '@backstage/catalog-model';
import { act } from 'react-dom/test-utils';
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
similarity index 98%
rename from plugins/pagerduty/src/components/TriggerDialog.tsx
rename to plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
index 08044d02f6..91ec3cd27d 100644
--- a/plugins/pagerduty/src/components/TriggerDialog.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/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 { pagerDutyApiRef } from '../api/pagerDutyClient';
+import { pagerDutyApiRef } from '../../api';
const useStyles = makeStyles({
warningText: {
diff --git a/plugins/pagerduty/src/components/TriggerDialog/index.ts b/plugins/pagerduty/src/components/TriggerDialog/index.ts
new file mode 100644
index 0000000000..655cef8504
--- /dev/null
+++ b/plugins/pagerduty/src/components/TriggerDialog/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 { TriggerDialog } from './TriggerDialog';
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index b229949cb4..9c9e29cb01 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -51,10 +51,6 @@ export type User = {
name: string;
};
-export type ClientApiConfig = {
- token?: string;
-};
-
export type ServicesResponse = {
services: Service[];
};
@@ -72,3 +68,8 @@ export type RequestOptions = {
headers: HeadersInit;
body?: BodyInit;
};
+
+export type ClientApiConfig = {
+ api_url: string;
+ events_url: string;
+};
From 6fc060842ed1de91b0c173b3693e3d3f11c91f9b Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 16 Nov 2020 16:34:49 +0100
Subject: [PATCH 20/77] refactor api to use token from proxy
---
.../app/src/components/catalog/EntityPage.tsx | 4 +-
plugins/pagerduty/package.json | 1 +
.../src/api/{pagerDutyClient.ts => client.ts} | 60 +++++++------------
plugins/pagerduty/src/api/index.ts | 22 +++++++
plugins/pagerduty/src/api/types.ts | 37 ++++++++++++
plugins/pagerduty/src/index.ts | 4 +-
plugins/pagerduty/src/plugin.ts | 7 ++-
7 files changed, 91 insertions(+), 44 deletions(-)
rename plugins/pagerduty/src/api/{pagerDutyClient.ts => client.ts} (68%)
create mode 100644 plugins/pagerduty/src/api/index.ts
create mode 100644 plugins/pagerduty/src/api/types.ts
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index ddc8ed4b13..4fc894ef90 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -68,7 +68,7 @@ import {
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
- PagerDutyServiceCard,
+ PagerDutyCard,
} from '@backstage/plugin-pagerduty';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
@@ -137,7 +137,7 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
{isPagerDutyAvailable(entity) && (
-
+
)}
diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json
index 43aad8579a..29518bd858 100644
--- a/plugins/pagerduty/package.json
+++ b/plugins/pagerduty/package.json
@@ -24,6 +24,7 @@
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
+ "@backstage/plugin-catalog": "^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",
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/client.ts
similarity index 68%
rename from plugins/pagerduty/src/api/pagerDutyClient.ts
rename to plugins/pagerduty/src/api/client.ts
index 7b1aa442c3..985bb25e5a 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -25,55 +25,41 @@ import {
OnCallsResponse,
OnCall,
} from '../components/types';
+import { PagerDutyClient } from './types';
+
+export class UnauthorizedError extends Error {
+ constructor() {
+ super();
+ }
+}
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?: ClientApiConfig) {}
+ constructor(private readonly config: ClientApiConfig) {}
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 url = `${this.config.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 url = `${this.config.api_url}/incidents?${params}`;
const { incidents } = await this.getByUrl(url);
return incidents;
}
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 url = `${this.config.api_url}/oncalls?${params}`;
const { oncalls } = await this.getByUrl(url);
return oncalls;
@@ -110,14 +96,13 @@ export class PagerDutyClientApi implements PagerDutyClient {
body,
};
- return this.request(`${this.EVENTS_API_URL}/enqueue`, options);
+ return this.request(`${this.config.events_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',
},
@@ -131,17 +116,16 @@ export class PagerDutyClientApi implements PagerDutyClient {
url: string,
options: RequestOptions,
): 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);
+ const response = await fetch(url, options);
+ if (response.status === 401) {
+ throw new UnauthorizedError();
}
+ 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;
}
}
diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts
new file mode 100644
index 0000000000..5ac7470475
--- /dev/null
+++ b/plugins/pagerduty/src/api/index.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.
+ */
+
+export {
+ PagerDutyClientApi,
+ pagerDutyApiRef,
+ UnauthorizedError,
+} from './client';
+export type { PagerDutyClient } from './types';
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
new file mode 100644
index 0000000000..cb9d7fc5bc
--- /dev/null
+++ b/plugins/pagerduty/src/api/types.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { Incident, OnCall, Service } from '../components/types';
+
+export interface PagerDutyClient {
+ /**
+ * Fetches a list of services, filtered by the provided integration key.
+ *
+ */
+ getServiceByIntegrationKey(integrationKey: string): Promise;
+
+ /**
+ * Fetches a list of incidents a provided service has.
+ *
+ */
+ getIncidentsByServiceId(serviceId: string): Promise;
+
+ /**
+ * Fetches the list of users in an escalation policy.
+ *
+ */
+ getOnCallByPolicyId(policyId: string): Promise;
+}
diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts
index b24018887a..58dc39932d 100644
--- a/plugins/pagerduty/src/index.ts
+++ b/plugins/pagerduty/src/index.ts
@@ -16,5 +16,5 @@
export { plugin } from './plugin';
export {
isPluginApplicableToEntity,
- PagerDutyServiceCard,
-} from './components/PagerDutyServiceCard';
+ PagerDutyCard,
+} from './components/PagerDutyCard';
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
index 4073551dc8..2e07b5c430 100644
--- a/plugins/pagerduty/src/plugin.ts
+++ b/plugins/pagerduty/src/plugin.ts
@@ -19,7 +19,7 @@ import {
createRouteRef,
configApiRef,
} from '@backstage/core';
-import { pagerDutyApiRef, PagerDutyClientApi } from './api/pagerDutyClient';
+import { pagerDutyApiRef, PagerDutyClientApi } from './api';
export const rootRouteRef = createRouteRef({
path: '/pagerduty',
@@ -34,7 +34,10 @@ export const plugin = createPlugin({
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
new PagerDutyClientApi({
- token: configApi.getOptionalString('pagerduty.api.token'),
+ api_url: `${configApi.getString(
+ 'backend.baseUrl',
+ )}/api/proxy/pagerduty`,
+ events_url: 'https://events.pagerduty.com/v2',
}),
}),
],
From e1c23fa4b318a482c4dff49c62a86fc1268ccb38 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 16 Nov 2020 16:36:03 +0100
Subject: [PATCH 21/77] add action prop to support buttons
---
.../IconLinkVertical/IconLinkVertical.tsx | 26 +++++++++++++++++--
plugins/catalog/src/index.ts | 1 +
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
index 580dcff0f5..63df4feaf9 100644
--- a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
+++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
@@ -23,9 +23,10 @@ export type IconLinkVerticalProps = {
href?: string;
disabled?: boolean;
label: string;
+ action?: React.ReactNode;
};
-const useIconStyles = makeStyles({
+const useIconStyles = makeStyles(theme => ({
link: {
display: 'grid',
justifyItems: 'center',
@@ -41,12 +42,16 @@ const useIconStyles = makeStyles({
fontWeight: 600,
letterSpacing: 1.2,
},
-});
+ linkStyle: {
+ color: theme.palette.secondary.main,
+ },
+}));
export function IconLinkVertical({
icon = ,
href = '#',
disabled = false,
+ action,
...props
}: IconLinkVerticalProps) {
const classes = useIconStyles();
@@ -64,6 +69,23 @@ export function IconLinkVertical({
);
}
+ if (action) {
+ return (
+
+ {icon}
+ {action}
+
+ );
+ }
+
return (
{icon}
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index 762f4924c0..101680c552 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -23,3 +23,4 @@ export { Router } from './components/Router';
export { useEntity, EntityContext } from './hooks/useEntity';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
+export { IconLinkVertical } from './components/AboutCard/IconLinkVertical';
From 00186ab37818c2efbe3f4be7e6a782bf47755996 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 16 Nov 2020 16:50:58 +0100
Subject: [PATCH 22/77] remove 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 | 169 ------------------
.../src/service/standaloneServer.ts | 62 -------
plugins/pagerduty-backend/src/setupTests.ts | 18 --
12 files changed, 431 deletions(-)
delete mode 100644 packages/backend/src/plugins/pagerduty.ts
delete mode 100644 plugins/pagerduty-backend/.eslintrc.js
delete mode 100644 plugins/pagerduty-backend/README.md
delete mode 100644 plugins/pagerduty-backend/package.json
delete mode 100644 plugins/pagerduty-backend/src/index.ts
delete mode 100644 plugins/pagerduty-backend/src/run.ts
delete mode 100644 plugins/pagerduty-backend/src/service/router.test.ts
delete mode 100644 plugins/pagerduty-backend/src/service/router.ts
delete mode 100644 plugins/pagerduty-backend/src/service/standaloneServer.ts
delete mode 100644 plugins/pagerduty-backend/src/setupTests.ts
diff --git a/packages/backend/package.json b/packages/backend/package.json
index c31629e82e..ef55b007a7 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -31,7 +31,6 @@
"@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 0ca17c7d7f..18fd44f089 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -46,7 +46,6 @@ 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();
@@ -80,7 +79,6 @@ 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));
@@ -92,7 +90,6 @@ 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
deleted file mode 100644
index 6dc3d8c9ba..0000000000
--- a/packages/backend/src/plugins/pagerduty.ts
+++ /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 { 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
deleted file mode 100644
index 16a033dbc6..0000000000
--- a/plugins/pagerduty-backend/.eslintrc.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- extends: [require.resolve('@backstage/cli/config/eslint.backend')],
-};
diff --git a/plugins/pagerduty-backend/README.md b/plugins/pagerduty-backend/README.md
deleted file mode 100644
index 099a94a85f..0000000000
--- a/plugins/pagerduty-backend/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# 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
deleted file mode 100644
index cad4ba268e..0000000000
--- a/plugins/pagerduty-backend/package.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "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
deleted file mode 100644
index 7612c392a2..0000000000
--- a/plugins/pagerduty-backend/src/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 * from './service/router';
diff --git a/plugins/pagerduty-backend/src/run.ts b/plugins/pagerduty-backend/src/run.ts
deleted file mode 100644
index b96989e4b8..0000000000
--- a/plugins/pagerduty-backend/src/run.ts
+++ /dev/null
@@ -1,33 +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 { 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
deleted file mode 100644
index 0aaeafa379..0000000000
--- a/plugins/pagerduty-backend/src/service/router.test.ts
+++ /dev/null
@@ -1,45 +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 { 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
deleted file mode 100644
index 1fbab9b551..0000000000
--- a/plugins/pagerduty-backend/src/service/router.ts
+++ /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 { 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';
-import { Response } from 'express';
-
-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
- 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);
- }
-}
-
-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: PagerDutyService[] = [];
-
- async function getData() {
- const result = await request(
- `${API_URL}/services?offset=${offset}`,
- options,
- );
-
- 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 < 2 && result.more) {
- ++offset;
- await getData();
- }
- }
-
- try {
- await getData();
- } catch (error) {
- throw new Error(error);
- }
-
- return data;
-}
-
-export interface RouterOptions {
- logger: Logger;
- 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 {
- const { logger, config } = options;
- 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;
- 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);
-
- useHotCleanup(module, () => clearInterval(intervalId));
-
- router.use(express.json());
-
- 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-backend/src/service/standaloneServer.ts b/plugins/pagerduty-backend/src/service/standaloneServer.ts
deleted file mode 100644
index 73f928bef4..0000000000
--- a/plugins/pagerduty-backend/src/service/standaloneServer.ts
+++ /dev/null
@@ -1,62 +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.
- */
-/*
- * 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
deleted file mode 100644
index a5907fd52f..0000000000
--- a/plugins/pagerduty-backend/src/setupTests.ts
+++ /dev/null
@@ -1,18 +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 {};
-global.fetch = require('node-fetch');
From e4bec4e17e484a03561e3ff81c161496bcc988b3 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 17 Nov 2020 10:01:59 +0100
Subject: [PATCH 23/77] add proxy to config
---
app-config.yaml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/app-config.yaml b/app-config.yaml
index 715d564feb..fbeba50835 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -41,6 +41,12 @@ proxy:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
+ '/pagerduty':
+ target: https://api.pagerduty.com
+ headers:
+ Authorization:
+ $env: PAGERDUTY_TOKEN
+
organization:
name: My Company
From 2e8c842ad73c5fe79def3daabc85287a41a26038 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 17 Nov 2020 11:11:35 +0100
Subject: [PATCH 24/77] update versions
---
.../catalog/src/components/AboutCard/index.ts | 1 +
plugins/catalog/src/index.ts | 3 +--
plugins/pagerduty/package.json | 16 ++++++++--------
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/plugins/catalog/src/components/AboutCard/index.ts b/plugins/catalog/src/components/AboutCard/index.ts
index 93765ff942..0491d467f8 100644
--- a/plugins/catalog/src/components/AboutCard/index.ts
+++ b/plugins/catalog/src/components/AboutCard/index.ts
@@ -15,3 +15,4 @@
*/
export { AboutCard } from './AboutCard';
+export { IconLinkVertical } from './IconLinkVertical';
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index f0e8b103e6..5949209128 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -15,9 +15,8 @@
*/
export * from '@backstage/catalog-client';
-export { AboutCard } from './components/AboutCard';
+export { AboutCard, IconLinkVertical } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
-export { IconLinkVertical } from './components/AboutCard/IconLinkVertical';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
export { EntityContext, useEntity } from './hooks/useEntity';
diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json
index 29518bd858..2fce2ebef3 100644
--- a/plugins/pagerduty/package.json
+++ b/plugins/pagerduty/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-pagerduty",
- "version": "0.1.1-alpha.25",
+ "version": "0.2.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,11 +20,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.25",
- "@backstage/theme": "^0.1.1-alpha.25",
- "@backstage/catalog-model": "^0.1.1-alpha.25",
- "@backstage/test-utils": "^0.1.1-alpha.25",
- "@backstage/plugin-catalog": "^0.1.1-alpha.25",
+ "@backstage/core": "^0.3.0",
+ "@backstage/theme": "^0.2.1",
+ "@backstage/catalog-model": "^0.2.0",
+ "@backstage/test-utils": "^0.1.2",
+ "@backstage/plugin-catalog": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -34,8 +34,8 @@
"moment": "^2.27.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.25",
- "@backstage/dev-utils": "^0.1.1-alpha.25",
+ "@backstage/cli": "^0.2.0",
+ "@backstage/dev-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
From 22b0500d76118aa07aca589a57d627f189facf7e Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Wed, 18 Nov 2020 09:51:33 +0100
Subject: [PATCH 25/77] small fixes
---
.../app/src/components/catalog/EntityPage.tsx | 3 +-
plugins/pagerduty/src/api/client.ts | 10 +--
plugins/pagerduty/src/api/types.ts | 10 +++
plugins/pagerduty/src/assets/pd.svg | 13 ----
.../components/Escalation/EscalationUser.tsx | 4 +-
.../components/Incident/IncidentListItem.tsx | 2 +-
.../TriggerButton/TriggerButton.tsx | 2 +-
.../TriggerDialog/TriggerDialog.tsx | 65 ++++++++++++-------
8 files changed, 60 insertions(+), 49 deletions(-)
delete mode 100644 plugins/pagerduty/src/assets/pd.svg
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 17231abb9a..6099b6ffe0 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -70,7 +70,8 @@ import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
-import { Router as BuildKiteRouter,
+import {
+ Router as BuildKiteRouter,
isPluginApplicableToEntity as isBuildKiteAvailable,
} from '@roadiehq/backstage-plugin-buildkite';
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
index 985bb25e5a..c9d40a1694 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -27,13 +27,9 @@ import {
} from '../components/types';
import { PagerDutyClient } from './types';
-export class UnauthorizedError extends Error {
- constructor() {
- super();
- }
-}
+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',
});
@@ -65,7 +61,7 @@ export class PagerDutyClientApi implements PagerDutyClient {
return oncalls;
}
- triggerPagerDutyAlarm(
+ triggerAlarm(
integrationKey: string,
source: string,
description: string,
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
index cb9d7fc5bc..c07834a213 100644
--- a/plugins/pagerduty/src/api/types.ts
+++ b/plugins/pagerduty/src/api/types.ts
@@ -34,4 +34,14 @@ export interface PagerDutyClient {
*
*/
getOnCallByPolicyId(policyId: string): Promise;
+
+ /**
+ * Triggers an incident to whoever is on-call.
+ */
+ triggerAlarm(
+ integrationKey: string,
+ source: string,
+ description: string,
+ userName: string,
+ ): Promise;
}
diff --git a/plugins/pagerduty/src/assets/pd.svg b/plugins/pagerduty/src/assets/pd.svg
deleted file mode 100644
index 237ffa2011..0000000000
--- a/plugins/pagerduty/src/assets/pd.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- pd_icon
- Created with Sketch.
-
-
-
-
-
-
-
-
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
index e20a5f7567..adc8d4e6d7 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
@@ -52,12 +52,12 @@ export const EscalationUser = ({ user }: { user: User }) => {
secondary={user.email}
/>
-
+
-
+
{
}
/>
-
+
{
setDescription(event.target.value);
};
- const [{ value, loading, error }, triggerAlarm] = useAsyncFn(
+ const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
async (desc: string) => {
- return await api.triggerPagerDutyAlarm(
+ return await api.triggerAlarm(
integrationKey,
window.location.toString(),
desc,
@@ -82,34 +91,41 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
}, [value, error, alertApi, onClose, userId]);
return (
-
+
- This action will send PagerDuty alarms and notifications to on-call
- people responsible for {name}.
+ 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
- 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.
-
-
+
+ {/* "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. You can find information about the owner of
+ this entity in the "About" card. 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.
-
+ possible. Your signed in user and a reference to the current page will
+ automatically be amended to the alarm so that the receiver can reach
+ out to you if necessary.
+
@@ -119,15 +135,16 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
id="trigger"
color="secondary"
disabled={!description || loading}
- onClick={() => triggerAlarm(description)}
+ variant="contained"
+ onClick={() => handleTriggerAlarm(description)}
+ endIcon={loading && }
>
- Trigger
+ Trigger Incident
-
+
Close
- {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/77] 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 0335cb5a9b..9c5c0b2485 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/77] 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 cbce0ce4a3..ccf70abb0b 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 adc8d4e6d7..642c9ecd96 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 6e22ccbbac..7a97dad281 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 6b6d466850..f24e91a9e0 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: (
+
+ Trigger Alarm
+
+ ),
};
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 a4f012ac47..0000000000
--- 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 (
- <>
-
- Trigger Alarm
-
- {showDialog && (
-
- )}
- >
- );
-};
diff --git a/plugins/pagerduty/src/components/TriggerButton/index.ts b/plugins/pagerduty/src/components/TriggerButton/index.ts
deleted file mode 100644
index 6a58a4f43f..0000000000
--- 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 4156074460..e6bb6d107d 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
-
+
Close
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index 9c9e29cb01..e968656e65 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 58dc39932d..1e8e549ac9 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/77] 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 4fa65eb999..15ff3278a0 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 fbe3783d30..6baa410021 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 0000000000..bca6f9f940
--- /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 7e26985136..440621dcf6 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/77] 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 0000000000..0952612d84
--- /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/77] 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 9c5c0b2485..da37282aa7 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 c9d40a1694..4ed994eec2 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/77] 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 e968656e65..5e5ee0a61b 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 2e07b5c430..02b11ed773 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/77] 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 0000000000..c22552b7c6
--- /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 f24e91a9e0..22a471c031 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/77] 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 da37282aa7..b3e0b9f4e6 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/77] 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 7e1dc61fa7..4a8bf2a0ab 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/77] 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 4ed994eec2..7b453b7298 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 c07834a213..17c2067818 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/77] 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 ccf70abb0b..5e67d52172 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 2d2e5b01fd..1d4ec3a1a6 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 22a471c031..333e042f82 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 d7857386de..0bfa67b49a 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/77] 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 0952612d84..2cf1ddd8e8 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/77] 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 b3e0b9f4e6..7a886627e3 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 7b453b7298..ab65514466 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 17c2067818..2231cc6991 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 642c9ecd96..3847896e89 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 7a97dad281..488631b834 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 6baa410021..3eef24cc6a 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 1d4ec3a1a6..4b229fb4bc 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 36f6e1989d..67cfc0036a 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 333e042f82..ac309e8a95 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 440621dcf6..3d2a48b419 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 e6bb6d107d..4717b8fece 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 5e5ee0a61b..a93791fe8e 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/77] 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 7a886627e3..b925e9d7a7 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 ab65514466..214e7d57a7 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 5ac7470475..90604c4012 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 2231cc6991..4b3ae74cc1 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 3eef24cc6a..3c5e30e0cc 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 bca6f9f940..26d67a55e4 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 1e8e549ac9..4ecd4edcc6 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 02b11ed773..4e8f429b05 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/77] 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 18eeb24400..e80b5df021 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 b0499c8431..f78a3474ff 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 3fdf236e8d..d70694969e 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 b925e9d7a7..1c22053b88 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 214e7d57a7..99e21ef6d3 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 4b3ae74cc1..733f171489 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 26d67a55e4..34ddb63f71 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 3d2a48b419..7ef57d67b4 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 4e8f429b05..34796f491e 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/77] 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 0000000000..ac309e8a95
--- /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: (
+
+ Trigger Alarm
+
+ ),
+ };
+
+ 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/77] 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 2fce2ebef3..64cc5a0893 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 488631b834..7a34501325 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: Wed, 2 Dec 2020 21:04:42 -0500
Subject: [PATCH 43/77] Align plugin ID
---
plugins/catalog-import/src/api/CatalogImportApi.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts
index 49012764e2..0ce569171f 100644
--- a/plugins/catalog-import/src/api/CatalogImportApi.ts
+++ b/plugins/catalog-import/src/api/CatalogImportApi.ts
@@ -18,7 +18,7 @@ import { createApiRef } from '@backstage/core';
import { PartialEntity } from '../util/types';
export const catalogImportApiRef = createApiRef({
- id: 'plugin.catalogimport.service',
+ id: 'plugin.catalog-import.service',
description: 'Used by the catalog import plugin to make requests',
});
From c02db852a2fd577d5f21458b696ea1e8ead46f87 Mon Sep 17 00:00:00 2001
From: Adam Harvey
Date: Wed, 2 Dec 2020 21:05:16 -0500
Subject: [PATCH 44/77] Fix variable typo
---
plugins/catalog-import/src/api/CatalogImportClient.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts
index 94ba260d55..aaa110329e 100644
--- a/plugins/catalog-import/src/api/CatalogImportClient.ts
+++ b/plugins/catalog-import/src/api/CatalogImportClient.ts
@@ -160,7 +160,7 @@ export class CatalogImportClient implements CatalogImportApi {
);
});
- const pullRequestRespone = await octo.pulls
+ const pullRequestResponse = await octo.pulls
.create({
owner,
repo,
@@ -178,7 +178,7 @@ export class CatalogImportClient implements CatalogImportApi {
});
return {
- link: pullRequestRespone.data.html_url,
+ link: pullRequestResponse.data.html_url,
location: `https://github.com/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`,
};
}
From 79418ddb6295a469e1bf4c1c65e5f6bad28cea0a Mon Sep 17 00:00:00 2001
From: Adam Harvey
Date: Wed, 2 Dec 2020 21:07:40 -0500
Subject: [PATCH 45/77] Add changeset
---
.changeset/grumpy-meals-wink.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/grumpy-meals-wink.md
diff --git a/.changeset/grumpy-meals-wink.md b/.changeset/grumpy-meals-wink.md
new file mode 100644
index 0000000000..8fb346b831
--- /dev/null
+++ b/.changeset/grumpy-meals-wink.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-import': patch
+---
+
+Align plugin ID and fix variable typo
From 9e11dfca4021ef93c03ea95661624ccdd0077392 Mon Sep 17 00:00:00 2001
From: keshan
Date: Wed, 2 Dec 2020 11:23:30 +0530
Subject: [PATCH 46/77] Introducing env prop to have configurable
authentication env
---
app-config.yaml | 5 +-
packages/core/config.d.ts | 7 ++
packages/core/src/api-wrappers/defaultApis.ts | 78 +++++++++++++++----
3 files changed, 71 insertions(+), 19 deletions(-)
diff --git a/app-config.yaml b/app-config.yaml
index 54d564de7d..2850145eef 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -190,6 +190,7 @@ scaffolder:
token:
$env: AZURE_TOKEN
auth:
+ environment: development
### Providing an auth.session.secret will enable session support in the auth-backend
# session:
# secret: custom session secret
@@ -203,9 +204,9 @@ auth:
github:
development:
clientId:
- $env: AUTH_GITHUB_CLIENT_ID
+ $env: 688ff0b3ac05066bbfbd
clientSecret:
- $env: AUTH_GITHUB_CLIENT_SECRET
+ $env: e054385169884b9618fd7ffa3e4d97b168708ca1
enterpriseInstanceUrl:
$env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
diff --git a/packages/core/config.d.ts b/packages/core/config.d.ts
index 014e4ac934..e13c00e561 100644
--- a/packages/core/config.d.ts
+++ b/packages/core/config.d.ts
@@ -62,4 +62,11 @@ export interface Config {
timezone: string;
}[];
};
+ auth?: {
+ /**
+ * The environment config added to be able to change the authentication environment.
+ * @visibility frontend
+ */
+ environment?: string;
+ };
}
diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts
index 1f18f54d2a..58cf50fca0 100644
--- a/packages/core/src/api-wrappers/defaultApis.ts
+++ b/packages/core/src/api-wrappers/defaultApis.ts
@@ -78,30 +78,42 @@ export const defaultApis = [
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- GoogleAuth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GoogleAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- MicrosoftAuth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ MicrosoftAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
+ environment: configApi.getString('auth.environment'),
}),
}),
createApiFactory({
@@ -109,60 +121,91 @@ export const defaultApis = [
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- OktaAuth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OktaAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- GitlabAuth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GitlabAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- Auth0Auth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ Auth0Auth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: oauth2ApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- OAuth2.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OAuth2.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: samlAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi }) => SamlAuth.create({ discoveryApi }),
+ factory: ({ discoveryApi, configApi }) =>
+ SamlAuth.create({
+ discoveryApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: oneloginAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
- OneLoginAuth.create({ discoveryApi, oauthRequestApi }),
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ OneLoginAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ environment: configApi.getString('auth.environment'),
+ }),
}),
createApiFactory({
api: oidcAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
@@ -171,6 +214,7 @@ export const defaultApis = [
title: 'Your Identity Provider',
icon: OAuth2Icon,
},
+ environment: configApi.getString('auth.environment'),
}),
}),
];
From 1f6a0a2803028f15f1cf951a9f2f48f2747f8102 Mon Sep 17 00:00:00 2001
From: keshan
Date: Wed, 2 Dec 2020 11:35:11 +0530
Subject: [PATCH 47/77] reverted back the github config value
---
app-config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app-config.yaml b/app-config.yaml
index 2850145eef..7fc041bf13 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -204,9 +204,9 @@ auth:
github:
development:
clientId:
- $env: 688ff0b3ac05066bbfbd
+ $env: AUTH_GITHUB_CLIENT_ID
clientSecret:
- $env: e054385169884b9618fd7ffa3e4d97b168708ca1
+ $env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
$env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
From ff243ce96a539500bbdfa8ed4cf73802606de8ff Mon Sep 17 00:00:00 2001
From: keshan
Date: Wed, 2 Dec 2020 11:35:49 +0530
Subject: [PATCH 48/77] changeset added with minor impact
---
.changeset/unlucky-kiwis-rescue.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/unlucky-kiwis-rescue.md
diff --git a/.changeset/unlucky-kiwis-rescue.md b/.changeset/unlucky-kiwis-rescue.md
new file mode 100644
index 0000000000..0ac7d8f299
--- /dev/null
+++ b/.changeset/unlucky-kiwis-rescue.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': minor
+---
+
+Introducing env prop to have configurable authentication env
From 6a5e3e204f0d9877a969140114458e021b7e6b0b Mon Sep 17 00:00:00 2001
From: keshan
Date: Thu, 3 Dec 2020 09:07:04 +0530
Subject: [PATCH 49/77] Updated comments and code to reflect feedbacks
---
.changeset/unlucky-kiwis-rescue.md | 11 +++++++++-
packages/core/config.d.ts | 8 +++++++-
packages/core/src/api-wrappers/defaultApis.ts | 20 +++++++++----------
3 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/.changeset/unlucky-kiwis-rescue.md b/.changeset/unlucky-kiwis-rescue.md
index 0ac7d8f299..71afaeb44a 100644
--- a/.changeset/unlucky-kiwis-rescue.md
+++ b/.changeset/unlucky-kiwis-rescue.md
@@ -2,4 +2,13 @@
'@backstage/core': minor
---
-Introducing env prop to have configurable authentication env
+Introducing a new optional property within `app-config.yaml` called `auth.environment` to have configurable environment value for `auth.providers`
+
+**Default Value:** 'development'
+
+**Optional Values:** 'production' | 'development'
+
+**Migration-steps:**
+
+- To override the default value, one could simply introduce the new property `environment` within the `auth` section of the `config.yaml`
+- re-run the build to reflect the changed configs
diff --git a/packages/core/config.d.ts b/packages/core/config.d.ts
index e13c00e561..a6f95ae71c 100644
--- a/packages/core/config.d.ts
+++ b/packages/core/config.d.ts
@@ -62,9 +62,15 @@ export interface Config {
timezone: string;
}[];
};
+
+ /**
+ * Configuration that provides information on available authentication providers configured for app
+ */
auth?: {
/**
- * The environment config added to be able to change the authentication environment.
+ * The 'environment' attribute added as an optional parameter to have configurable environment value for `auth.providers`.
+ * default value: 'development'
+ * optional values: 'development' | 'production'
* @visibility frontend
*/
environment?: string;
diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts
index 58cf50fca0..d044b50b7a 100644
--- a/packages/core/src/api-wrappers/defaultApis.ts
+++ b/packages/core/src/api-wrappers/defaultApis.ts
@@ -84,7 +84,7 @@ export const defaultApis = [
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -98,7 +98,7 @@ export const defaultApis = [
MicrosoftAuth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -113,7 +113,7 @@ export const defaultApis = [
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -127,7 +127,7 @@ export const defaultApis = [
OktaAuth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -141,7 +141,7 @@ export const defaultApis = [
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -155,7 +155,7 @@ export const defaultApis = [
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -169,7 +169,7 @@ export const defaultApis = [
OAuth2.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -181,7 +181,7 @@ export const defaultApis = [
factory: ({ discoveryApi, configApi }) =>
SamlAuth.create({
discoveryApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -195,7 +195,7 @@ export const defaultApis = [
OneLoginAuth.create({
discoveryApi,
oauthRequestApi,
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
@@ -214,7 +214,7 @@ export const defaultApis = [
title: 'Your Identity Provider',
icon: OAuth2Icon,
},
- environment: configApi.getString('auth.environment'),
+ environment: configApi.getOptionalString('auth.environment'),
}),
}),
];
From e5ff181467f391c0b33e2c5a1658a0804e9dd6b3 Mon Sep 17 00:00:00 2001
From: Adam Harvey
Date: Wed, 2 Dec 2020 22:51:24 -0500
Subject: [PATCH 50/77] Add register instructions
---
.../src/components/ImportComponentPage.tsx | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx
index c2eb935e54..8f494458fd 100644
--- a/plugins/catalog-import/src/components/ImportComponentPage.tsx
+++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx
@@ -15,7 +15,7 @@
*/
import React, { useState } from 'react';
-import { Grid } from '@material-ui/core';
+import { Grid, Typography } from '@material-ui/core';
import {
InfoCard,
Page,
@@ -57,15 +57,23 @@ export const ImportComponentPage = ({
-
+
- Start tracking your component in Backstage. TODO: Add more
- information about what this is.
+ Start tracking your component in Backstage by adding it to the
+ software catalog.
+
+ There are two ways to register an existing component. If you
+ already have a GitHub repository, enter the full URL to your
+ repo below and a new pull request with a sample metadata Entity
+ File (catalog-info.yaml) will be opened. Or, if
+ you've already created a Backstage metadata file and put it in
+ your repo, you can enter the full URL to that Entity File.
+
Date: Wed, 2 Dec 2020 22:52:16 -0500
Subject: [PATCH 51/77] Add changeset
---
.changeset/grumpy-toys-live.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/grumpy-toys-live.md
diff --git a/.changeset/grumpy-toys-live.md b/.changeset/grumpy-toys-live.md
new file mode 100644
index 0000000000..bb11001601
--- /dev/null
+++ b/.changeset/grumpy-toys-live.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-import': patch
+---
+
+Add register existing component instructions
From 2b027c913893aa256e67aa34756776018af78395 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Thu, 3 Dec 2020 14:25:58 +0100
Subject: [PATCH 52/77] 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 4a8bf2a0ab..dd267dde35 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 6c91854391c8b6d391b02338db44c14cc3f1a0ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikael=20=C3=96stberg?=
Date: Thu, 3 Dec 2020 20:41:31 +0100
Subject: [PATCH 53/77] Fix missing authentication in GitHubUrlReader
An oversight that probably stems from that most repositories for tech-docs are public anyway.
Ours are not, and are inside of a GitHub Enterprise, which is how i found this tiny bug.
I wish i knew how to mock the cross-fetch in order to inspect the Authorization header of the request to test this fix.
I can only confirm that it works on our backstage that targets GHE.
---
packages/backend-common/src/reading/GithubUrlReader.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts
index 692a3c6dd8..278fb70eb3 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.ts
@@ -196,6 +196,7 @@ export class GithubUrlReader implements UrlReader {
new URL(
`${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`,
).toString(),
+ getApiRequestOptions(this.config)
);
if (!response.ok) {
const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`;
From c950ee81f03769e3f722b1e7fb17d19f9905d27c Mon Sep 17 00:00:00 2001
From: Adam Harvey
Date: Thu, 3 Dec 2020 15:45:21 -0500
Subject: [PATCH 54/77] Move instructions card to right side
---
.../src/components/ImportComponentPage.tsx | 40 ++++++++++++++-----
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx
index 8f494458fd..339e7c8dd6 100644
--- a/plugins/catalog-import/src/components/ImportComponentPage.tsx
+++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx
@@ -63,17 +63,39 @@ export const ImportComponentPage = ({
software catalog.
-
-
-
+
+
+
- There are two ways to register an existing component. If you
- already have a GitHub repository, enter the full URL to your
- repo below and a new pull request with a sample metadata Entity
- File (catalog-info.yaml) will be opened. Or, if
- you've already created a Backstage metadata file and put it in
- your repo, you can enter the full URL to that Entity File.
+ There are two ways to register an existing component.
+
+
+ GitHub Repo
+
+ If you already have code in a GitHub repository, enter the
+ full URL to your repo and a new pull request with a sample
+ Backstage metadata Entity File (
+ catalog-info.yaml) will be opened for you.
+
+
+ GitHub Repo & Entity File
+
+ If you've already created a Backstage metadata file and put
+ it in your repo, you can enter the full URL to that Entity
+ File.
+
+
+
+
+
+
Date: Thu, 3 Dec 2020 15:45:43 -0500
Subject: [PATCH 55/77] Update doco screen cap
---
.../software-catalog/bsc-register-2.png | Bin 175353 -> 131597 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/docs/assets/software-catalog/bsc-register-2.png b/docs/assets/software-catalog/bsc-register-2.png
index de71141ba032cc58c9f1d0d43ac072512831197a..18a6c5a4f0b5241f0a785afb269e43e9d286c56e 100644
GIT binary patch
literal 131597
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