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 65ba6140b2836..487e4ad0c568a 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -21,6 +21,7 @@
"@backstage/plugin-kubernetes": "^0.1.1-alpha.25",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.25",
"@backstage/plugin-newrelic": "^0.1.1-alpha.25",
+ "@backstage/plugin-pagerduty": "^0.1.1-alpha.25",
"@backstage/plugin-register-component": "^0.1.1-alpha.25",
"@backstage/plugin-rollbar": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.25",
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index bdc38513186b7..3851556882379 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -38,3 +38,4 @@ export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
+export { plugin as Pagerduty } from '@backstage/plugin-pagerduty';
diff --git a/plugins/pagerduty/.eslintrc.js b/plugins/pagerduty/.eslintrc.js
new file mode 100644
index 0000000000000..13573efa9c466
--- /dev/null
+++ b/plugins/pagerduty/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md
new file mode 100644
index 0000000000000..0335cb5a9bbba
--- /dev/null
+++ b/plugins/pagerduty/README.md
@@ -0,0 +1,13 @@
+# pagerduty
+
+Welcome to the pagerduty plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx
new file mode 100644
index 0000000000000..264d6f801f5a2
--- /dev/null
+++ b/plugins/pagerduty/dev/index.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createDevApp } from '@backstage/dev-utils';
+import { plugin } from '../src/plugin';
+
+createDevApp().registerPlugin(plugin).render();
diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json
new file mode 100644
index 0000000000000..33054c6163aff
--- /dev/null
+++ b/plugins/pagerduty/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@backstage/plugin-pagerduty",
+ "version": "0.1.1-alpha.25",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "build": "backstage-cli plugin:build",
+ "start": "backstage-cli plugin:serve",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "diff": "backstage-cli plugin:diff",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/core": "^0.1.1-alpha.25",
+ "@backstage/theme": "^0.1.1-alpha.25",
+ "@material-ui/core": "^4.11.0",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-use": "^15.3.3"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.25",
+ "@backstage/dev-utils": "^0.1.1-alpha.25",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^12.0.0",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts
new file mode 100644
index 0000000000000..224e29389089d
--- /dev/null
+++ b/plugins/pagerduty/src/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { plugin } from './plugin';
diff --git a/plugins/pagerduty/src/plugin.test.ts b/plugins/pagerduty/src/plugin.test.ts
new file mode 100644
index 0000000000000..8d4545ac12fd4
--- /dev/null
+++ b/plugins/pagerduty/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { plugin } from './plugin';
+
+describe('pagerduty', () => {
+ it('should export plugin', () => {
+ expect(plugin).toBeDefined();
+ });
+});
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
new file mode 100644
index 0000000000000..8f8523b78bae1
--- /dev/null
+++ b/plugins/pagerduty/src/plugin.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createPlugin, createRouteRef } from '@backstage/core';
+
+export const rootRouteRef = createRouteRef({
+ path: '/pagerduty',
+ title: 'pagerduty',
+});
+
+export const plugin = createPlugin({
+ id: 'pagerduty',
+});
diff --git a/plugins/pagerduty/src/setupTests.ts b/plugins/pagerduty/src/setupTests.ts
new file mode 100644
index 0000000000000..d7857386de33a
--- /dev/null
+++ b/plugins/pagerduty/src/setupTests.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '@testing-library/jest-dom';
+
+global.fetch = require('node-fetch');
From 47e9c6d56bd8fac1cb3d06cfefeb31e5646a9235 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 20 Oct 2020 09:55:32 +0200
Subject: [PATCH 02/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 0000000000000..3dadacadfbf7e
--- /dev/null
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import {
+ InfoCard,
+ StatusError,
+ StatusWarning,
+ StatusOK,
+} from '@backstage/core';
+import { Entity } from '@backstage/catalog-model';
+import {
+ List,
+ ListSubheader,
+ ListItem,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+} from '@material-ui/core';
+import moment from 'moment';
+import Pagerduty from './pd.svg';
+import UserIcon from '@material-ui/icons/Person';
+import EmailIcon from '@material-ui/icons/Email';
+
+const useStyles = makeStyles({
+ buttonContainer: {},
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ svgButtonImage: {
+ height: '1em',
+ },
+});
+
+type IncidentListProps = {
+ incidents: any;
+};
+
+const IncidentList = ({ incidents }: IncidentListProps) => {
+ const classes = useStyles();
+ return incidents.map((incident: any) => (
+
+
+
+
+ {incident.status === 'triggered' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Created {moment(incident.createdAt).fromNow()}, assigned to{' '}
+ {(incident.assignees.length && incident.assignees[0].name) ||
+ 'nobody'}
+
+ }
+ />
+
+
+
+
+
+
+
+
+ ));
+};
+
+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 0000000000000..237ffa2011254
--- /dev/null
+++ b/plugins/pagerduty/src/components/pd.svg
@@ -0,0 +1,13 @@
+
+
+
+ pd_icon
+ Created with Sketch.
+
+
+
+
+
+
+
+
diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts
index 224e29389089d..b24018887a8d2 100644
--- a/plugins/pagerduty/src/index.ts
+++ b/plugins/pagerduty/src/index.ts
@@ -14,3 +14,7 @@
* limitations under the License.
*/
export { plugin } from './plugin';
+export {
+ isPluginApplicableToEntity,
+ PagerDutyServiceCard,
+} from './components/PagerDutyServiceCard';
From e8854140c3f8af226f3d2a1bf8acaadb562deab2 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 20 Oct 2020 09:56:55 +0200
Subject: [PATCH 03/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 f30e7ff34fc3b..51467fc2bbecc 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -66,6 +66,10 @@ import {
isPluginApplicableToEntity as isPullRequestsAvailable,
PullRequestsStatsCard,
} from '@roadiehq/backstage-plugin-github-pull-requests';
+import {
+ isPluginApplicableToEntity as isPagerDutyAvailable,
+ PagerDutyServiceCard,
+} from '@backstage/plugin-pagerduty';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
@@ -131,6 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
+ {isPagerDutyAvailable(entity) && (
+
+
+
+ )}
{isGitHubAvailable(entity) && (
<>
From 82fbe62743b45cbab7d3bac590532f1baabdbb64 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 20 Oct 2020 11:21:18 +0200
Subject: [PATCH 04/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 0000000000000..217b93a400527
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import {
+ List,
+ ListSubheader,
+ ListItem,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+} from '@material-ui/core';
+import UserIcon from '@material-ui/icons/Person';
+import EmailIcon from '@material-ui/icons/Email';
+import { StatusWarning } from '@backstage/core';
+import Pagerduty from './pd.svg';
+import { PagerDutyUserData } from './types';
+
+const useStyles = makeStyles({
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ svgButtonImage: {
+ height: '1em',
+ },
+});
+
+type EscalationUserProps = {
+ user: PagerDutyUserData;
+};
+
+const EscalationUser = ({ user }: EscalationUserProps) => {
+ const classes = useStyles();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const EscalationUsersEmptyState = () => {
+ const classes = useStyles();
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+type EscalationPolicyProps = {
+ escalation: PagerDutyUserData[];
+};
+
+export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => (
+ Escalation Policy}>
+ {escalation.length ? (
+ escalation.map((user, index) => (
+
+ ))
+ ) : (
+
+ )}
+
+);
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
new file mode 100644
index 0000000000000..6f6d194a77c8c
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import {
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+ ListSubheader,
+} from '@material-ui/core';
+import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
+import Pagerduty from './pd.svg';
+import moment from 'moment';
+
+const useStyles = makeStyles({
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ svgButtonImage: {
+ height: '1em',
+ },
+});
+
+const IncidentsEmptyState = () => {
+ const classes = useStyles();
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+type IncidentListProps = {
+ incidents: any;
+};
+
+const IncidentList = ({ incidents }: IncidentListProps) => {
+ const classes = useStyles();
+ return incidents.map((incident: any) => (
+
+
+
+
+ {incident.status === 'triggered' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Created {moment(incident.createdAt).fromNow()}, assigned to{' '}
+ {(incident.assignees.length && incident.assignees[0].name) ||
+ 'nobody'}
+
+ }
+ />
+
+
+
+
+
+
+
+
+ ));
+};
+
+type IncidentsProps = {
+ incidents: Array;
+};
+
+export const Incidents = ({ incidents }: IncidentsProps) => (
+ Incidents}>
+ {incidents.length ? (
+
+ ) : (
+
+ )}
+
+);
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 3dadacadfbf7e..cd51c4bcee994 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -14,237 +14,17 @@
* limitations under the License.
*/
import React from 'react';
-import {
- InfoCard,
- StatusError,
- StatusWarning,
- StatusOK,
-} from '@backstage/core';
+import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
-import {
- List,
- ListSubheader,
- ListItem,
- ListItemIcon,
- ListItemSecondaryAction,
- Tooltip,
- ListItemText,
- makeStyles,
- IconButton,
-} from '@material-ui/core';
-import moment from 'moment';
-import Pagerduty from './pd.svg';
-import UserIcon from '@material-ui/icons/Person';
-import EmailIcon from '@material-ui/icons/Email';
+import { Grid, Button } from '@material-ui/core';
+import { Incidents } from './Incidents';
+import { EscalationPolicy } from './Escalation';
+import { PagerDutyData } from './types';
-const useStyles = makeStyles({
- buttonContainer: {},
- denseListIcon: {
- marginRight: 0,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- },
- svgButtonImage: {
- height: '1em',
- },
-});
+const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
-type IncidentListProps = {
- incidents: any;
-};
-
-const IncidentList = ({ incidents }: IncidentListProps) => {
- const classes = useStyles();
- return incidents.map((incident: any) => (
-
-
-
-
- {incident.status === 'triggered' ? (
-
- ) : (
-
- )}
-
-
-
-
- Created {moment(incident.createdAt).fromNow()}, assigned to{' '}
- {(incident.assignees.length && incident.assignees[0].name) ||
- 'nobody'}
-
- }
- />
-
-
-
-
-
-
-
-
- ));
-};
-
-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 0000000000000..b3b94f02007ff
--- /dev/null
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -0,0 +1,27 @@
+export type PagerDutyIncident = {
+ id: string;
+ title: string;
+ status: string;
+ createdAt: string;
+ homepageUrl: string;
+ assignees: Partial[];
+};
+
+export type PagerDutyUserData = {
+ email: string;
+ name: string;
+ homepageUrl: string;
+ id: string;
+};
+
+export type PagerDutyServicesData = {
+ activeIncidents: PagerDutyIncident[];
+ escalationPolicy: any; // PagerDutyUserData[];
+ id: string;
+ name: string;
+ homepageUrl: string;
+};
+
+export type PagerDutyData = {
+ pagerDutyServices: PagerDutyServicesData[];
+};
From 4dba9ea97ae30e1e5b83ee6dcf251c141b41519c Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Wed, 21 Oct 2020 10:04:05 +0200
Subject: [PATCH 05/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 cd51c4bcee994..e60575ee9beb0 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -16,10 +16,11 @@
import React from 'react';
import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
-import { Grid, Button } from '@material-ui/core';
+import { Grid } from '@material-ui/core';
import { Incidents } from './Incidents';
import { EscalationPolicy } from './Escalation';
import { PagerDutyData } from './types';
+import { TriggerButton } from './TriggerButton';
const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -64,19 +65,25 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
],
};
- const { activeIncidents, escalationPolicy } = mockData.pagerDutyServices[0];
+ const {
+ activeIncidents,
+ escalationPolicy,
+ homepageUrl,
+ } = mockData.pagerDutyServices[0];
+
+ const link = {
+ title: 'View in PagerDuty',
+ link: homepageUrl,
+ };
return isPluginApplicableToEntity(entity) ? (
) : (
-
+
- {/* */}
-
- Trigger Alarm
-
+
);
diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx
new file mode 100644
index 0000000000000..578b86d50b282
--- /dev/null
+++ b/plugins/pagerduty/src/components/TriggerButton.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState } from 'react';
+import { Button } from '@material-ui/core';
+import { TriggerDialog } from './TriggerDialog';
+import { Entity } from '@backstage/catalog-model';
+
+type Props = {
+ entity: Entity;
+};
+
+export const TriggerButton = ({ entity }: Props) => {
+ const [showDialog, setShowDialog] = useState(false);
+
+ const handleDialog = () => {
+ setShowDialog(!showDialog);
+ };
+
+ // const onTriggerAlarm = async (description: string) => {
+ // try {
+ // //TODO: call method from pagerduty client
+ // alertApi.post({
+ // message: `Alarm successfully triggered by ${userId}`,
+ // });
+ // } catch (error) {
+ // alertApi.post({
+ // message: `Failed to trigger alarm, ${error.message}`,
+ // severity: 'error',
+ // });
+ // throw error;
+ // }
+ // };
+
+ return (
+ <>
+
+ Trigger alarm
+
+ {showDialog && (
+
+ )}
+ >
+ );
+};
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx
new file mode 100644
index 0000000000000..29378cce63fa5
--- /dev/null
+++ b/plugins/pagerduty/src/components/TriggerDialog.tsx
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ TextField,
+ makeStyles,
+ DialogActions,
+ Button,
+ DialogContent,
+ FormControl,
+ FormHelperText,
+} from '@material-ui/core';
+import { Progress, useApi, alertApiRef, identityApiRef } from '@backstage/core';
+import { useAsyncFn } from 'react-use';
+
+const useStyles = makeStyles({
+ warningText: {
+ border: '1px solid rgba(245, 155, 35, 0.5)',
+ backgroundColor: 'rgba(245, 155, 35, 0.2)',
+ padding: '0.5em 1em',
+ },
+});
+
+type Props = {
+ name: string;
+ integrationKey: string;
+ onClose: () => void;
+};
+
+export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
+ const classes = useStyles();
+ const [description, setDescription] = useState('');
+ const alertApi = useApi(alertApiRef);
+ const identityApi = useApi(identityApiRef);
+ const userId = identityApi.getUserId();
+
+ const descriptionChanged = (event: any) => {
+ setDescription(event.target.value);
+ };
+
+ const promiseFunc = () =>
+ new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject('Inside test await');
+ }, 1000);
+ });
+
+ const [{ value, loading, error }, triggerAlarm] = useAsyncFn(async () => {
+ return await promiseFunc();
+ });
+
+ useEffect(() => {
+ if (value) {
+ alertApi.post({
+ message: `Alarm successfully triggered by ${userId}`,
+ });
+ onClose();
+ }
+
+ if (error) {
+ alertApi.post({
+ message: `Failed to trigger alarm, ${error.message}`,
+ severity: 'error',
+ });
+ }
+ }, [value, error]);
+
+ return (
+
+
+ This action will send PagerDuty alarms and notifications to on-call
+ people responsible for {name}.
+
+
+
+ Note: If the issue you are seeing does not need urgent attention,
+ please get in touch with the responsible team using their preferred
+ communications channel. For most views, you can find links to support
+ and information channels by clicking the support button in the top
+ right corner of the page. If the issue is urgent, please don't
+ hesitate to trigger the alert.
+
+
+ Please describe the problem you want to report. Be as descriptive as
+ possible. Your Spotify username and a reference to the current page
+ will automatically be sent amended to the alarm so that we can debug
+ the issue and reach out to you if necessary.
+
+
+
+
+
+ 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 33054c6163aff..e5d0264be3dda 100644
--- a/plugins/pagerduty/package.json
+++ b/plugins/pagerduty/package.json
@@ -22,12 +22,14 @@
"dependencies": {
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
+ "@backstage/catalog-model": "^0.1.1-alpha.25",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^15.3.3"
+ "react-use": "^15.3.3",
+ "moment": "^2.27.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
new file mode 100644
index 0000000000000..d5db842dab999
--- /dev/null
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+type Options = {
+ method: string;
+ headers: {
+ 'Content-Type': string;
+ Accept: string;
+ };
+ body: string;
+};
+
+const request = async (
+ url: string,
+ options: Options,
+): Promise => {
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ const payload = await response.json();
+ const errors = payload.errors.map((error: string) => error).join(' ');
+ const message = `Request failed with ${response.status}, ${errors}`;
+
+ throw new Error(message);
+ }
+
+ return await response.json();
+};
+
+export function triggerPagerDutyAlarm(
+ integrationKey: string,
+ source: string,
+ description: string,
+ userName: string,
+) {
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ Accept: 'application/json, text/plain, */*',
+ },
+ body: JSON.stringify({
+ event_action: 'trigger',
+ routing_key: integrationKey,
+ client: 'Backstage',
+ client_url: source,
+ payload: {
+ summary: description,
+ source: source,
+ severity: 'error',
+ class: 'manual trigger',
+ custom_details: {
+ user: userName,
+ },
+ },
+ }),
+ };
+
+ return request('https://events.pagerduty.com/v2/enqueue', options);
+}
diff --git a/plugins/pagerduty/src/components/pd.svg b/plugins/pagerduty/src/assets/pd.svg
similarity index 100%
rename from plugins/pagerduty/src/components/pd.svg
rename to plugins/pagerduty/src/assets/pd.svg
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx
index 217b93a400527..74e253d51613e 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -29,7 +29,7 @@ import {
import UserIcon from '@material-ui/icons/Person';
import EmailIcon from '@material-ui/icons/Email';
import { StatusWarning } from '@backstage/core';
-import Pagerduty from './pd.svg';
+import Pagerduty from '../assets/pd.svg';
import { PagerDutyUserData } from './types';
const useStyles = makeStyles({
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index 6f6d194a77c8c..98a26c66dbaec 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -27,7 +27,7 @@ import {
ListSubheader,
} from '@material-ui/core';
import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
-import Pagerduty from './pd.svg';
+import Pagerduty from '../assets/pd.svg';
import moment from 'moment';
const useStyles = makeStyles({
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index e60575ee9beb0..e0564d76ce183 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -22,7 +22,7 @@ import { EscalationPolicy } from './Escalation';
import { PagerDutyData } from './types';
import { TriggerButton } from './TriggerButton';
-const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
+export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
@@ -76,7 +76,7 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
link: homepageUrl,
};
- return isPluginApplicableToEntity(entity) ? (
+ return !isPluginApplicableToEntity(entity) ? (
) : (
diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx
index 578b86d50b282..eb051e68fa0d6 100644
--- a/plugins/pagerduty/src/components/TriggerButton.tsx
+++ b/plugins/pagerduty/src/components/TriggerButton.tsx
@@ -18,6 +18,7 @@ import React, { useState } from 'react';
import { Button } from '@material-ui/core';
import { TriggerDialog } from './TriggerDialog';
import { Entity } from '@backstage/catalog-model';
+import { PAGERDUTY_INTEGRATION_KEY } from './PagerDutyServiceCard';
type Props = {
entity: Entity;
@@ -30,21 +31,6 @@ export const TriggerButton = ({ entity }: Props) => {
setShowDialog(!showDialog);
};
- // const onTriggerAlarm = async (description: string) => {
- // try {
- // //TODO: call method from pagerduty client
- // alertApi.post({
- // message: `Alarm successfully triggered by ${userId}`,
- // });
- // } catch (error) {
- // alertApi.post({
- // message: `Failed to trigger alarm, ${error.message}`,
- // severity: 'error',
- // });
- // throw error;
- // }
- // };
-
return (
<>
{
{showDialog && (
)}
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx
index 29378cce63fa5..7dbb0be5f0d6c 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 51467fc2bbecc..7629c86a1e70a 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -135,11 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
- {isPagerDutyAvailable(entity) && (
-
-
-
- )}
+ {/* {isPagerDutyAvailable(entity) && ( */}
+
+
+
+ {/* )} */}
{isGitHubAvailable(entity) && (
<>
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index d5db842dab999..fa42c82f9eb54 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -14,18 +14,22 @@
* limitations under the License.
*/
+const API_URL = 'https://api.pagerduty.com';
+const EVENTS_API_URL = 'https://events.pagerduty.com/v2';
+
type Options = {
method: string;
headers: {
'Content-Type': string;
Accept: string;
+ Authorization?: string;
};
- body: string;
+ body?: string;
};
const request = async (
url: string,
- options: Options,
+ options: any, //Options,
): Promise => {
const response = await fetch(url, options);
@@ -40,6 +44,25 @@ const request = async (
return await response.json();
};
+export const getServices = async (token: string, integrationKey: string) => {
+ const options = {
+ method: 'GET',
+ headers: {
+ Authorization: `Token token=${token}`,
+ Accept: 'application/vnd.pagerduty+json;version=2',
+ 'Content-Type': 'application/json',
+ },
+ // query: {
+ // query: encodeURIComponent(`key:${integrationKey}`),
+ // },
+ };
+
+ return request(
+ `${API_URL}/services/?query=key%253A238b701cc9d048f0bdd828355178eafe`,
+ options,
+ );
+};
+
export function triggerPagerDutyAlarm(
integrationKey: string,
source: string,
@@ -69,5 +92,5 @@ export function triggerPagerDutyAlarm(
}),
};
- return request('https://events.pagerduty.com/v2/enqueue', options);
+ return request(`${EVENTS_API_URL}/enqueue`, options);
}
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index e0564d76ce183..917ae27090b03 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -14,13 +14,20 @@
* limitations under the License.
*/
import React from 'react';
-import { InfoCard, MissingAnnotationEmptyState } from '@backstage/core';
+import {
+ InfoCard,
+ MissingAnnotationEmptyState,
+ useApi,
+ configApiRef,
+} from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
import { Incidents } from './Incidents';
import { EscalationPolicy } from './Escalation';
import { PagerDutyData } from './types';
import { TriggerButton } from './TriggerButton';
+import { useAsync } from 'react-use';
+import { getServices } from '../api/pagerDutyClient';
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -32,6 +39,20 @@ type Props = {
};
export const PagerDutyServiceCard = ({ entity }: Props) => {
+ const configApi = useApi(configApiRef);
+ const pagerDutyToken =
+ configApi.getOptionalString('pagerduty.api_token') ?? undefined;
+
+ console.log({ pagerDutyToken });
+ const { value, loading, error } = useAsync(async () => {
+ return await getServices(
+ pagerDutyToken!,
+ entity.metadata.annotations![PAGERDUTY_INTEGRATION_KEY],
+ );
+ });
+ if (value) console.log(value);
+ if (error) throw new Error(`Error in getting services, ${error}`);
+
// TODO: fetch this data
const mockData: PagerDutyData = {
pagerDutyServices: [
@@ -85,6 +106,7 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
+ {/* todo show something when we dont have token */}
);
};
From 0609e726936e457c0407fa83ba8783409275f864 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Thu, 22 Oct 2020 15:12:38 +0200
Subject: [PATCH 08/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 ef55b007a7b45..c31629e82e0e4 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -31,6 +31,7 @@
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25",
+ "@backstage/plugin-pagerduty-backend": "^0.1.1-alpha.25",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"azure-devops-node-api": "^10.1.1",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 18fd44f0891b3..0ca17c7d7fb01 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -46,6 +46,7 @@ import techdocs from './plugins/techdocs';
import graphql from './plugins/graphql';
import app from './plugins/app';
import { PluginEnvironment } from './types';
+import pagerduty from './plugins/pagerduty';
function makeCreateEnv(config: ConfigReader) {
const root = getRootLogger();
@@ -79,6 +80,7 @@ async function main() {
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
+ const pagerdutyEnv = useHotMemoize(module, () => createEnv('pagerduty'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -90,6 +92,7 @@ async function main() {
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/graphql', await graphql(graphqlEnv));
+ apiRouter.use('/pagerduty', await pagerduty(pagerdutyEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
diff --git a/packages/backend/src/plugins/pagerduty.ts b/packages/backend/src/plugins/pagerduty.ts
new file mode 100644
index 0000000000000..6dc3d8c9ba1cc
--- /dev/null
+++ b/packages/backend/src/plugins/pagerduty.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createRouter } from '@backstage/plugin-pagerduty-backend';
+import { PluginEnvironment } from '../types';
+
+export default async function createPlugin({
+ logger,
+ config,
+}: PluginEnvironment) {
+ return await createRouter({
+ logger,
+ config,
+ });
+}
diff --git a/plugins/pagerduty-backend/.eslintrc.js b/plugins/pagerduty-backend/.eslintrc.js
new file mode 100644
index 0000000000000..16a033dbc6027
--- /dev/null
+++ b/plugins/pagerduty-backend/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint.backend')],
+};
diff --git a/plugins/pagerduty-backend/README.md b/plugins/pagerduty-backend/README.md
new file mode 100644
index 0000000000000..099a94a85f500
--- /dev/null
+++ b/plugins/pagerduty-backend/README.md
@@ -0,0 +1,13 @@
+# pagerduty
+
+Welcome to the pagerduty backend plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
diff --git a/plugins/pagerduty-backend/package.json b/plugins/pagerduty-backend/package.json
new file mode 100644
index 0000000000000..cad4ba268ea59
--- /dev/null
+++ b/plugins/pagerduty-backend/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@backstage/plugin-pagerduty-backend",
+ "version": "0.1.1-alpha.25",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "start": "backstage-cli backend:dev",
+ "build": "backstage-cli backend:build",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "^0.1.1-alpha.25",
+ "@backstage/config": "^0.1.1-alpha.25",
+ "@types/express": "^4.17.6",
+ "express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
+ "winston": "^3.2.1",
+ "node-fetch": "^2.6.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.25",
+ "@types/supertest": "^2.0.8",
+ "supertest": "^4.0.2",
+ "msw": "^0.20.5"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/pagerduty-backend/src/index.ts b/plugins/pagerduty-backend/src/index.ts
new file mode 100644
index 0000000000000..7612c392a2c33
--- /dev/null
+++ b/plugins/pagerduty-backend/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './service/router';
diff --git a/plugins/pagerduty-backend/src/run.ts b/plugins/pagerduty-backend/src/run.ts
new file mode 100644
index 0000000000000..b96989e4b85ab
--- /dev/null
+++ b/plugins/pagerduty-backend/src/run.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getRootLogger } from '@backstage/backend-common';
+import yn from 'yn';
+import { startStandaloneServer } from './service/standaloneServer';
+
+const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
+const logger = getRootLogger();
+
+startStandaloneServer({ port, enableCors, logger }).catch(err => {
+ logger.error(err);
+ process.exit(1);
+});
+
+process.on('SIGINT', () => {
+ logger.info('CTRL+C pressed; exiting.');
+ process.exit(0);
+});
diff --git a/plugins/pagerduty-backend/src/service/router.test.ts b/plugins/pagerduty-backend/src/service/router.test.ts
new file mode 100644
index 0000000000000..0aaeafa379667
--- /dev/null
+++ b/plugins/pagerduty-backend/src/service/router.test.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getVoidLogger } from '@backstage/backend-common';
+import express from 'express';
+import request from 'supertest';
+
+import { createRouter } from './router';
+
+describe('createRouter', () => {
+ let app: express.Express;
+
+ beforeAll(async () => {
+ const router = await createRouter({
+ logger: getVoidLogger(),
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /health', () => {
+ it('returns ok', async () => {
+ const response = await request(app).get('/health');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ status: 'ok' });
+ });
+ });
+});
diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts
new file mode 100644
index 0000000000000..eb7cc734c2536
--- /dev/null
+++ b/plugins/pagerduty-backend/src/service/router.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { errorHandler } from '@backstage/backend-common';
+import { Config } from '@backstage/config';
+import express from 'express';
+import Router from 'express-promise-router';
+import { Logger } from 'winston';
+
+export interface RouterOptions {
+ logger: Logger;
+ config: Config;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger } = options;
+
+ const router = Router();
+ router.use(express.json());
+
+ router.get('/health', (_, response) => {
+ logger.info('PONG!');
+ response.send({ status: 'ok' });
+ });
+ router.use(errorHandler());
+ return router;
+}
diff --git a/plugins/pagerduty-backend/src/service/standaloneServer.ts b/plugins/pagerduty-backend/src/service/standaloneServer.ts
new file mode 100644
index 0000000000000..73f928bef4dee
--- /dev/null
+++ b/plugins/pagerduty-backend/src/service/standaloneServer.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createServiceBuilder } from '@backstage/backend-common';
+import { Server } from 'http';
+import { Logger } from 'winston';
+import { createRouter } from './router';
+
+export interface ServerOptions {
+ port: number;
+ enableCors: boolean;
+ logger: Logger;
+}
+
+export async function startStandaloneServer(
+ options: ServerOptions,
+): Promise {
+ const logger = options.logger.child({ service: 'pagerduty-backend' });
+ logger.debug('Starting application server...');
+ const router = await createRouter({
+ logger,
+ });
+
+ const service = createServiceBuilder(module)
+ .enableCors({ origin: 'http://localhost:3000' })
+ .addRouter('/pagerduty', router);
+
+ return await service.start().catch(err => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/plugins/pagerduty-backend/src/setupTests.ts b/plugins/pagerduty-backend/src/setupTests.ts
new file mode 100644
index 0000000000000..a5907fd52ff07
--- /dev/null
+++ b/plugins/pagerduty-backend/src/setupTests.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
+global.fetch = require('node-fetch');
From 6d1f4a996d25f93a6b5a0c5f8f605f58048d3e2f Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 23 Oct 2020 13:14:08 +0200
Subject: [PATCH 09/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 eb7cc734c2536..192a22019a783 100644
--- a/plugins/pagerduty-backend/src/service/router.ts
+++ b/plugins/pagerduty-backend/src/service/router.ts
@@ -14,11 +14,79 @@
* limitations under the License.
*/
-import { errorHandler } from '@backstage/backend-common';
+import { errorHandler, useHotCleanup } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
+import fetch from 'node-fetch';
+
+const router = Router();
+const API_URL = 'https://api.pagerduty.com';
+const EVENTS_API_URL = 'https://events.pagerduty.com/v2';
+
+type Options = {
+ method: string;
+ headers: {
+ 'Content-Type': string;
+ Accept: string;
+ Authorization?: string;
+ };
+ body?: string;
+};
+
+async function request(
+ url: string,
+ options: any, // Options,
+): Promise {
+ // TODO: handle errors better
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ const payload = await response.json();
+ const errors = payload.errors.map((error: string) => error).join(' ');
+ const message = `Request failed with ${response.status}, ${errors}`;
+
+ throw new Error(message);
+ }
+
+ return await response.json();
+}
+
+async function services(token?: string) {
+ // TODO: handle missing token differently
+ if (!token) {
+ return 'Missing Token';
+ }
+ const options = {
+ method: 'GET',
+ headers: {
+ Authorization: `Token token=${token}`,
+ Accept: 'application/vnd.pagerduty+json;version=2',
+ 'Content-Type': 'application/json',
+ },
+ };
+ let offset = 0;
+ // TODO: Create type for this data
+ const data: any = [];
+
+ async function fetchData() {
+ const result: any = await request(
+ `${API_URL}/services?offset=${offset}`,
+ options,
+ );
+ data.push(...result.services);
+
+ // TODO: remove offset when we are done
+ if (offset < 4 && result.more) {
+ ++offset;
+ await fetchData();
+ }
+ }
+
+ await fetchData();
+ return data;
+}
export interface RouterOptions {
logger: Logger;
@@ -28,15 +96,36 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise {
- const { logger } = options;
+ const { logger, config } = options;
+ const token = config.getOptionalString('pagerduty.api_token') ?? undefined;
+
+ let cachedData: any = [];
+
+ let cancel = false;
+ const intervalId = setInterval(async () => {
+ if (!cancel) {
+ cancel = true;
+ const result = await services(token);
+ cachedData = result;
+ cancel = false;
+ }
+ }, 10000);
+
+ useHotCleanup(module, () => clearInterval(intervalId));
- const router = Router();
router.use(express.json());
+ router.get('/services', async (_, response) => {
+ logger.info('pinged');
+
+ response.send({ services: cachedData, size: cachedData.length });
+ });
+
router.get('/health', (_, response) => {
logger.info('PONG!');
response.send({ status: 'ok' });
});
+
router.use(errorHandler());
return router;
}
From c9c212e064a365f2cffcfc7eb6a10eef5699a0ae Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 23 Oct 2020 14:59:36 +0200
Subject: [PATCH 10/77] added error handling
---
.../pagerduty-backend/src/service/router.ts | 100 ++++++++++++------
plugins/pagerduty/src/api/pagerDutyClient.ts | 20 ----
.../src/components/PagerDutyServiceCard.tsx | 10 +-
3 files changed, 72 insertions(+), 58 deletions(-)
diff --git a/plugins/pagerduty-backend/src/service/router.ts b/plugins/pagerduty-backend/src/service/router.ts
index 192a22019a783..1fbab9b55169f 100644
--- a/plugins/pagerduty-backend/src/service/router.ts
+++ b/plugins/pagerduty-backend/src/service/router.ts
@@ -20,6 +20,7 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import fetch from 'node-fetch';
+import { Response } from 'express';
const router = Router();
const API_URL = 'https://api.pagerduty.com';
@@ -38,19 +39,21 @@ type Options = {
async function request(
url: string,
options: any, // Options,
-): Promise {
+): Promise {
// TODO: handle errors better
- const response = await fetch(url, options);
-
- if (!response.ok) {
- const payload = await response.json();
- const errors = payload.errors.map((error: string) => error).join(' ');
- const message = `Request failed with ${response.status}, ${errors}`;
-
- throw new Error(message);
+ try {
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ const payload = await response.json();
+ const errors = payload.errors.map((error: string) => error).join(' ');
+ const message = `Request failed with ${response.status}, ${errors}`;
+ throw new Error(message);
+ }
+ return await response.json();
+ } catch (error) {
+ throw new Error(error);
}
-
- return await response.json();
}
async function services(token?: string) {
@@ -58,6 +61,7 @@ async function services(token?: string) {
if (!token) {
return 'Missing Token';
}
+
const options = {
method: 'GET',
headers: {
@@ -66,25 +70,38 @@ async function services(token?: string) {
'Content-Type': 'application/json',
},
};
+
let offset = 0;
// TODO: Create type for this data
- const data: any = [];
+ const data: PagerDutyService[] = [];
- async function fetchData() {
- const result: any = await request(
+ async function getData() {
+ const result = await request(
`${API_URL}/services?offset=${offset}`,
options,
);
- data.push(...result.services);
+
+ data.push(
+ ...result.services.map(service => ({
+ id: service.id,
+ name: service.name,
+ homepageUrl: service.html_url,
+ })),
+ );
// TODO: remove offset when we are done
- if (offset < 4 && result.more) {
+ if (offset < 2 && result.more) {
++offset;
- await fetchData();
+ await getData();
}
}
- await fetchData();
+ try {
+ await getData();
+ } catch (error) {
+ throw new Error(error);
+ }
+
return data;
}
@@ -93,6 +110,19 @@ export interface RouterOptions {
config: Config;
}
+type PagerDutyService = {
+ // activeIncidents: any[];
+ // escalationPolicy: any[];
+ id: string;
+ name: string;
+ homepageUrl: string;
+};
+
+type PagerDutyResponse = {
+ services: any[];
+ more: boolean;
+};
+
export async function createRouter(
options: RouterOptions,
): Promise {
@@ -100,14 +130,21 @@ export async function createRouter(
const token = config.getOptionalString('pagerduty.api_token') ?? undefined;
let cachedData: any = [];
+ let errorMessage: string;
let cancel = false;
const intervalId = setInterval(async () => {
if (!cancel) {
cancel = true;
- const result = await services(token);
- cachedData = result;
- cancel = false;
+ try {
+ logger.info('Fetching data from PagerDuty API');
+ cachedData = await services(token);
+ } catch (error) {
+ logger.error(`Failed to fetch data, ${error.message}`);
+ errorMessage = error.message;
+ } finally {
+ cancel = false;
+ }
}
}, 10000);
@@ -115,17 +152,18 @@ export async function createRouter(
router.use(express.json());
- router.get('/services', async (_, response) => {
- logger.info('pinged');
-
- response.send({ services: cachedData, size: cachedData.length });
- });
-
- router.get('/health', (_, response) => {
- logger.info('PONG!');
- response.send({ status: 'ok' });
- });
+ router.get(
+ '/services',
+ async (_, response: Response) => {
+ if (errorMessage) {
+ response.status(500).send({ error: errorMessage });
+ } else {
+ response.send({ services: cachedData });
+ }
+ },
+ );
router.use(errorHandler());
+
return router;
}
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index fa42c82f9eb54..dc2176680df65 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-const API_URL = 'https://api.pagerduty.com';
const EVENTS_API_URL = 'https://events.pagerduty.com/v2';
type Options = {
@@ -44,25 +43,6 @@ const request = async (
return await response.json();
};
-export const getServices = async (token: string, integrationKey: string) => {
- const options = {
- method: 'GET',
- headers: {
- Authorization: `Token token=${token}`,
- Accept: 'application/vnd.pagerduty+json;version=2',
- 'Content-Type': 'application/json',
- },
- // query: {
- // query: encodeURIComponent(`key:${integrationKey}`),
- // },
- };
-
- return request(
- `${API_URL}/services/?query=key%253A238b701cc9d048f0bdd828355178eafe`,
- options,
- );
-};
-
export function triggerPagerDutyAlarm(
integrationKey: string,
source: string,
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 917ae27090b03..b7fde8eae3445 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -27,7 +27,6 @@ import { EscalationPolicy } from './Escalation';
import { PagerDutyData } from './types';
import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
-import { getServices } from '../api/pagerDutyClient';
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -40,15 +39,12 @@ type Props = {
export const PagerDutyServiceCard = ({ entity }: Props) => {
const configApi = useApi(configApiRef);
- const pagerDutyToken =
- configApi.getOptionalString('pagerduty.api_token') ?? undefined;
- console.log({ pagerDutyToken });
const { value, loading, error } = useAsync(async () => {
- return await getServices(
- pagerDutyToken!,
- entity.metadata.annotations![PAGERDUTY_INTEGRATION_KEY],
+ const response = await fetch(
+ `${configApi.getString('backend.baseUrl')}/api/pagerduty/services`,
);
+ return await response.json();
});
if (value) console.log(value);
if (error) throw new Error(`Error in getting services, ${error}`);
From 361049d9c2c7e8209b383db73c28bf6caee6b79a Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 3 Nov 2020 11:26:23 +0100
Subject: [PATCH 11/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 | 99 +++++++++----------
5 files changed, 102 insertions(+), 60 deletions(-)
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 7629c86a1e70a..ddc8ed4b1302c 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -135,11 +135,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
- {/* {isPagerDutyAvailable(entity) && ( */}
-
-
-
- {/* )} */}
+ {isPagerDutyAvailable(entity) && (
+
+
+
+ )}
{isGitHubAvailable(entity) && (
<>
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index dc2176680df65..dbdc6622f4d76 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+const API_URL = 'https://api.pagerduty.com';
const EVENTS_API_URL = 'https://events.pagerduty.com/v2';
type Options = {
@@ -43,6 +44,49 @@ const request = async (
return await response.json();
};
+const getByUrl = async (url: string, token: string) => {
+ const options = {
+ method: 'GET',
+ headers: {
+ Authorization: `Token token=${token}`,
+ Accept: 'application/vnd.pagerduty+json;version=2',
+ 'Content-Type': 'application/json',
+ },
+ };
+ return await request(url, options);
+};
+
+export const getServiceByIntegrationKey = async (
+ integrationKey: string,
+ token: string,
+) => {
+ const response = await getByUrl(
+ `${API_URL}/services?include[]=integrations&include[]=escalation_policies&query=${integrationKey}`,
+ token,
+ );
+ if (response.services.length > 1) {
+ throw new Error('More than one service in response');
+ }
+ return response.services[0];
+};
+
+export const getIncidentsByServiceId = async (
+ serviceId: string,
+ token: string,
+) => {
+ return await getByUrl(
+ `${API_URL}/incidents?service_ids[]=${serviceId}`,
+ token,
+ );
+};
+
+export const getOncallByPolicyId = async (policyId: string, token: string) => {
+ return await getByUrl(
+ `${API_URL}/oncalls?include[]=users&escalation_policy_ids[]=${policyId}`,
+ token,
+ );
+};
+
export function triggerPagerDutyAlarm(
integrationKey: string,
source: string,
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx
index 74e253d51613e..69a4543c4bbaf 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -102,8 +102,8 @@ type EscalationPolicyProps = {
export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => (
Escalation Policy}>
{escalation.length ? (
- escalation.map((user, index) => (
-
+ escalation.map((item, index) => (
+
))
) : (
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index 98a26c66dbaec..d2f6fc7128b7c 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -80,8 +80,9 @@ const IncidentList = ({ incidents }: IncidentListProps) => {
primary={incident.title}
secondary={
- Created {moment(incident.createdAt).fromNow()}, assigned to{' '}
- {(incident.assignees.length && incident.assignees[0].name) ||
+ Created {moment(incident.created_at).fromNow()}, assigned to{' '}
+ {(incident?.assignments[0]?.assignee?.summary &&
+ incident.assignments[0].assignee.summary) ||
'nobody'}
}
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index b7fde8eae3445..e17e35a99660a 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -14,19 +14,18 @@
* limitations under the License.
*/
import React from 'react';
-import {
- InfoCard,
- MissingAnnotationEmptyState,
- useApi,
- configApiRef,
-} from '@backstage/core';
+import { InfoCard, useApi, configApiRef } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
-import { Grid } from '@material-ui/core';
+import { Grid, LinearProgress } from '@material-ui/core';
import { Incidents } from './Incidents';
import { EscalationPolicy } from './Escalation';
-import { PagerDutyData } from './types';
import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
+import {
+ getServiceByIntegrationKey,
+ getIncidentsByServiceId,
+ getOncallByPolicyId,
+} from '../api/pagerDutyClient';
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -40,65 +39,63 @@ type Props = {
export const PagerDutyServiceCard = ({ entity }: Props) => {
const configApi = useApi(configApiRef);
+ // TODO: handle missing token
+ const token = configApi.getOptionalString('pagerduty.api.token') ?? undefined;
+ console.log({ token });
+
const { value, loading, error } = useAsync(async () => {
- const response = await fetch(
- `${configApi.getString('backend.baseUrl')}/api/pagerduty/services`,
+ const integrationKey = entity.metadata.annotations![
+ PAGERDUTY_INTEGRATION_KEY
+ ];
+
+ const service = await getServiceByIntegrationKey(integrationKey, token);
+ const incidents = await getIncidentsByServiceId(
+ (service as any).id /*// TODO: fix type */,
+ token,
);
- return await response.json();
+ const oncalls = await getOncallByPolicyId(
+ (service as any).escalation_policy.id, // TODO: fix type
+ token,
+ );
+
+ return {
+ pagerDutyServices: [
+ {
+ activeIncidents: incidents,
+ escalationPolicy: oncalls,
+ id: service.id,
+ name: service.name,
+ homepageUrl: service.html_url,
+ },
+ ],
+ };
});
- if (value) console.log(value);
- if (error) throw new Error(`Error in getting services, ${error}`);
- // TODO: fetch this data
- const mockData: PagerDutyData = {
- pagerDutyServices: [
- {
- activeIncidents: [
- {
- id: 'id',
- title: 'something happened',
- status: 'triggered',
- createdAt: '2020:01:01',
- homepageUrl: 'url',
- assignees: [
- {
- name: 'name',
- },
- ],
- },
- ],
- escalationPolicy: [
- {
- email: 'user@example.com',
- name: 'Name',
- homepageUrl: 'https://spotify.pagerduty.com/users/FOO',
- id: 'user id',
- },
- ],
- id: 'id',
- name: 'name',
- homepageUrl: 'https://spotify.pagety.com/service-directory/BAR',
- },
- ],
- };
+ if (error) {
+ // TODO: use the errorApi
+ console.log(error);
+ throw new Error(`Error in getting data: ${error.message}`);
+ }
+
+ if (loading) {
+ return ;
+ }
const {
activeIncidents,
escalationPolicy,
homepageUrl,
- } = mockData.pagerDutyServices[0];
+ } = value!.pagerDutyServices[0]!;
const link = {
title: 'View in PagerDuty',
link: homepageUrl,
};
- return !isPluginApplicableToEntity(entity) ? (
-
- ) : (
+ return (
-
-
+
+
From 122410f1ab388d0bd48480d9d486c2b4ecd89212 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Thu, 5 Nov 2020 10:49:11 +0100
Subject: [PATCH 12/77] fix typings
---
.../pagerduty/src/components/Escalation.tsx | 12 +--
.../pagerduty/src/components/Incidents.tsx | 15 ++--
plugins/pagerduty/src/components/types.tsx | 84 ++++++++++++++++---
3 files changed, 84 insertions(+), 27 deletions(-)
diff --git a/plugins/pagerduty/src/components/Escalation.tsx b/plugins/pagerduty/src/components/Escalation.tsx
index 69a4543c4bbaf..19c6a15096d9c 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -30,7 +30,7 @@ import UserIcon from '@material-ui/icons/Person';
import EmailIcon from '@material-ui/icons/Email';
import { StatusWarning } from '@backstage/core';
import Pagerduty from '../assets/pd.svg';
-import { PagerDutyUserData } from './types';
+import { Oncall, PagerDutyEscalationPolicy } from './types';
const useStyles = makeStyles({
denseListIcon: {
@@ -45,11 +45,7 @@ const useStyles = makeStyles({
},
});
-type EscalationUserProps = {
- user: PagerDutyUserData;
-};
-
-const EscalationUser = ({ user }: EscalationUserProps) => {
+const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => {
const classes = useStyles();
return (
@@ -65,7 +61,7 @@ const EscalationUser = ({ user }: EscalationUserProps) => {
@@ -96,7 +92,7 @@ const EscalationUsersEmptyState = () => {
};
type EscalationPolicyProps = {
- escalation: PagerDutyUserData[];
+ escalation: Oncall[];
};
export const EscalationPolicy = ({ escalation }: EscalationPolicyProps) => (
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index d2f6fc7128b7c..0117281a0ed20 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -29,6 +29,7 @@ import {
import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
import Pagerduty from '../assets/pd.svg';
import moment from 'moment';
+import { Incident } from '../components/types';
const useStyles = makeStyles({
denseListIcon: {
@@ -57,13 +58,13 @@ const IncidentsEmptyState = () => {
);
};
-type IncidentListProps = {
- incidents: any;
+type IncidentListItemProps = {
+ incident: Incident;
};
-const IncidentList = ({ incidents }: IncidentListProps) => {
+const IncidentListItem = ({ incident }: IncidentListItemProps) => {
const classes = useStyles();
- return incidents.map((incident: any) => (
+ return (
@@ -103,17 +104,17 @@ const IncidentList = ({ incidents }: IncidentListProps) => {
- ));
+ );
};
type IncidentsProps = {
- incidents: Array;
+ incidents: Incident[];
};
export const Incidents = ({ incidents }: IncidentsProps) => (
Incidents}>
{incidents.length ? (
-
+ incidents.map(incident => )
) : (
)}
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index b3b94f02007ff..377ae6d0eaded 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -1,27 +1,87 @@
-export type PagerDutyIncident = {
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export type Incident = {
id: string;
title: string;
status: string;
- createdAt: string;
homepageUrl: string;
- assignees: Partial[];
+ assignments: [
+ {
+ assignee: User;
+ },
+ ];
+ serviceId: string;
+ created_at: string;
};
-export type PagerDutyUserData = {
- email: string;
- name: string;
- homepageUrl: string;
+export type Service = {
id: string;
+ name: string;
+ html_url: string;
+ integrationKey: string;
+ escalation_policy: PagerDutyEscalationPolicy;
+};
+
+export type ResponseService = {
+ services: Service[];
+ more: boolean;
};
-export type PagerDutyServicesData = {
- activeIncidents: PagerDutyIncident[];
- escalationPolicy: any; // PagerDutyUserData[];
+export type ResponseOncall = {
id: string;
name: string;
+ email: string;
homepageUrl: string;
};
-export type PagerDutyData = {
- pagerDutyServices: PagerDutyServicesData[];
+export type Options = {
+ method: string;
+ headers: HeadersInit;
+ body?: BodyInit;
+};
+
+export type PagerDutyClientConfig = {
+ token?: string;
+};
+
+export type ServicesResponse = {
+ services: Service[];
+};
+
+export type IncidentResponse = {
+ incidents: Incident[];
+};
+
+export type OncallsResponse = {
+ oncalls: Oncall[];
+};
+
+export type PagerDutyEscalationPolicy = {
+ user: User;
+};
+
+export type User = {
+ id: string;
+ summary: string;
+ email: string;
+ html_url: string;
+ name: string;
+};
+
+export type Oncall = {
+ escalation_policy: PagerDutyEscalationPolicy;
+ user: User;
};
From f7614ee6fce099c4e50403f979fdcbf38956193a Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Thu, 5 Nov 2020 11:17:19 +0100
Subject: [PATCH 13/77] add utility api and use it in pluign
---
plugins/pagerduty/src/api/pagerDutyClient.ts | 196 ++++++++++--------
.../pagerduty/src/components/Incidents.tsx | 4 +-
.../src/components/PagerDutyServiceCard.tsx | 66 +++---
.../src/components/TriggerDialog.tsx | 5 +-
plugins/pagerduty/src/plugin.ts | 18 +-
5 files changed, 158 insertions(+), 131 deletions(-)
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index dbdc6622f4d76..09abaf856c68b 100644
--- a/plugins/pagerduty/src/api/pagerDutyClient.ts
+++ b/plugins/pagerduty/src/api/pagerDutyClient.ts
@@ -14,92 +14,78 @@
* limitations under the License.
*/
-const API_URL = 'https://api.pagerduty.com';
-const EVENTS_API_URL = 'https://events.pagerduty.com/v2';
-
-type Options = {
- method: string;
- headers: {
- 'Content-Type': string;
- Accept: string;
- Authorization?: string;
- };
- body?: string;
-};
-
-const request = async (
- url: string,
- options: any, //Options,
-): Promise => {
- const response = await fetch(url, options);
-
- if (!response.ok) {
- const payload = await response.json();
- const errors = payload.errors.map((error: string) => error).join(' ');
- const message = `Request failed with ${response.status}, ${errors}`;
-
- throw new Error(message);
+import { createApiRef } from '@backstage/core';
+import {
+ Service,
+ Incident,
+ Options,
+ PagerDutyClientConfig,
+ ServicesResponse,
+ IncidentResponse,
+ OncallsResponse,
+ Oncall,
+} from '../components/types';
+
+export const pagerDutyApiRef = createApiRef({
+ id: 'plugin.pagerduty.api',
+ description: 'Used to fetch data from PagerDuty API',
+});
+
+interface PagerDutyClient {
+ getServiceByIntegrationKey(integrationKey: string): Promise;
+ getIncidentsByServiceId(serviceId: string): Promise;
+ getOncallByPolicyId(policyId: string): Promise;
+}
+
+export class PagerDutyClientApi implements PagerDutyClient {
+ private API_URL = 'https://api.pagerduty.com';
+ private EVENTS_API_URL = 'https://events.pagerduty.com/v2';
+
+ constructor(private readonly config?: PagerDutyClientConfig) {}
+
+ async getServiceByIntegrationKey(integrationKey: string): Promise {
+ if (!this.config?.token) {
+ throw new Error('Missing token');
+ }
+
+ const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`;
+ const url = `${this.API_URL}/services?${params}`;
+ const { services } = await this.getByUrl(url);
+
+ return services;
+ }
+
+ async getIncidentsByServiceId(serviceId: string): Promise {
+ if (!this.config?.token) {
+ throw new Error('Missing token');
+ }
+
+ const params = `service_ids[]=${serviceId}`;
+ const url = `${this.API_URL}/incidents?${params}`;
+ const { incidents } = await this.getByUrl(url);
+
+ return incidents;
}
- return await response.json();
-};
-
-const getByUrl = async (url: string, token: string) => {
- const options = {
- method: 'GET',
- headers: {
- Authorization: `Token token=${token}`,
- Accept: 'application/vnd.pagerduty+json;version=2',
- 'Content-Type': 'application/json',
- },
- };
- return await request(url, options);
-};
-
-export const getServiceByIntegrationKey = async (
- integrationKey: string,
- token: string,
-) => {
- const response = await getByUrl(
- `${API_URL}/services?include[]=integrations&include[]=escalation_policies&query=${integrationKey}`,
- token,
- );
- if (response.services.length > 1) {
- throw new Error('More than one service in response');
+ async getOncallByPolicyId(policyId: string): Promise {
+ if (!this.config?.token) {
+ throw new Error('Missing token');
+ }
+
+ const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
+ const url = `${this.API_URL}/oncalls?${params}`;
+ const { oncalls } = await this.getByUrl(url);
+
+ return oncalls;
}
- return response.services[0];
-};
-
-export const getIncidentsByServiceId = async (
- serviceId: string,
- token: string,
-) => {
- return await getByUrl(
- `${API_URL}/incidents?service_ids[]=${serviceId}`,
- token,
- );
-};
-
-export const getOncallByPolicyId = async (policyId: string, token: string) => {
- return await getByUrl(
- `${API_URL}/oncalls?include[]=users&escalation_policy_ids[]=${policyId}`,
- token,
- );
-};
-
-export function triggerPagerDutyAlarm(
- integrationKey: string,
- source: string,
- description: string,
- userName: string,
-) {
- const options = {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json; charset=UTF-8',
- Accept: 'application/json, text/plain, */*',
- },
- body: JSON.stringify({
+
+ triggerPagerDutyAlarm(
+ integrationKey: string,
+ source: string,
+ description: string,
+ userName: string,
+ ) {
+ const body = JSON.stringify({
event_action: 'trigger',
routing_key: integrationKey,
client: 'Backstage',
@@ -113,8 +99,46 @@ export function triggerPagerDutyAlarm(
user: userName,
},
},
- }),
- };
+ });
+
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ Accept: 'application/json, text/plain, */*',
+ },
+ body,
+ };
+
+ return this.request(`${this.EVENTS_API_URL}/enqueue`, options);
+ }
+
+ private async getByUrl(url: string): Promise {
+ const options = {
+ method: 'GET',
+ headers: {
+ Authorization: `Token token=${this.config!.token}`,
+ Accept: 'application/vnd.pagerduty+json;version=2',
+ 'Content-Type': 'application/json',
+ },
+ };
+ const response = await this.request(url, options);
- return request(`${EVENTS_API_URL}/enqueue`, options);
+ return response.json();
+ }
+
+ private async request(url: string, options: Options): Promise {
+ try {
+ const response = await fetch(url, options);
+ if (!response.ok) {
+ const payload = await response.json();
+ const errors = payload.errors.map((error: string) => error).join(' ');
+ const message = `Request failed with ${response.status}, ${errors}`;
+ throw new Error(message);
+ }
+ return response;
+ } catch (error) {
+ throw new Error(error);
+ }
+ }
}
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index 0117281a0ed20..dd3301904db20 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -114,7 +114,9 @@ type IncidentsProps = {
export const Incidents = ({ incidents }: IncidentsProps) => (
Incidents}>
{incidents.length ? (
- incidents.map(incident => )
+ incidents.map((incident, index) => (
+
+ ))
) : (
)}
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index e17e35a99660a..54713b8ecd818 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -14,18 +14,15 @@
* limitations under the License.
*/
import React from 'react';
-import { InfoCard, useApi, configApiRef } from '@backstage/core';
+import { InfoCard, useApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { Grid, LinearProgress } from '@material-ui/core';
import { Incidents } from './Incidents';
import { EscalationPolicy } from './Escalation';
import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
-import {
- getServiceByIntegrationKey,
- getIncidentsByServiceId,
- getOncallByPolicyId,
-} from '../api/pagerDutyClient';
+import { Alert } from '@material-ui/lab';
+import { pagerDutyApiRef } from '../api/pagerDutyClient';
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -37,65 +34,52 @@ type Props = {
};
export const PagerDutyServiceCard = ({ entity }: Props) => {
- const configApi = useApi(configApiRef);
-
- // TODO: handle missing token
- const token = configApi.getOptionalString('pagerduty.api.token') ?? undefined;
- console.log({ token });
+ const api = useApi(pagerDutyApiRef);
const { value, loading, error } = useAsync(async () => {
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
- const service = await getServiceByIntegrationKey(integrationKey, token);
- const incidents = await getIncidentsByServiceId(
- (service as any).id /*// TODO: fix type */,
- token,
- );
- const oncalls = await getOncallByPolicyId(
- (service as any).escalation_policy.id, // TODO: fix type
- token,
- );
+ const services = await api.getServiceByIntegrationKey(integrationKey);
+ // TODO check services length
+ const service = services[0];
+
+ const incidents = await api.getIncidentsByServiceId(service.id);
+ const oncalls = await api.getOncallByPolicyId(service.escalation_policy.id);
return {
- pagerDutyServices: [
- {
- activeIncidents: incidents,
- escalationPolicy: oncalls,
- id: service.id,
- name: service.name,
- homepageUrl: service.html_url,
- },
- ],
+ incidents,
+ oncalls,
+ id: service.id,
+ name: service.name,
+ homepageUrl: service.html_url,
};
});
if (error) {
- // TODO: use the errorApi
- console.log(error);
- throw new Error(`Error in getting data: ${error.message}`);
+ return (
+
+
+ Error encountered while fetching information. {error.message}
+
+
+ );
}
if (loading) {
return ;
}
- const {
- activeIncidents,
- escalationPolicy,
- homepageUrl,
- } = value!.pagerDutyServices[0]!;
-
const link = {
title: 'View in PagerDuty',
- link: homepageUrl,
+ link: value!.homepageUrl,
};
return (
-
-
+
+
diff --git a/plugins/pagerduty/src/components/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog.tsx
index 7dbb0be5f0d6c..13c2ba6aad819 100644
--- a/plugins/pagerduty/src/components/TriggerDialog.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog.tsx
@@ -26,7 +26,7 @@ import {
} from '@material-ui/core';
import { Progress, useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
-import { triggerPagerDutyAlarm } from '../api/pagerDutyClient';
+import { pagerDutyApiRef } from '../api/pagerDutyClient';
const useStyles = makeStyles({
warningText: {
@@ -48,6 +48,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
+ const api = useApi(pagerDutyApiRef);
const descriptionChanged = (event: any) => {
setDescription(event.target.value);
@@ -55,7 +56,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
const [{ value, loading, error }, triggerAlarm] = useAsyncFn(
async (desc: string) => {
- return await triggerPagerDutyAlarm(
+ return await api.triggerPagerDutyAlarm(
integrationKey,
window.location.toString(),
desc,
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
index 8f8523b78bae1..4073551dc8a06 100644
--- a/plugins/pagerduty/src/plugin.ts
+++ b/plugins/pagerduty/src/plugin.ts
@@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { createPlugin, createRouteRef } from '@backstage/core';
+import {
+ createApiFactory,
+ createPlugin,
+ createRouteRef,
+ configApiRef,
+} from '@backstage/core';
+import { pagerDutyApiRef, PagerDutyClientApi } from './api/pagerDutyClient';
export const rootRouteRef = createRouteRef({
path: '/pagerduty',
@@ -22,4 +28,14 @@ export const rootRouteRef = createRouteRef({
export const plugin = createPlugin({
id: 'pagerduty',
+ apis: [
+ createApiFactory({
+ api: pagerDutyApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) =>
+ new PagerDutyClientApi({
+ token: configApi.getOptionalString('pagerduty.api.token'),
+ }),
+ }),
+ ],
});
From 2aed296e459dde1703a8376c6118b3c499c53686 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Wed, 11 Nov 2020 14:24:40 +0100
Subject: [PATCH 14/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 19c6a15096d9c..f53f79af41a6d 100644
--- a/plugins/pagerduty/src/components/Escalation.tsx
+++ b/plugins/pagerduty/src/components/Escalation.tsx
@@ -29,8 +29,8 @@ import {
import UserIcon from '@material-ui/icons/Person';
import EmailIcon from '@material-ui/icons/Email';
import { StatusWarning } from '@backstage/core';
-import Pagerduty from '../assets/pd.svg';
-import { Oncall, PagerDutyEscalationPolicy } from './types';
+import { Oncall } from './types';
+import PagerdutyIcon from './Pd';
const useStyles = makeStyles({
denseListIcon: {
@@ -45,7 +45,7 @@ const useStyles = makeStyles({
},
});
-const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => {
+const EscalationUser = ({ user }: Oncall) => {
const classes = useStyles();
return (
@@ -56,7 +56,7 @@ const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => {
-
+
@@ -64,12 +64,9 @@ const EscalationUser = ({ user }: PagerDutyEscalationPolicy) => {
href={user.html_url}
target="_blank"
rel="noopener noreferrer"
+ color="primary"
>
-
+
diff --git a/plugins/pagerduty/src/components/Incidents.tsx b/plugins/pagerduty/src/components/Incidents.tsx
index dd3301904db20..512836422c455 100644
--- a/plugins/pagerduty/src/components/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incidents.tsx
@@ -25,11 +25,12 @@ import {
makeStyles,
IconButton,
ListSubheader,
+ Link,
} from '@material-ui/core';
import { StatusError, StatusWarning, StatusOK } from '@backstage/core';
-import Pagerduty from '../assets/pd.svg';
import moment from 'moment';
import { Incident } from '../components/types';
+import PagerdutyIcon from './Pd';
const useStyles = makeStyles({
denseListIcon: {
@@ -64,6 +65,7 @@ type IncidentListItemProps = {
const IncidentListItem = ({ incident }: IncidentListItemProps) => {
const classes = useStyles();
+ const user = incident.assignments[0].assignee;
return (
@@ -82,8 +84,9 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => {
secondary={
Created {moment(incident.created_at).fromNow()}, assigned to{' '}
- {(incident?.assignments[0]?.assignee?.summary &&
- incident.assignments[0].assignee.summary) ||
+ {(incident?.assignments[0]?.assignee?.summary && (
+ {user.summary}
+ )) ||
'nobody'}
}
@@ -91,15 +94,12 @@ const IncidentListItem = ({ incident }: IncidentListItemProps) => {
-
+
diff --git a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
index 54713b8ecd818..4fd4691458903 100644
--- a/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyServiceCard.tsx
@@ -14,15 +14,16 @@
* limitations under the License.
*/
import React from 'react';
-import { InfoCard, useApi } from '@backstage/core';
+import { useApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
-import { Grid, LinearProgress } from '@material-ui/core';
+import { LinearProgress } from '@material-ui/core';
import { Incidents } from './Incidents';
import { EscalationPolicy } from './Escalation';
import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { pagerDutyApiRef } from '../api/pagerDutyClient';
+import { PagerdutyCard } from './PagerdutyCard';
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -71,19 +72,26 @@ export const PagerDutyServiceCard = ({ entity }: Props) => {
return ;
}
- const link = {
+ const pagerdutyLink = {
title: 'View in PagerDuty',
- link: value!.homepageUrl,
+ href: value!.homepageUrl,
+ };
+
+ const triggerAlarm = {
+ title: 'Trigger Alarm',
+ action: ,
};
return (
-
-
-
-
-
-
- {/* todo show something when we dont have token */}
-
+
+
+
+ >
+ }
+ >
);
};
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
new file mode 100644
index 0000000000000..2af4d2e4bc0e6
--- /dev/null
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import {
+ Card,
+ CardHeader,
+ CardContent,
+ Divider,
+ Link,
+ makeStyles,
+} from '@material-ui/core';
+import LinkIcon from '@material-ui/icons/Link';
+import ReportProblemIcon from '@material-ui/icons/ReportProblem';
+import classnames from 'classnames';
+import Pagerduty from '../assets/pd.svg';
+import PagerdutyIcon from './Pd';
+
+// TODO: create a general component to use it in pagerduty and aboutCard
+
+const useStyles = makeStyles(theme => ({
+ links: {
+ margin: theme.spacing(2, 0),
+ display: 'grid',
+ gridAutoFlow: 'column',
+ gridAutoColumns: 'min-content',
+ gridGap: theme.spacing(3),
+ },
+ link: {
+ display: 'grid',
+ justifyItems: 'center',
+ gridGap: 4,
+ textAlign: 'center',
+ },
+ label: {
+ fontSize: '0.7rem',
+ textTransform: 'uppercase',
+ fontWeight: 600,
+ letterSpacing: 1.2,
+ },
+ trigger: {
+ color: theme.palette.secondary.main,
+ },
+ svgButtonImage: {
+ height: '1.5em',
+ color: theme.palette.primary.main,
+ },
+}));
+
+type SubHeaderProps = {
+ ViewPagerduty: { title: string; href: string };
+ TriggerAlarm: { title: string; action: React.ReactNode };
+};
+
+type PagerdutyCardProps = {
+ title: string;
+ subheader: SubHeaderProps;
+ content: React.ReactNode;
+};
+
+type VerticalIconProps = {
+ label: string;
+ href?: string;
+ action?: React.ReactNode;
+ icon?: React.ReactNode;
+};
+
+const VerticalIcon = ({
+ label,
+ href,
+ icon = ,
+ action,
+}: VerticalIconProps) => {
+ const classes = useStyles();
+ if (action) {
+ return (
+ <>
+
+ {icon}
+ {action}
+
+ >
+ );
+ }
+ return (
+ <>
+
+ {icon}
+ {label}
+
+ >
+ );
+};
+export const PagerdutyCard = ({
+ title,
+ subheader,
+ content,
+}: PagerdutyCardProps) => {
+ const classes = useStyles();
+ const getSubheader = ({ ViewPagerduty, TriggerAlarm }: SubHeaderProps) => {
+ return (
+
+
+ //
+
+ }
+ >
+ }
+ 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 0000000000000..3698d0a1c497b
--- /dev/null
+++ b/plugins/pagerduty/src/components/Pd.tsx
@@ -0,0 +1,27 @@
+// import React from 'react';
+// import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+// const SvgPd = (props: SvgIconProps) =>
+// React.createElement(
+// SvgIcon,
+// props,
+//
+//
+// ,
+// );
+
+// export default SvgPd;
+
+import React from 'react';
+import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+const SvgPd = (props: SvgIconProps) =>
+ React.createElement(
+ SvgIcon,
+ props,
+
+
+ ,
+ );
+
+export default SvgPd;
diff --git a/plugins/pagerduty/src/components/TriggerButton.tsx b/plugins/pagerduty/src/components/TriggerButton.tsx
index eb051e68fa0d6..48bed68e2e9de 100644
--- a/plugins/pagerduty/src/components/TriggerButton.tsx
+++ b/plugins/pagerduty/src/components/TriggerButton.tsx
@@ -15,17 +15,34 @@
*/
import React, { useState } from 'react';
-import { Button } from '@material-ui/core';
+import { Button, makeStyles } from '@material-ui/core';
import { TriggerDialog } from './TriggerDialog';
import { Entity } from '@backstage/catalog-model';
import { PAGERDUTY_INTEGRATION_KEY } from './PagerDutyServiceCard';
+const useStyles = makeStyles({
+ triggerAlarm: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ fontSize: '0.7rem',
+ textTransform: 'uppercase',
+ fontWeight: 600,
+ letterSpacing: 1.2,
+ lineHeight: 1.1,
+ '&:hover, &:focus, &.focus': {
+ backgroundColor: 'transparent',
+ textDecoration: 'none',
+ },
+ },
+});
+
type Props = {
entity: Entity;
};
export const TriggerButton = ({ entity }: Props) => {
const [showDialog, setShowDialog] = useState(false);
+ const classes = useStyles();
const handleDialog = () => {
setShowDialog(!showDialog);
@@ -34,12 +51,11 @@ export const TriggerButton = ({ entity }: Props) => {
return (
<>
- 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 0000000000000..db52e36f4af47
--- /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 0000000000000..62c8151af256f
--- /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 0000000000000..a99e2c03f4ede
--- /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 09abaf856c68b..d0cb3719d622f 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 0000000000000..57b35238dd1cd
--- /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 db52e36f4af47..13e2687b04664 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 f53f79af41a6d..56c2c27a037f9 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 512836422c455..52247b8f06fc1 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 4fd4691458903..5bb7c9b5a9e43 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 2af4d2e4bc0e6..539bf69540dfe 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 ca06e795106f0..dec8f18739afe 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 | 44 ++++++++-----------
4 files changed, 49 insertions(+), 38 deletions(-)
diff --git a/plugins/pagerduty/src/api/pagerDutyClient.ts b/plugins/pagerduty/src/api/pagerDutyClient.ts
index d0cb3719d622f..7b1aa442c342f 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 5bb7c9b5a9e43..d343a9fea1226 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 eeaea87a7f80b..36f6e1989d4ff 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 dec8f18739afe..b229949cb4bea 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,28 +33,25 @@ 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 OnCall = {
+ user: User;
};
-export type ResponseOnCall = {
+export type User = {
id: string;
- name: string;
+ summary: string;
email: string;
- homepageUrl: string;
-};
-
-export type Options = {
- method: string;
- headers: HeadersInit;
- body?: BodyInit;
+ html_url: string;
+ name: string;
};
-export type PagerDutyClientConfig = {
+export type ClientApiConfig = {
token?: string;
};
@@ -61,22 +59,16 @@ export type ServicesResponse = {
services: Service[];
};
-export type IncidentResponse = {
+export type IncidentsResponse = {
incidents: Incident[];
};
export type OnCallsResponse = {
- onCalls: OnCall[];
-};
-
-export type User = {
- id: string;
- summary: string;
- email: string;
- html_url: string;
- name: string;
+ oncalls: OnCall[];
};
-export type OnCall = {
- user: User;
+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 e5d0264be3dda..43aad8579af2e 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 13e2687b04664..0000000000000
--- 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 0000000000000..3e4a59d15a617
--- /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 0000000000000..8a2aacead9678
--- /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 56c2c27a037f9..3fa8ef8d659b4 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 0000000000000..d5870116014a3
--- /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 0000000000000..ded161261ed9c
--- /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 52247b8f06fc1..f01c99fc8cfc6 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 62c8151af256f..fbe3783d3032e 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 0000000000000..a49d913c80c79
--- /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 d343a9fea1226..480e7cdbd6f51 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 a99e2c03f4ede..387b168e058ce 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 | 190 ++++++++++--------
.../{ => 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, 184 insertions(+), 197 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 3e4a59d15a617..4fa65eb999852 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 8a2aacead9678..cbce0ce4a3fb3 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 3fa8ef8d659b4..e20a5f7567b7e 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 0000000000000..ac2db62cd9f3a
--- /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 f01c99fc8cfc6..4700583a8bd67 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 0000000000000..fb2702602b5e7
--- /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 480e7cdbd6f51..0000000000000
--- 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 539bf69540dfe..6b6d46685082d 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';
-type PagerdutyCardProps = {
- title: string;
- subheader: SubHeaderProps;
- content: React.ReactNode;
-};
+export const isPluginApplicableToEntity = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
-type VerticalIconProps = {
- label: string;
- href?: string;
- action?: React.ReactNode;
- icon?: React.ReactNode;
+type Props = {
+ entity: Entity;
};
-const VerticalIcon = ({
- label,
- href,
- icon = ,
- action,
-}: VerticalIconProps) => {
+export const PagerDutyCard = ({ entity }: Props) => {
const classes = useStyles();
- if (action) {
- return (
- <>
-
- {icon}
- {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,
);
- }
- return (
- <>
-
- {icon}
- {label}
-
- >
- );
-};
-export const PagerdutyCard = ({
- title,
- subheader,
- content,
-}: PagerdutyCardProps) => {
- const classes = useStyles();
- const getSubheader = ({ ViewPagerduty, TriggerAlarm }: SubHeaderProps) => {
+ 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 (
-
- }
- >
- }
- action={TriggerAlarm.action}
- >
-
+
+ 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 (
+ 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 48bed68e2e9de..8feebf9e89367 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 0000000000000..6a58a4f43f5e1
--- /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 387b168e058ce..7e26985136e9b 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 08044d02f6860..91ec3cd27d2be 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 0000000000000..655cef85044da
--- /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 b229949cb4bea..9c9e29cb019d9 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 ddc8ed4b1302c..4fc894ef90a76 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 43aad8579af2e..29518bd858c7c 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 7b1aa442c342f..985bb25e5a709 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 0000000000000..5ac7470475b49
--- /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 0000000000000..cb9d7fc5bc712
--- /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 b24018887a8d2..58dc39932dd9c 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 4073551dc8a06..2e07b5c430a18 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 580dcff0f572d..63df4feaf903b 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 762f4924c0063..101680c55299d 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 c31629e82e0e4..ef55b007a7b45 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 0ca17c7d7fb01..18fd44f0891b3 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 6dc3d8c9ba1cc..0000000000000
--- 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 16a033dbc6027..0000000000000
--- 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 099a94a85f500..0000000000000
--- 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 cad4ba268ea59..0000000000000
--- 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 7612c392a2c33..0000000000000
--- 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 b96989e4b85ab..0000000000000
--- 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 0aaeafa379667..0000000000000
--- 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 1fbab9b55169f..0000000000000
--- 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 73f928bef4dee..0000000000000
--- 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 a5907fd52ff07..0000000000000
--- 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 715d564febc3a..fbeba50835c20 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 93765ff942aa5..0491d467f8962 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 f0e8b103e6980..5949209128843 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 29518bd858c7c..2fce2ebef321a 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 17231abb9ab1f..6099b6ffe0367 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 985bb25e5a709..c9d40a16941cd 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 cb9d7fc5bc712..c07834a213701 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 237ffa2011254..0000000000000
--- 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 e20a5f7567b7e..adc8d4e6d7196 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 0335cb5a9bbba..9c5c0b24857e4 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -1,13 +1,89 @@
-# pagerduty
+# PagerDuty
-Welcome to the pagerduty plugin!
+## Overview
-_This plugin was created through the Backstage CLI_
+This plugin displays PagerDuty information about an entity such as if there are any active incidents and how the escalation policy looks like.
-## Getting started
+There is also an easy way to trigger an alarm directly to the person who is currently on-call.
-Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/pagerduty](http://localhost:3000/pagerduty).
+This plugin requires that entities are annotated with an [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service). See more further down in this document.
-You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
-This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
-It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+This plugin provides:
+
+- A list of incidents
+- A way to trigger an alarm to the person on-call
+- Information details about the person on-call
+
+## Setup instructions
+
+Install the plugin:
+
+```bash
+yarn add @backstage/plugin-pagerduty
+```
+
+Add it to the app in `plugins.ts`:
+
+```ts
+export { plugin as Pagerduty } from '@backstage/plugin-pagerduty';
+```
+
+Add it to the `EntityPage.ts`:
+
+```ts
+import {
+ isPluginApplicableToEntity as isPagerDutyAvailable,
+ PagerDutyCard,
+} from '@backstage/plugin-pagerduty';
+// add to code
+{
+ isPagerDutyAvailable(entity) && (
+
+
+
+ );
+}
+```
+
+## Client configuration
+
+The PagerDutyClient can be configured with the appropriate urls:
+
+In `apis.ts`:
+
+```ts
+createApiFactory({
+ api: pagerDutyApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) =>
+ new PagerDutyClientApi({
+ api_url: `https://api.pagerduty.com`,
+ events_url: 'https://events.pagerduty.com/v2',
+ }),
+}),
+```
+
+## Providing the API Token
+
+In order for the client to make requests to the [PagerDuty API](https://developer.pagerduty.com/docs/rest-api-v2/rest-api/) it needs an [API Token](https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key).
+
+Then start the backend passing the token as an environment variable:
+
+```bash
+$ PAGERDUTY_TOKEN='Token token=' yarn start
+```
+
+This will proxy the request by adding `Authorization` header with the provided token.
+
+## Integration Key
+
+The information displayed for each entity is based on the [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service).
+
+### Adding the integration key to the entity annotation
+
+If you want to use this plugin for an entity, you need to label it with the below annotation:
+
+```yml
+annotations:
+ pagerduty.com/integration-key: [INTEGRATION_KEY]
+```
From d14f125ed1769b2c8fee7c51f572eb304b4af43d Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 20 Nov 2020 12:03:27 +0100
Subject: [PATCH 27/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 cbce0ce4a3fb3..ccf70abb0b777 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
@@ -15,21 +15,47 @@
*/
import React from 'react';
-import { List, ListSubheader } from '@material-ui/core';
-import { User } from '../types';
+import { List, ListSubheader, LinearProgress } from '@material-ui/core';
import { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
import { EscalationUser } from './EscalationUser';
+import { useAsync } from 'react-use';
+import { pagerDutyApiRef } from '../../api';
+import { useApi } from '@backstage/core';
+import { Alert } from '@material-ui/lab';
-type EscalationPolicyProps = {
- users: User[];
+type Props = {
+ policyId: string;
};
-export const EscalationPolicy = ({ users }: EscalationPolicyProps) => (
- ON CALL}>
- {users.length ? (
- users.map((user, index) => )
- ) : (
-
- )}
-
-);
+export const EscalationPolicy = ({ policyId }: Props) => {
+ const api = useApi(pagerDutyApiRef);
+
+ const { value: users, loading, error } = useAsync(async () => {
+ const oncalls = await api.getOnCallByPolicyId(policyId);
+ const users = oncalls.map(oncall => oncall.user);
+
+ return users;
+ });
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return users!.length ? (
+ ON CALL}>
+ {users!.map((user, index) => (
+
+ ))}
+
+ ) : (
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
index adc8d4e6d7196..642c9ecd96e93 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
@@ -36,8 +36,13 @@ const useStyles = makeStyles({
},
});
-export const EscalationUser = ({ user }: { user: User }) => {
+type Props = {
+ user: User;
+};
+
+export const EscalationUser = ({ user }: Props) => {
const classes = useStyles();
+
return (
diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
index 6e22ccbbac106..7a97dad281ac7 100644
--- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
+++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
@@ -31,10 +31,6 @@ import moment from 'moment';
import { Incident } from '../types';
import PagerdutyIcon from '../PagerDutyIcon';
-type IncidentListItemProps = {
- incident: Incident;
-};
-
const useStyles = makeStyles({
denseListIcon: {
marginRight: 0,
@@ -51,9 +47,15 @@ const useStyles = makeStyles({
},
});
-export const IncidentListItem = ({ incident }: IncidentListItemProps) => {
+type Props = {
+ incident: Incident;
+};
+
+export const IncidentListItem = ({ incident }: Props) => {
const classes = useStyles();
- const user = incident.assignments[0].assignee;
+ const user = incident.assignments[0]?.assignee;
+ const createdAt = moment(incident.created_at).fromNow();
+
return (
@@ -68,25 +70,28 @@ export const IncidentListItem = ({ incident }: IncidentListItemProps) => {
- {incident.title}
-
- }
+ primary={incident.title}
+ primaryTypographyProps={{
+ variant: 'body1',
+ className: classes.listItemPrimary,
+ }}
secondary={
-
- Created {moment(incident.created_at).fromNow()}, assigned to{' '}
- {(incident?.assignments[0]?.assignee?.summary && (
- {user.summary}
- )) ||
- 'nobody'}
-
+
+ Created {createdAt} and assigned to{' '}
+
+ {user?.summary ?? 'nobody'}
+
+
}
/>
void;
};
-export const Incidents = ({ incidents }: IncidentsProps) => (
- {incidents.length && 'INCIDENTS'}}
- >
- {incidents.length ? (
- incidents.map((incident, index) => (
+export const Incidents = ({
+ serviceId,
+ shouldRefreshIncidents,
+ setShouldRefreshIncidents,
+}: Props) => {
+ const api = useApi(pagerDutyApiRef);
+
+ const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
+ async () => {
+ setShouldRefreshIncidents(false);
+ return await api.getIncidentsByServiceId(serviceId);
+ },
+ );
+
+ useEffect(() => {
+ getIncidents();
+ }, [shouldRefreshIncidents, getIncidents]);
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ return incidents?.length ? (
+ INCIDENTS}>
+ {incidents!.map((incident, index) => (
- ))
- ) : (
-
- )}
-
-);
+ ))}
+
+ ) : (
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index 6b6d46685082d..f24e91a9e05a9 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { useApi, EmptyState } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
+ Button,
Card,
CardContent,
CardHeader,
@@ -26,13 +27,13 @@ import {
} from '@material-ui/core';
import { Incidents } from './Incident';
import { EscalationPolicy } from './Escalation';
-import { TriggerButton } from './TriggerButton';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { pagerDutyApiRef, UnauthorizedError } from '../api';
import { IconLinkVertical } from '@backstage/plugin-catalog';
import PagerDutyIcon from './PagerDutyIcon';
-import ReportProblemIcon from '@material-ui/icons/ReportProblem';
+import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
+import { TriggerDialog } from './TriggerDialog';
const useStyles = makeStyles(theme => ({
links: {
@@ -42,6 +43,19 @@ const useStyles = makeStyles(theme => ({
gridAutoColumns: 'min-content',
gridGap: theme.spacing(3),
},
+ triggerAlarm: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ fontSize: '0.7rem',
+ textTransform: 'uppercase',
+ fontWeight: 600,
+ letterSpacing: 1.2,
+ lineHeight: 1.5,
+ '&:hover, &:focus, &.focus': {
+ backgroundColor: 'transparent',
+ textDecoration: 'none',
+ },
+ },
}));
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -56,35 +70,45 @@ type Props = {
export const PagerDutyCard = ({ entity }: Props) => {
const classes = useStyles();
const api = useApi(pagerDutyApiRef);
+ const [showDialog, setShowDialog] = useState(false);
+ const [shouldRefreshIncidents, setShouldRefreshIncidents] = useState(
+ false,
+ );
+ const integrationKey = entity.metadata.annotations![
+ PAGERDUTY_INTEGRATION_KEY
+ ];
- const { value, loading, error } = useAsync(async () => {
- const integrationKey = entity.metadata.annotations![
- PAGERDUTY_INTEGRATION_KEY
- ];
-
+ const { value: service, loading, error } = useAsync(async () => {
const services = await api.getServiceByIntegrationKey(integrationKey);
- const incidents = await api.getIncidentsByServiceId(services[0].id);
- const oncalls = await api.getOnCallByPolicyId(
- services[0].escalation_policy.id,
- );
- const users = oncalls.map(oncall => oncall.user);
return {
- incidents,
- users,
id: services[0].id,
name: services[0].name,
- homepageUrl: services[0].html_url,
+ url: services[0].html_url,
+ policyId: services[0].escalation_policy.id,
};
});
+ const handleDialog = () => {
+ setShowDialog(!showDialog);
+ };
+
if (error) {
if (error instanceof UnauthorizedError) {
return (
+ Read More
+
+ }
/>
);
}
@@ -102,12 +126,21 @@ export const PagerDutyCard = ({ entity }: Props) => {
const pagerdutyLink = {
title: 'View in PagerDuty',
- href: value!.homepageUrl,
+ href: service!.url,
};
const triggerAlarm = {
title: 'Trigger Alarm',
- action: ,
+ action: (
+
+ 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 a4f012ac47bd7..0000000000000
--- a/plugins/pagerduty/src/components/TriggerButton/TriggerButton.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React, { useState } from 'react';
-import { Button, makeStyles } from '@material-ui/core';
-import { TriggerDialog } from '../TriggerDialog';
-import { Entity } from '@backstage/catalog-model';
-import { PAGERDUTY_INTEGRATION_KEY } from '../PagerDutyCard';
-
-const useStyles = makeStyles({
- triggerAlarm: {
- paddingTop: 0,
- paddingBottom: 0,
- fontSize: '0.7rem',
- textTransform: 'uppercase',
- fontWeight: 600,
- letterSpacing: 1.2,
- lineHeight: 1.5,
- '&:hover, &:focus, &.focus': {
- backgroundColor: 'transparent',
- textDecoration: 'none',
- },
- },
-});
-
-type Props = {
- entity: Entity;
-};
-
-export const TriggerButton = ({ entity }: Props) => {
- const [showDialog, setShowDialog] = useState(false);
- const classes = useStyles();
-
- const handleDialog = () => {
- setShowDialog(!showDialog);
- };
-
- return (
- <>
-
- 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 6a58a4f43f5e1..0000000000000
--- a/plugins/pagerduty/src/components/TriggerButton/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export { TriggerButton } from './TriggerButton';
diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
index 4156074460066..e6bb6d107d6e3 100644
--- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
@@ -19,67 +19,61 @@ import {
Dialog,
DialogTitle,
TextField,
- makeStyles,
DialogActions,
Button,
DialogContent,
Typography,
CircularProgress,
} from '@material-ui/core';
-import {
- Progress,
- useApi,
- alertApiRef,
- identityApiRef,
- WarningPanel,
-} from '@backstage/core';
+import { useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
import { pagerDutyApiRef } from '../../api';
-import { Alert, AlertTitle } from '@material-ui/lab';
-
-const useStyles = makeStyles({
- warningText: {
- border: '1px solid rgba(245, 155, 35, 0.5)',
- backgroundColor: 'rgba(245, 155, 35, 0.2)',
- padding: '0.5em 1em',
- },
-});
+import { Alert } from '@material-ui/lab';
type Props = {
name: string;
integrationKey: string;
- onClose: () => void;
+ showDialog: boolean;
+ handleDialog: () => void;
+ setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void;
};
-export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
- const classes = useStyles();
- const [description, setDescription] = useState('');
+export const TriggerDialog = ({
+ name,
+ integrationKey,
+ showDialog,
+ handleDialog,
+ setShouldRefreshIncidents,
+}: Props) => {
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const api = useApi(pagerDutyApiRef);
-
- const descriptionChanged = (event: any) => {
- setDescription(event.target.value);
- };
+ const [description, setDescription] = useState('');
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
- async (desc: string) => {
- return await api.triggerAlarm(
+ async (desc: string) =>
+ await api.triggerAlarm(
integrationKey,
window.location.toString(),
desc,
userId,
- );
- },
+ ),
);
+ const descriptionChanged = (
+ event: React.ChangeEvent,
+ ) => {
+ setDescription(event.target.value);
+ };
+
useEffect(() => {
if (value) {
alertApi.post({
message: `Alarm successfully triggered by ${userId}`,
});
- onClose();
+ setShouldRefreshIncidents(true);
+ handleDialog();
}
if (error) {
@@ -88,16 +82,19 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
severity: 'error',
});
}
- }, [value, error, alertApi, onClose, userId]);
+ }, [value, error, alertApi, handleDialog, userId, setShouldRefreshIncidents]);
+
+ if (!showDialog) {
+ return null;
+ }
return (
-
+
This action will trigger an incident for "{name}" .
- {/* "Note" */}
If the issue you are seeing does not need urgent attention, please
get in touch with the responsible team using their preferred
@@ -141,7 +138,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
>
Trigger Incident
-
+
Close
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.tsx
index 9c9e29cb019d9..e968656e65d6f 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -18,10 +18,10 @@ export type Incident = {
id: string;
title: string;
status: string;
- homepageUrl: string;
+ html_url: string;
assignments: [
{
- assignee: User;
+ assignee: Assignee;
},
];
serviceId: string;
@@ -43,6 +43,12 @@ export type OnCall = {
user: User;
};
+export type Assignee = {
+ id: string;
+ summary: string;
+ html_url: string;
+};
+
export type User = {
id: string;
summary: string;
diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts
index 58dc39932dd9c..1e8e549ac92f2 100644
--- a/plugins/pagerduty/src/index.ts
+++ b/plugins/pagerduty/src/index.ts
@@ -17,4 +17,9 @@ export { plugin } from './plugin';
export {
isPluginApplicableToEntity,
PagerDutyCard,
-} from './components/PagerDutyCard';
+} from './components/PagerdutyCard';
+export {
+ PagerDutyClientApi,
+ pagerDutyApiRef,
+ UnauthorizedError,
+} from './api/client';
From 5519358df8ec1d4d2e39b24d991ec9f110ef92d9 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 20 Nov 2020 12:05:51 +0100
Subject: [PATCH 28/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 4fa65eb999852..15ff3278a0cad 100644
--- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
+++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
@@ -14,34 +14,82 @@
* limitations under the License.
*/
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import { EscalationPolicy } from './EscalationPolicy';
import { wrapInTestApp } from '@backstage/test-utils';
import { User } from '../types';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { pagerDutyApiRef } from '../../api';
-const escalations: User[] = [
- {
- name: 'person1',
- id: 'p1',
- summary: 'person1',
- email: 'person1@example.com',
- html_url: 'http://a.com/id1',
- },
-];
+const mockPagerDutyApi = {
+ getOnCallByPolicyId: () => [],
+};
+const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]);
describe('Escalation', () => {
- it('render emptyState', () => {
- const { getByText } = render(
- wrapInTestApp( ),
+ it('Handles an empty response', async () => {
+ mockPagerDutyApi.getOnCallByPolicyId = jest
+ .fn()
+ .mockImplementationOnce(async () => []);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
);
+ await waitFor(() => !queryByTestId('progress'));
+
expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith('456');
});
- it('render escalation list', () => {
- const { getByText } = render(
- wrapInTestApp( ),
+ it('Render a list of users', async () => {
+ mockPagerDutyApi.getOnCallByPolicyId = jest
+ .fn()
+ .mockImplementationOnce(async () => [
+ {
+ user: {
+ name: 'person1',
+ id: 'p1',
+ summary: 'person1',
+ email: 'person1@example.com',
+ html_url: 'http://a.com/id1',
+ } as User,
+ },
+ ]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
);
+ await waitFor(() => !queryByTestId('progress'));
+
expect(getByText('person1')).toBeInTheDocument();
expect(getByText('person1@example.com')).toBeInTheDocument();
+ expect(mockPagerDutyApi.getOnCallByPolicyId).toHaveBeenCalledWith('abc');
+ });
+
+ it('Handles errors', async () => {
+ mockPagerDutyApi.getOnCallByPolicyId = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('Error message'));
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(
+ getByText('Error encountered while fetching information. Error message'),
+ ).toBeInTheDocument();
});
});
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
index fbe3783d3032e..6baa41002104f 100644
--- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
@@ -14,65 +14,100 @@
* limitations under the License.
*/
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import { Incidents } from './Incidents';
import { wrapInTestApp } from '@backstage/test-utils';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import { pagerDutyApiRef } from '../../api';
import { Incident } from '../types';
-const incidents: Incident[] = [
- {
- id: 'id1',
- status: 'triggered',
- title: 'title1',
- created_at: '2020-11-06T00:00:00Z',
- assignments: [
- {
- assignee: {
- name: 'person1',
- id: 'p1',
- summary: 'person1',
- email: 'person1@example.com',
- html_url: 'http://a.com/id1',
- },
- },
- ],
- homepageUrl: 'http://a.com/id1',
- serviceId: 'sId1',
- },
- {
- id: 'id2',
- status: 'acknowledged',
- title: 'title2',
- created_at: '2020-11-07T00:00:00Z',
- assignments: [
- {
- assignee: {
- name: 'person2',
- id: 'p2',
- summary: 'person2',
- email: 'person2@example.com',
- html_url: 'http://a.com/id2',
- },
- },
- ],
- homepageUrl: 'http://a.com/id2',
- serviceId: 'sId2',
- },
-];
+const mockPagerDutyApi = {
+ getIncidentsByServiceId: () => [],
+};
+const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]);
describe('Incidents', () => {
- it('renders an empty state is there are no incidents', () => {
- const { getByText } = render(wrapInTestApp( ));
+ it('Renders an empty state when there are no incidents', async () => {
+ mockPagerDutyApi.getIncidentsByServiceId = jest
+ .fn()
+ .mockImplementationOnce(async () => []);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+ {}}
+ />
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
expect(
getByText('Nice! No incidents are assigned to you!'),
).toBeInTheDocument();
});
- it('renders all incidents', () => {
- const { getByText, getByTitle, getAllByTitle, getByLabelText } = render(
- wrapInTestApp( ),
- );
+ it('Renders all incidents', async () => {
+ mockPagerDutyApi.getIncidentsByServiceId = jest.fn().mockImplementationOnce(
+ async () =>
+ [
+ {
+ id: 'id1',
+ status: 'triggered',
+ title: 'title1',
+ created_at: '2020-11-06T00:00:00Z',
+ assignments: [
+ {
+ assignee: {
+ id: 'p1',
+ summary: 'person1',
+ html_url: 'http://a.com/id1',
+ },
+ },
+ ],
+ html_url: 'http://a.com/id1',
+ serviceId: 'sId1',
+ },
+ {
+ id: 'id2',
+ status: 'acknowledged',
+ title: 'title2',
+ created_at: '2020-11-07T00:00:00Z',
+ assignments: [
+ {
+ assignee: {
+ id: 'p2',
+ summary: 'person2',
+ html_url: 'http://a.com/id2',
+ },
+ },
+ ],
+ html_url: 'http://a.com/id2',
+ serviceId: 'sId2',
+ },
+ ] as Incident[],
+ );
+ const {
+ getByText,
+ getByTitle,
+ getAllByTitle,
+ getByLabelText,
+ queryByTestId,
+ } = render(
+ wrapInTestApp(
+
+ {}}
+ />
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
expect(getByText('title1')).toBeInTheDocument();
expect(getByText('title2')).toBeInTheDocument();
expect(getByText('person1')).toBeInTheDocument();
@@ -85,4 +120,26 @@ describe('Incidents', () => {
// assert links, mailto and hrefs, date calculation
expect(getAllByTitle('View in PagerDuty').length).toEqual(2);
});
+
+ it('Handle errors', async () => {
+ mockPagerDutyApi.getIncidentsByServiceId = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('Error occurred'));
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+ {}}
+ />
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(
+ getByText('Error encountered while fetching information. Error occurred'),
+ ).toBeInTheDocument();
+ });
});
diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
new file mode 100644
index 0000000000000..bca6f9f940fd6
--- /dev/null
+++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { render, waitFor, fireEvent } from '@testing-library/react';
+import { PagerDutyCard } from './PagerdutyCard';
+import { Entity } from '@backstage/catalog-model';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ alertApiRef,
+ ApiProvider,
+ ApiRegistry,
+ createApiRef,
+} from '@backstage/core';
+import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api';
+import { Service } from './types';
+import { act } from 'react-dom/test-utils';
+
+const mockPagerDutyApi: Partial = {
+ getServiceByIntegrationKey: async () => [],
+ getOnCallByPolicyId: async () => [],
+ getIncidentsByServiceId: async () => [],
+};
+
+const apis = ApiRegistry.from([
+ [pagerDutyApiRef, mockPagerDutyApi],
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+]);
+const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'pagerduty-test',
+ annotations: {
+ 'pagerduty.com/integration-key': 'abc123',
+ },
+ },
+};
+
+const service: Service = {
+ id: 'abc',
+ name: 'pagerduty-name',
+ html_url: 'www.example.com',
+ escalation_policy: {
+ id: 'def',
+ user: {
+ name: 'person1',
+ id: 'p1',
+ summary: 'person1',
+ email: 'person1@example.com',
+ html_url: 'http://a.com/id1',
+ },
+ },
+ integrationKey: 'abcd',
+};
+
+describe('PageDutyCard', () => {
+ it('Render pagerduty', async () => {
+ mockPagerDutyApi.getServiceByIntegrationKey = jest
+ .fn()
+ .mockImplementationOnce(async () => [service]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('View in PagerDuty')).toBeInTheDocument();
+ expect(getByText('Trigger Alarm')).toBeInTheDocument();
+ expect(
+ getByText('Nice! No incidents are assigned to you!'),
+ ).toBeInTheDocument();
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
+ it('Handles custom error for missing token', async () => {
+ mockPagerDutyApi.getServiceByIntegrationKey = jest
+ .fn()
+ .mockRejectedValueOnce(new UnauthorizedError());
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Missing or invalid PagerDuty Token')).toBeInTheDocument();
+ });
+
+ it('handles general error', async () => {
+ mockPagerDutyApi.getServiceByIntegrationKey = jest
+ .fn()
+ .mockRejectedValueOnce(new Error('An error occurred'));
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(
+ getByText(
+ 'Error encountered while fetching information. An error occurred',
+ ),
+ ).toBeInTheDocument();
+ });
+ it('opens the dialog when trigger button is clicked', async () => {
+ mockPagerDutyApi.getServiceByIntegrationKey = jest
+ .fn()
+ .mockImplementationOnce(async () => [service]);
+
+ const { getByText, queryByTestId, getByTestId, getByRole } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('View in PagerDuty')).toBeInTheDocument();
+ expect(getByText('Trigger Alarm')).toBeInTheDocument();
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(getByRole('dialog')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
index 7e26985136e9b..440621dcf6f3c 100644
--- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -25,9 +25,9 @@ import {
identityApiRef,
} from '@backstage/core';
import { pagerDutyApiRef } from '../../api';
-import { TriggerButton } from '../TriggerButton';
import { Entity } from '@backstage/catalog-model';
import { act } from 'react-dom/test-utils';
+import { TriggerDialog } from './TriggerDialog';
describe('TriggerDialog', () => {
const mockIdentityApi: Partial = {
@@ -36,7 +36,7 @@ describe('TriggerDialog', () => {
const mockTriggerAlarmFn = jest.fn();
const mockPagerDutyApi = {
- triggerPagerDutyAlarm: mockTriggerAlarmFn,
+ triggerAlarm: mockTriggerAlarmFn,
};
const apis = ApiRegistry.from([
@@ -63,24 +63,25 @@ describe('TriggerDialog', () => {
},
};
- const { getByText, getByRole, queryByRole, getByTestId } = render(
+ const { getByText, getByRole, getByTestId } = render(
wrapInTestApp(
-
+ {}}
+ name={entity.metadata.name}
+ integrationKey="abc123"
+ setShouldRefreshIncidents={() => {}}
+ />
,
),
);
- expect(queryByRole('dialog')).toBeNull();
- const alarmButton = getByText('Trigger Alarm');
- fireEvent.click(alarmButton);
+
expect(getByRole('dialog')).toBeInTheDocument();
expect(
- getByText(
- 'This action will send PagerDuty alarms and notifications to on-call people responsible for',
- {
- exact: false,
- },
- ),
+ getByText('This action will trigger an incident for ', {
+ exact: false,
+ }),
).toBeInTheDocument();
const input = getByTestId('trigger-input');
const description = 'Test Trigger Alarm';
From 6011b7d3e5476bf6c0e582cb132d282c434d872c Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 20 Nov 2020 12:07:43 +0100
Subject: [PATCH 29/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 0000000000000..0952612d846fe
--- /dev/null
+++ b/.changeset/great-spiders-repair.md
@@ -0,0 +1,7 @@
+---
+'example-app': patch
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-pagerduty': patch
+---
+
+Added pagerduty plugin to example app
From 60b290cd0bcbe4e1fccf04897041836c3f051f0d Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 11:10:04 +0100
Subject: [PATCH 30/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 9c5c0b24857e4..da37282aa7aef 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -47,18 +47,17 @@ import {
## Client configuration
-The PagerDutyClient can be configured with the appropriate urls:
+The client takes a config object which contains an instance of the `discoveryApi` which is used to look up the `baseUrl` of the `proxy`, and a `eventUrl` which is defaulted to: "https://events.pagerduty.com/v2".
In `apis.ts`:
```ts
createApiFactory({
api: pagerDutyApiRef,
- deps: { configApi: configApiRef },
- factory: ({ configApi }) =>
+ deps: { discoveryApi: discoveryApiRef },
+ factory: ({ discoveryApi }) =>
new PagerDutyClientApi({
- api_url: `https://api.pagerduty.com`,
- events_url: 'https://events.pagerduty.com/v2',
+ discoveryApi: discoveryApi
}),
}),
```
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
index c9d40a16941cd..4ed994eec24be 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -39,7 +39,9 @@ export class PagerDutyClientApi implements PagerDutyClient {
async getServiceByIntegrationKey(integrationKey: string): Promise {
const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`;
- const url = `${this.config.api_url}/services?${params}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/services?${params}`;
const { services } = await this.getByUrl(url);
return services;
@@ -47,7 +49,9 @@ export class PagerDutyClientApi implements PagerDutyClient {
async getIncidentsByServiceId(serviceId: string): Promise {
const params = `service_ids[]=${serviceId}`;
- const url = `${this.config.api_url}/incidents?${params}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/incidents?${params}`;
const { incidents } = await this.getByUrl(url);
return incidents;
@@ -55,7 +59,9 @@ export class PagerDutyClientApi implements PagerDutyClient {
async getOnCallByPolicyId(policyId: string): Promise {
const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
- const url = `${this.config.api_url}/oncalls?${params}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/oncalls?${params}`;
const { oncalls } = await this.getByUrl(url);
return oncalls;
@@ -92,7 +98,10 @@ export class PagerDutyClientApi implements PagerDutyClient {
body,
};
- return this.request(`${this.config.events_url}/enqueue`, options);
+ return this.request(
+ `${this.config.eventsUrl ?? 'https://events.pagerduty.com/v2'}/enqueue`,
+ options,
+ );
}
private async getByUrl(url: string): Promise {
From aa88559488d9070f93471a716b235ab07c55eb3e Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 11:24:19 +0100
Subject: [PATCH 31/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 e968656e65d6f..5e5ee0a61be47 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.tsx
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { DiscoveryApi } from '@backstage/core';
+
export type Incident = {
id: string;
title: string;
@@ -76,6 +78,6 @@ export type RequestOptions = {
};
export type ClientApiConfig = {
- api_url: string;
- events_url: string;
+ eventsUrl?: string;
+ discoveryApi: DiscoveryApi;
};
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
index 2e07b5c430a18..02b11ed773106 100644
--- a/plugins/pagerduty/src/plugin.ts
+++ b/plugins/pagerduty/src/plugin.ts
@@ -17,7 +17,7 @@ import {
createApiFactory,
createPlugin,
createRouteRef,
- configApiRef,
+ discoveryApiRef,
} from '@backstage/core';
import { pagerDutyApiRef, PagerDutyClientApi } from './api';
@@ -31,13 +31,10 @@ export const plugin = createPlugin({
apis: [
createApiFactory({
api: pagerDutyApiRef,
- deps: { configApi: configApiRef },
- factory: ({ configApi }) =>
+ deps: { discoveryApi: discoveryApiRef },
+ factory: ({ discoveryApi }) =>
new PagerDutyClientApi({
- api_url: `${configApi.getString(
- 'backend.baseUrl',
- )}/api/proxy/pagerduty`,
- events_url: 'https://events.pagerduty.com/v2',
+ discoveryApi: discoveryApi,
}),
}),
],
From ac6e65aa1ae4bcb280e5a8d76f8cf00f76430bd4 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 14:10:17 +0100
Subject: [PATCH 32/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 0000000000000..c22552b7c6b7c
--- /dev/null
+++ b/plugins/pagerduty/src/components/MissingTokenError.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { EmptyState } from '@backstage/core';
+import { Button } from '@material-ui/core';
+
+export const MissingTokenError = () => (
+
+ Read More
+
+ }
+ />
+);
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index f24e91a9e05a9..22a471c031db9 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState } from 'react';
-import { useApi, EmptyState } from '@backstage/core';
+import { useApi } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
Button,
@@ -31,9 +31,10 @@ import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { pagerDutyApiRef, UnauthorizedError } from '../api';
import { IconLinkVertical } from '@backstage/plugin-catalog';
-import PagerDutyIcon from './PagerDutyIcon';
+import { PagerDutyIcon } from './PagerDutyIcon';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
+import { MissingTokenError } from './MissingTokenError';
const useStyles = makeStyles(theme => ({
links: {
@@ -93,26 +94,11 @@ export const PagerDutyCard = ({ entity }: Props) => {
setShowDialog(!showDialog);
};
- if (error) {
- if (error instanceof UnauthorizedError) {
- return (
-
- Read More
-
- }
- />
- );
- }
+ if (error instanceof UnauthorizedError) {
+ return ;
+ }
+ if (error) {
return (
Error encountered while fetching information. {error.message}
From a57ed028f85b4beda64830a2f17b14254be9b7e2 Mon Sep 17 00:00:00 2001
From: samiramkr
Date: Fri, 27 Nov 2020 14:49:51 +0100
Subject: [PATCH 33/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 da37282aa7aef..b3e0b9f4e642b 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -2,7 +2,7 @@
## Overview
-This plugin displays PagerDuty information about an entity such as if there are any active incidents and how the escalation policy looks like.
+This plugin displays PagerDuty information about an entity such as if there are any active incidents and what the escalation policy is.
There is also an easy way to trigger an alarm directly to the person who is currently on-call.
From 43d0967db89e2cf32808d0ae4a02ed442a27c755 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 16:10:55 +0100
Subject: [PATCH 34/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 7e1dc61fa72e4..4a8bf2a0abc6c 100644
--- a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
+++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
@@ -75,11 +75,11 @@ export function IconLinkVertical({
if (action) {
return (
From b2013cc40b33ec4d43138c4d02e553c5b142b981 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 16:35:53 +0100
Subject: [PATCH 35/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 4ed994eec24be..7b453b7298bc4 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -25,7 +25,7 @@ import {
OnCallsResponse,
OnCall,
} from '../components/types';
-import { PagerDutyClient } from './types';
+import { PagerDutyClient, TriggerAlarmRequest } from './types';
export class UnauthorizedError extends Error {}
@@ -67,12 +67,12 @@ export class PagerDutyClientApi implements PagerDutyClient {
return oncalls;
}
- triggerAlarm(
- integrationKey: string,
- source: string,
- description: string,
- userName: string,
- ) {
+ triggerAlarm({
+ integrationKey,
+ source,
+ description,
+ userName,
+ }: TriggerAlarmRequest): Promise {
const body = JSON.stringify({
event_action: 'trigger',
routing_key: integrationKey,
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
index c07834a213701..17c2067818c38 100644
--- a/plugins/pagerduty/src/api/types.ts
+++ b/plugins/pagerduty/src/api/types.ts
@@ -16,6 +16,13 @@
import { Incident, OnCall, Service } from '../components/types';
+export type TriggerAlarmRequest = {
+ integrationKey: string;
+ source: string;
+ description: string;
+ userName: string;
+};
+
export interface PagerDutyClient {
/**
* Fetches a list of services, filtered by the provided integration key.
@@ -38,10 +45,5 @@ export interface PagerDutyClient {
/**
* Triggers an incident to whoever is on-call.
*/
- triggerAlarm(
- integrationKey: string,
- source: string,
- description: string,
- userName: string,
- ): Promise;
+ triggerAlarm(request: TriggerAlarmRequest): Promise;
}
From 6a02802d728031f19550b3cf26203e29b3e7008b Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Fri, 27 Nov 2020 16:59:16 +0100
Subject: [PATCH 36/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 ccf70abb0b777..5e67d5217221b 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
@@ -15,12 +15,12 @@
*/
import React from 'react';
-import { List, ListSubheader, LinearProgress } from '@material-ui/core';
+import { List, ListSubheader } from '@material-ui/core';
import { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
import { EscalationUser } from './EscalationUser';
import { useAsync } from 'react-use';
import { pagerDutyApiRef } from '../../api';
-import { useApi } from '@backstage/core';
+import { useApi, Progress } from '@backstage/core';
import { Alert } from '@material-ui/lab';
type Props = {
@@ -46,16 +46,18 @@ export const EscalationPolicy = ({ policyId }: Props) => {
}
if (loading) {
- return ;
+ return ;
}
- return users!.length ? (
+ if (!users?.length) {
+ return ;
+ }
+
+ return (
ON CALL}>
{users!.map((user, index) => (
))}
- ) : (
-
);
};
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx
index 2d2e5b01fd6bd..1d4ec3a1a638f 100644
--- a/plugins/pagerduty/src/components/Incident/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx
@@ -15,12 +15,12 @@
*/
import React, { useEffect } from 'react';
-import { List, ListSubheader, LinearProgress } from '@material-ui/core';
+import { List, ListSubheader } from '@material-ui/core';
import { IncidentListItem } from './IncidentListItem';
import { IncidentsEmptyState } from './IncidentEmptyState';
import { useAsyncFn } from 'react-use';
import { pagerDutyApiRef } from '../../api';
-import { useApi } from '@backstage/core';
+import { useApi, Progress } from '@backstage/core';
import { Alert } from '@material-ui/lab';
type Props = {
@@ -56,16 +56,18 @@ export const Incidents = ({
}
if (loading) {
- return ;
+ return ;
}
- return incidents?.length ? (
+ if (!incidents?.length) {
+ return ;
+ }
+
+ return (
INCIDENTS}>
{incidents!.map((incident, index) => (
))}
- ) : (
-
);
};
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index 22a471c031db9..333e042f82f6e 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -22,7 +22,6 @@ import {
CardContent,
CardHeader,
Divider,
- LinearProgress,
makeStyles,
} from '@material-ui/core';
import { Incidents } from './Incident';
@@ -35,6 +34,7 @@ import { PagerDutyIcon } from './PagerDutyIcon';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
import { MissingTokenError } from './MissingTokenError';
+import { Progress } from '@backstage/core';
const useStyles = makeStyles(theme => ({
links: {
@@ -107,7 +107,7 @@ export const PagerDutyCard = ({ entity }: Props) => {
}
if (loading) {
- return ;
+ return ;
}
const pagerdutyLink = {
diff --git a/plugins/pagerduty/src/setupTests.ts b/plugins/pagerduty/src/setupTests.ts
index d7857386de33a..0bfa67b49a755 100644
--- a/plugins/pagerduty/src/setupTests.ts
+++ b/plugins/pagerduty/src/setupTests.ts
@@ -14,5 +14,3 @@
* limitations under the License.
*/
import '@testing-library/jest-dom';
-
-global.fetch = require('node-fetch');
From d2f716d64083ad4769153bb6dd833d5e9b093604 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 30 Nov 2020 10:57:16 +0100
Subject: [PATCH 37/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 0952612d846fe..2cf1ddd8e8551 100644
--- a/.changeset/great-spiders-repair.md
+++ b/.changeset/great-spiders-repair.md
@@ -1,5 +1,4 @@
---
-'example-app': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-pagerduty': patch
---
From 49938595bcc8b54a0074bc8d34a8c8b5b4f94382 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Mon, 30 Nov 2020 18:31:35 +0100
Subject: [PATCH 38/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 b3e0b9f4e642b..7a886627e307f 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -57,7 +57,8 @@ createApiFactory({
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) =>
new PagerDutyClientApi({
- discoveryApi: discoveryApi
+ discoveryApi: discoveryApi,
+ eventUrl: "https://events.pagerduty.com/v2" //to override the default value
}),
}),
```
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
index 7b453b7298bc4..ab65514466cae 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -15,17 +15,16 @@
*/
import { createApiRef } from '@backstage/core';
+import { Service, Incident, OnCall } from '../components/types';
import {
- Service,
- Incident,
- RequestOptions,
- ClientApiConfig,
+ PagerDutyClient,
+ TriggerAlarmRequest,
ServicesResponse,
IncidentsResponse,
OnCallsResponse,
- OnCall,
-} from '../components/types';
-import { PagerDutyClient, TriggerAlarmRequest } from './types';
+ ClientApiConfig,
+ RequestOptions,
+} from './types';
export class UnauthorizedError extends Error {}
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
index 17c2067818c38..2231cc6991e9d 100644
--- a/plugins/pagerduty/src/api/types.ts
+++ b/plugins/pagerduty/src/api/types.ts
@@ -15,6 +15,7 @@
*/
import { Incident, OnCall, Service } from '../components/types';
+import { DiscoveryApi } from '@backstage/core';
export type TriggerAlarmRequest = {
integrationKey: string;
@@ -47,3 +48,26 @@ export interface PagerDutyClient {
*/
triggerAlarm(request: TriggerAlarmRequest): Promise;
}
+
+export type ServicesResponse = {
+ services: Service[];
+};
+
+export type IncidentsResponse = {
+ incidents: Incident[];
+};
+
+export type OnCallsResponse = {
+ oncalls: OnCall[];
+};
+
+export type ClientApiConfig = {
+ eventsUrl?: string;
+ discoveryApi: DiscoveryApi;
+};
+
+export type RequestOptions = {
+ method: string;
+ headers: HeadersInit;
+ body?: BodyInit;
+};
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
index 642c9ecd96e93..3847896e89fe7 100644
--- a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
@@ -27,7 +27,7 @@ import {
} from '@material-ui/core';
import Avatar from '@material-ui/core/Avatar';
import EmailIcon from '@material-ui/icons/Email';
-import PagerdutyIcon from '../PagerDutyIcon';
+import { PagerDutyIcon } from '../PagerDutyIcon';
import { User } from '../types';
const useStyles = makeStyles({
@@ -69,7 +69,7 @@ export const EscalationUser = ({ user }: Props) => {
rel="noopener noreferrer"
color="primary"
>
-
+
diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
index 7a97dad281ac7..488631b834418 100644
--- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
+++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
@@ -29,7 +29,7 @@ import {
import { StatusError, StatusWarning } from '@backstage/core';
import moment from 'moment';
import { Incident } from '../types';
-import PagerdutyIcon from '../PagerDutyIcon';
+import { PagerDutyIcon } from '../PagerDutyIcon';
const useStyles = makeStyles({
denseListIcon: {
@@ -96,7 +96,7 @@ export const IncidentListItem = ({ incident }: Props) => {
rel="noopener noreferrer"
color="primary"
>
-
+
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
index 6baa41002104f..3eef24cc6ae8c 100644
--- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
@@ -37,7 +37,7 @@ describe('Incidents', () => {
{}}
/>
,
@@ -101,7 +101,7 @@ describe('Incidents', () => {
{}}
/>
,
@@ -131,7 +131,7 @@ describe('Incidents', () => {
{}}
/>
,
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.tsx b/plugins/pagerduty/src/components/Incident/Incidents.tsx
index 1d4ec3a1a638f..4b229fb4bc699 100644
--- a/plugins/pagerduty/src/components/Incident/Incidents.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.tsx
@@ -25,27 +25,21 @@ import { Alert } from '@material-ui/lab';
type Props = {
serviceId: string;
- shouldRefreshIncidents: boolean;
- setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void;
+ refreshIncidents: Boolean;
};
-export const Incidents = ({
- serviceId,
- shouldRefreshIncidents,
- setShouldRefreshIncidents,
-}: Props) => {
+export const Incidents = ({ serviceId, refreshIncidents }: Props) => {
const api = useApi(pagerDutyApiRef);
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
async () => {
- setShouldRefreshIncidents(false);
return await api.getIncidentsByServiceId(serviceId);
},
);
useEffect(() => {
getIncidents();
- }, [shouldRefreshIncidents, getIncidents]);
+ }, [refreshIncidents, getIncidents]);
if (error) {
return (
diff --git a/plugins/pagerduty/src/components/PagerDutyIcon.tsx b/plugins/pagerduty/src/components/PagerDutyIcon.tsx
index 36f6e1989d4ff..67cfc0036ae73 100644
--- a/plugins/pagerduty/src/components/PagerDutyIcon.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyIcon.tsx
@@ -17,7 +17,7 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
-const SvgPd = (props: SvgIconProps) =>
+export const PagerDutyIcon = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
@@ -25,5 +25,3 @@ const SvgPd = (props: SvgIconProps) =>
,
);
-
-export default SvgPd;
diff --git a/plugins/pagerduty/src/components/PagerdutyCard.tsx b/plugins/pagerduty/src/components/PagerdutyCard.tsx
index 333e042f82f6e..ac309e8a95de2 100644
--- a/plugins/pagerduty/src/components/PagerdutyCard.tsx
+++ b/plugins/pagerduty/src/components/PagerdutyCard.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState } from 'react';
-import { useApi } from '@backstage/core';
+import { useApi, Progress } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
Button,
@@ -34,7 +34,6 @@ import { PagerDutyIcon } from './PagerDutyIcon';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
import { MissingTokenError } from './MissingTokenError';
-import { Progress } from '@backstage/core';
const useStyles = makeStyles(theme => ({
links: {
@@ -72,9 +71,7 @@ export const PagerDutyCard = ({ entity }: Props) => {
const classes = useStyles();
const api = useApi(pagerDutyApiRef);
const [showDialog, setShowDialog] = useState(false);
- const [shouldRefreshIncidents, setShouldRefreshIncidents] = useState(
- false,
- );
+ const [refreshIncidents, setRefreshIncidents] = useState(false);
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
@@ -129,6 +126,10 @@ export const PagerDutyCard = ({ entity }: Props) => {
),
};
+ const onTriggerRefresh = () => {
+ setRefreshIncidents(true);
+ };
+
return (
{
{
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
- setShouldRefreshIncidents={setShouldRefreshIncidents}
+ onTriggerRefresh={onTriggerRefresh}
/>
diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
index 440621dcf6f3c..3d2a48b419021 100644
--- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -71,7 +71,7 @@ describe('TriggerDialog', () => {
handleDialog={() => {}}
name={entity.metadata.name}
integrationKey="abc123"
- setShouldRefreshIncidents={() => {}}
+ onTriggerRefresh={() => {}}
/>
,
),
@@ -93,11 +93,13 @@ describe('TriggerDialog', () => {
fireEvent.click(triggerButton);
});
expect(mockTriggerAlarmFn).toHaveBeenCalled();
- expect(mockTriggerAlarmFn).toHaveBeenCalledWith(
- entity!.metadata!.annotations!['pagerduty.com/integration-key'],
- window.location.toString(),
+ expect(mockTriggerAlarmFn).toHaveBeenCalledWith({
+ integrationKey: entity!.metadata!.annotations![
+ 'pagerduty.com/integration-key'
+ ],
+ source: window.location.toString(),
description,
- 'guest@example.com',
- );
+ userName: 'guest@example.com',
+ });
});
});
diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
index e6bb6d107d6e3..4717b8fece059 100644
--- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx
@@ -35,7 +35,7 @@ type Props = {
integrationKey: string;
showDialog: boolean;
handleDialog: () => void;
- setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void;
+ onTriggerRefresh: () => void;
};
export const TriggerDialog = ({
@@ -43,22 +43,22 @@ export const TriggerDialog = ({
integrationKey,
showDialog,
handleDialog,
- setShouldRefreshIncidents,
+ onTriggerRefresh,
}: Props) => {
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
- const userId = identityApi.getUserId();
+ const userName = identityApi.getUserId();
const api = useApi(pagerDutyApiRef);
const [description, setDescription] = useState('');
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
- async (desc: string) =>
- await api.triggerAlarm(
+ async (description: string) =>
+ await api.triggerAlarm({
integrationKey,
- window.location.toString(),
- desc,
- userId,
- ),
+ source: window.location.toString(),
+ description,
+ userName,
+ }),
);
const descriptionChanged = (
@@ -70,9 +70,9 @@ export const TriggerDialog = ({
useEffect(() => {
if (value) {
alertApi.post({
- message: `Alarm successfully triggered by ${userId}`,
+ message: `Alarm successfully triggered by ${userName}`,
});
- setShouldRefreshIncidents(true);
+ onTriggerRefresh();
handleDialog();
}
@@ -82,7 +82,7 @@ export const TriggerDialog = ({
severity: 'error',
});
}
- }, [value, error, alertApi, handleDialog, userId, setShouldRefreshIncidents]);
+ }, [value, error, alertApi, handleDialog, userName, onTriggerRefresh]);
if (!showDialog) {
return null;
diff --git a/plugins/pagerduty/src/components/types.tsx b/plugins/pagerduty/src/components/types.ts
similarity index 74%
rename from plugins/pagerduty/src/components/types.tsx
rename to plugins/pagerduty/src/components/types.ts
index 5e5ee0a61be47..a93791fe8e050 100644
--- a/plugins/pagerduty/src/components/types.tsx
+++ b/plugins/pagerduty/src/components/types.ts
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-import { DiscoveryApi } from '@backstage/core';
-
export type Incident = {
id: string;
title: string;
@@ -58,26 +56,3 @@ export type User = {
html_url: string;
name: string;
};
-
-export type ServicesResponse = {
- services: Service[];
-};
-
-export type IncidentsResponse = {
- incidents: Incident[];
-};
-
-export type OnCallsResponse = {
- oncalls: OnCall[];
-};
-
-export type RequestOptions = {
- method: string;
- headers: HeadersInit;
- body?: BodyInit;
-};
-
-export type ClientApiConfig = {
- eventsUrl?: string;
- discoveryApi: DiscoveryApi;
-};
From 04a53de52fdd9424d35bd398904ff21989853153 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 1 Dec 2020 13:46:46 +0100
Subject: [PATCH 39/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 7a886627e307f..b925e9d7a767f 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -56,7 +56,7 @@ createApiFactory({
api: pagerDutyApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) =>
- new PagerDutyClientApi({
+ new PagerDutyClient({
discoveryApi: discoveryApi,
eventUrl: "https://events.pagerduty.com/v2" //to override the default value
}),
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
index ab65514466cae..214e7d57a74ef 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -17,7 +17,7 @@
import { createApiRef } from '@backstage/core';
import { Service, Incident, OnCall } from '../components/types';
import {
- PagerDutyClient,
+ PagerDutyApi,
TriggerAlarmRequest,
ServicesResponse,
IncidentsResponse,
@@ -28,12 +28,12 @@ import {
export class UnauthorizedError extends Error {}
-export const pagerDutyApiRef = createApiRef({
+export const pagerDutyApiRef = createApiRef({
id: 'plugin.pagerduty.api',
description: 'Used to fetch data from PagerDuty API',
});
-export class PagerDutyClientApi implements PagerDutyClient {
+export class PagerDutyClient implements PagerDutyApi {
constructor(private readonly config: ClientApiConfig) {}
async getServiceByIntegrationKey(integrationKey: string): Promise {
diff --git a/plugins/pagerduty/src/api/index.ts b/plugins/pagerduty/src/api/index.ts
index 5ac7470475b49..90604c40126be 100644
--- a/plugins/pagerduty/src/api/index.ts
+++ b/plugins/pagerduty/src/api/index.ts
@@ -14,9 +14,5 @@
* limitations under the License.
*/
-export {
- PagerDutyClientApi,
- pagerDutyApiRef,
- UnauthorizedError,
-} from './client';
-export type { PagerDutyClient } from './types';
+export { PagerDutyClient, pagerDutyApiRef, UnauthorizedError } from './client';
+export type { PagerDutyApi } from './types';
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
index 2231cc6991e9d..4b3ae74cc18e8 100644
--- a/plugins/pagerduty/src/api/types.ts
+++ b/plugins/pagerduty/src/api/types.ts
@@ -24,7 +24,7 @@ export type TriggerAlarmRequest = {
userName: string;
};
-export interface PagerDutyClient {
+export interface PagerDutyApi {
/**
* Fetches a list of services, filtered by the provided integration key.
*
diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
index 3eef24cc6ae8c..3c5e30e0ccfa8 100644
--- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
+++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx
@@ -35,11 +35,7 @@ describe('Incidents', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
- {}}
- />
+
,
),
);
@@ -99,11 +95,7 @@ describe('Incidents', () => {
} = render(
wrapInTestApp(
- {}}
- />
+
,
),
);
@@ -129,11 +121,7 @@ describe('Incidents', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
- {}}
- />
+
,
),
);
diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
index bca6f9f940fd6..26d67a55e4065 100644
--- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react';
-import { PagerDutyCard } from './PagerdutyCard';
+import { PagerDutyCard } from './PagerDutyCard';
import { Entity } from '@backstage/catalog-model';
import { wrapInTestApp } from '@backstage/test-utils';
import {
diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts
index 1e8e549ac92f2..4ecd4edcc63a2 100644
--- a/plugins/pagerduty/src/index.ts
+++ b/plugins/pagerduty/src/index.ts
@@ -17,9 +17,9 @@ export { plugin } from './plugin';
export {
isPluginApplicableToEntity,
PagerDutyCard,
-} from './components/PagerdutyCard';
+} from './components/PagerDutyCard';
export {
- PagerDutyClientApi,
+ PagerDutyClient,
pagerDutyApiRef,
UnauthorizedError,
} from './api/client';
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
index 02b11ed773106..4e8f429b059d0 100644
--- a/plugins/pagerduty/src/plugin.ts
+++ b/plugins/pagerduty/src/plugin.ts
@@ -19,7 +19,7 @@ import {
createRouteRef,
discoveryApiRef,
} from '@backstage/core';
-import { pagerDutyApiRef, PagerDutyClientApi } from './api';
+import { pagerDutyApiRef, PagerDutyClient } from './api';
export const rootRouteRef = createRouteRef({
path: '/pagerduty',
@@ -33,7 +33,7 @@ export const plugin = createPlugin({
api: pagerDutyApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) =>
- new PagerDutyClientApi({
+ new PagerDutyClient({
discoveryApi: discoveryApi,
}),
}),
From 3d392a0b6acfe0852d8c7eb83200dc8d276e5a48 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 1 Dec 2020 16:38:40 +0100
Subject: [PATCH 40/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 18eeb24400845..e80b5df0213ac 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -312,3 +312,5 @@ homepage:
timezone: 'Europe/Stockholm'
- label: TYO
timezone: 'Asia/Tokyo'
+pagerduty:
+ eventsBaseUrl: 'https://events.pagerduty.com/v2'
diff --git a/packages/app/package.json b/packages/app/package.json
index b0499c8431ab9..f78a3474ff07b 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -21,25 +21,25 @@
"@backstage/plugin-kubernetes": "^0.3.0",
"@backstage/plugin-lighthouse": "^0.2.3",
"@backstage/plugin-newrelic": "^0.2.1",
+ "@backstage/plugin-pagerduty": "0.2.1",
"@backstage/plugin-register-component": "^0.2.2",
"@backstage/plugin-rollbar": "^0.2.3",
"@backstage/plugin-scaffolder": "^0.3.1",
- "@backstage/plugin-sentry": "^0.2.3",
"@backstage/plugin-search": "^0.2.1",
+ "@backstage/plugin-sentry": "^0.2.3",
"@backstage/plugin-tech-radar": "^0.3.0",
"@backstage/plugin-techdocs": "^0.2.3",
"@backstage/plugin-user-settings": "^0.2.2",
"@backstage/plugin-welcome": "^0.2.1",
"@backstage/test-utils": "^0.1.3",
- "@backstage/plugin-pagerduty": "^0.2.1",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
+ "@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.2.15",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.8",
- "@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index 3fdf236e8d686..d70694969ee3b 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -38,6 +38,6 @@ export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
-export { plugin as Pagerduty } from '@backstage/plugin-pagerduty';
+export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md
index b925e9d7a767f..1c22053b889b5 100644
--- a/plugins/pagerduty/README.md
+++ b/plugins/pagerduty/README.md
@@ -47,20 +47,13 @@ import {
## Client configuration
-The client takes a config object which contains an instance of the `discoveryApi` which is used to look up the `baseUrl` of the `proxy`, and a `eventUrl` which is defaulted to: "https://events.pagerduty.com/v2".
+If you want to override the default URL for events, you can add it to `app-config.yaml`.
-In `apis.ts`:
+In `app-config.yaml`:
-```ts
-createApiFactory({
- api: pagerDutyApiRef,
- deps: { discoveryApi: discoveryApiRef },
- factory: ({ discoveryApi }) =>
- new PagerDutyClient({
- discoveryApi: discoveryApi,
- eventUrl: "https://events.pagerduty.com/v2" //to override the default value
- }),
-}),
+```yaml
+pagerduty:
+ eventsBaseUrl: 'https://events.pagerduty.com/v2'
```
## Providing the API Token
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
index 214e7d57a74ef..99e21ef6d366a 100644
--- a/plugins/pagerduty/src/api/client.ts
+++ b/plugins/pagerduty/src/api/client.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { createApiRef } from '@backstage/core';
+import { createApiRef, DiscoveryApi, ConfigApi } from '@backstage/core';
import { Service, Incident, OnCall } from '../components/types';
import {
PagerDutyApi,
@@ -34,6 +34,15 @@ export const pagerDutyApiRef = createApiRef({
});
export class PagerDutyClient implements PagerDutyApi {
+ static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
+ const eventsBaseUrl: string =
+ configApi.getOptionalString('pagerDuty.eventsBaseUrl') ??
+ 'https://events.pagerduty.com/v2';
+ return new PagerDutyClient({
+ eventsBaseUrl,
+ discoveryApi,
+ });
+ }
constructor(private readonly config: ClientApiConfig) {}
async getServiceByIntegrationKey(integrationKey: string): Promise {
@@ -98,7 +107,8 @@ export class PagerDutyClient implements PagerDutyApi {
};
return this.request(
- `${this.config.eventsUrl ?? 'https://events.pagerduty.com/v2'}/enqueue`,
+ `${this.config.eventsBaseUrl ??
+ 'https://events.pagerduty.com/v2'}/enqueue`,
options,
);
}
diff --git a/plugins/pagerduty/src/api/types.ts b/plugins/pagerduty/src/api/types.ts
index 4b3ae74cc18e8..733f171489bef 100644
--- a/plugins/pagerduty/src/api/types.ts
+++ b/plugins/pagerduty/src/api/types.ts
@@ -62,7 +62,7 @@ export type OnCallsResponse = {
};
export type ClientApiConfig = {
- eventsUrl?: string;
+ eventsBaseUrl?: string;
discoveryApi: DiscoveryApi;
};
diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
index 26d67a55e4065..34ddb63f71386 100644
--- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
+++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
-import { render, waitFor, fireEvent } from '@testing-library/react';
+import { render, waitFor, fireEvent, act } from '@testing-library/react';
import { PagerDutyCard } from './PagerDutyCard';
import { Entity } from '@backstage/catalog-model';
import { wrapInTestApp } from '@backstage/test-utils';
@@ -26,7 +26,6 @@ import {
} from '@backstage/core';
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api';
import { Service } from './types';
-import { act } from 'react-dom/test-utils';
const mockPagerDutyApi: Partial = {
getServiceByIntegrationKey: async () => [],
diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
index 3d2a48b419021..7ef57d67b474c 100644
--- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
+++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, act } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import {
ApiRegistry,
@@ -26,7 +26,6 @@ import {
} from '@backstage/core';
import { pagerDutyApiRef } from '../../api';
import { Entity } from '@backstage/catalog-model';
-import { act } from 'react-dom/test-utils';
import { TriggerDialog } from './TriggerDialog';
describe('TriggerDialog', () => {
diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts
index 4e8f429b059d0..34796f491ec0c 100644
--- a/plugins/pagerduty/src/plugin.ts
+++ b/plugins/pagerduty/src/plugin.ts
@@ -18,6 +18,7 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
+ configApiRef,
} from '@backstage/core';
import { pagerDutyApiRef, PagerDutyClient } from './api';
@@ -31,11 +32,9 @@ export const plugin = createPlugin({
apis: [
createApiFactory({
api: pagerDutyApiRef,
- deps: { discoveryApi: discoveryApiRef },
- factory: ({ discoveryApi }) =>
- new PagerDutyClient({
- discoveryApi: discoveryApi,
- }),
+ deps: { discoveryApi: discoveryApiRef, configApi: configApiRef },
+ factory: ({ configApi, discoveryApi }) =>
+ PagerDutyClient.fromConfig(configApi, discoveryApi),
}),
],
});
From fa25d9f448934593c32ee43c80fe41f9252ed1c0 Mon Sep 17 00:00:00 2001
From: Samira Mokaram
Date: Tue, 1 Dec 2020 16:58:35 +0100
Subject: [PATCH 41/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 0000000000000..ac309e8a95de2
--- /dev/null
+++ b/plugins/pagerduty/src/components/PagerDutyCard.tsx
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useState } from 'react';
+import { useApi, Progress } from '@backstage/core';
+import { Entity } from '@backstage/catalog-model';
+import {
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ Divider,
+ makeStyles,
+} from '@material-ui/core';
+import { Incidents } from './Incident';
+import { EscalationPolicy } from './Escalation';
+import { useAsync } from 'react-use';
+import { Alert } from '@material-ui/lab';
+import { pagerDutyApiRef, UnauthorizedError } from '../api';
+import { IconLinkVertical } from '@backstage/plugin-catalog';
+import { PagerDutyIcon } from './PagerDutyIcon';
+import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
+import { TriggerDialog } from './TriggerDialog';
+import { MissingTokenError } from './MissingTokenError';
+
+const useStyles = makeStyles(theme => ({
+ links: {
+ margin: theme.spacing(2, 0),
+ display: 'grid',
+ gridAutoFlow: 'column',
+ gridAutoColumns: 'min-content',
+ gridGap: theme.spacing(3),
+ },
+ triggerAlarm: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ fontSize: '0.7rem',
+ textTransform: 'uppercase',
+ fontWeight: 600,
+ letterSpacing: 1.2,
+ lineHeight: 1.5,
+ '&:hover, &:focus, &.focus': {
+ backgroundColor: 'transparent',
+ textDecoration: 'none',
+ },
+ },
+}));
+
+export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
+
+export const isPluginApplicableToEntity = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
+
+type Props = {
+ entity: Entity;
+};
+
+export const PagerDutyCard = ({ entity }: Props) => {
+ const classes = useStyles();
+ const api = useApi(pagerDutyApiRef);
+ const [showDialog, setShowDialog] = useState(false);
+ const [refreshIncidents, setRefreshIncidents] = useState(false);
+ const integrationKey = entity.metadata.annotations![
+ PAGERDUTY_INTEGRATION_KEY
+ ];
+
+ const { value: service, loading, error } = useAsync(async () => {
+ const services = await api.getServiceByIntegrationKey(integrationKey);
+
+ return {
+ id: services[0].id,
+ name: services[0].name,
+ url: services[0].html_url,
+ policyId: services[0].escalation_policy.id,
+ };
+ });
+
+ const handleDialog = () => {
+ setShowDialog(!showDialog);
+ };
+
+ if (error instanceof UnauthorizedError) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ const pagerdutyLink = {
+ title: 'View in PagerDuty',
+ href: service!.url,
+ };
+
+ const triggerAlarm = {
+ title: 'Trigger Alarm',
+ action: (
+
+ 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 2fce2ebef321a..64cc5a089362a 100644
--- a/plugins/pagerduty/package.json
+++ b/plugins/pagerduty/package.json
@@ -20,21 +20,21 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/core": "^0.3.0",
- "@backstage/theme": "^0.2.1",
"@backstage/catalog-model": "^0.2.0",
- "@backstage/test-utils": "^0.1.2",
+ "@backstage/core": "^0.3.0",
"@backstage/plugin-catalog": "^0.2.1",
+ "@backstage/test-utils": "^0.1.2",
+ "@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
+ "date-fns": "^2.16.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^15.3.3",
- "moment": "^2.27.0"
+ "react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.2.0",
+ "@backstage/cli": "^0.3.1",
"@backstage/dev-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
index 488631b834418..7a34501325d36 100644
--- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
+++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
@@ -27,7 +27,7 @@ import {
Typography,
} from '@material-ui/core';
import { StatusError, StatusWarning } from '@backstage/core';
-import moment from 'moment';
+import { formatDistanceToNowStrict } from 'date-fns';
import { Incident } from '../types';
import { PagerDutyIcon } from '../PagerDutyIcon';
@@ -54,7 +54,7 @@ type Props = {
export const IncidentListItem = ({ incident }: Props) => {
const classes = useStyles();
const user = incident.assignments[0]?.assignee;
- const createdAt = moment(incident.created_at).fromNow();
+ const createdAt = formatDistanceToNowStrict(new Date(incident.created_at));
return (
@@ -77,7 +77,7 @@ export const IncidentListItem = ({ incident }: Props) => {
}}
secondary={
- Created {createdAt} and assigned to{' '}
+ Created {createdAt} ago and assigned to{' '}
Date: 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 49012764e2e29..0ce569171ff0a 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 94ba260d5573e..aaa110329ea29 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 0000000000000..8fb346b831845
--- /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 54d564de7dbf3..2850145eefbf9 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 014e4ac934521..e13c00e561a04 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 1f18f54d2a7fa..58cf50fca0929 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 2850145eefbf9..7fc041bf13932 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 0000000000000..0ac7d8f299c83
--- /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 0ac7d8f299c83..71afaeb44ac5d 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 e13c00e561a04..a6f95ae71c279 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 58cf50fca0929..d044b50b7aaf9 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 c2eb935e54ffe..8f494458fd1e0 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 0000000000000..bb1100160113b
--- /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 4a8bf2a0abc6c..dd267dde35353 100644
--- a/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
+++ b/plugins/catalog/src/components/AboutCard/IconLinkVertical/IconLinkVertical.tsx
@@ -25,10 +25,9 @@ export type IconLinkVerticalProps = {
disabled?: boolean;
title?: string;
label: string;
- action?: React.ReactNode;
};
-const useIconStyles = makeStyles(theme => ({
+const useIconStyles = makeStyles({
link: {
display: 'grid',
justifyItems: 'center',
@@ -44,16 +43,12 @@ const useIconStyles = makeStyles(theme => ({
fontWeight: 600,
letterSpacing: 1.2,
},
- linkStyle: {
- color: theme.palette.secondary.main,
- },
-}));
+});
export function IconLinkVertical({
icon = ,
href = '#',
disabled = false,
- action,
...props
}: IconLinkVerticalProps) {
const classes = useIconStyles();
@@ -72,23 +67,6 @@ export function IconLinkVertical({
);
}
- if (action) {
- return (
-
- {icon}
- {action}
-
- );
- }
-
// Absolute links should not be using RouterLink
if (href?.startsWith('//') || href?.includes('://')) {
return (
From 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 692a3c6dd8804..278fb70eb3f9d 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 8f494458fd1e0..339e7c8dd63a5 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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