diff --git a/.changeset/great-spiders-repair.md b/.changeset/great-spiders-repair.md
new file mode 100644
index 0000000000000..2cf1ddd8e8551
--- /dev/null
+++ b/.changeset/great-spiders-repair.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-pagerduty': patch
+---
+
+Added pagerduty plugin to example app
diff --git a/app-config.yaml b/app-config.yaml
index 7fc041bf13932..236fc8e3b735b 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -46,6 +46,12 @@ proxy:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
+ '/pagerduty':
+ target: https://api.pagerduty.com
+ headers:
+ Authorization:
+ $env: PAGERDUTY_TOKEN
+
'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
@@ -310,3 +316,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 fd6667ac85fc7..afe35ff3821d4 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -22,6 +22,7 @@
"@backstage/plugin-kubernetes": "^0.3.1",
"@backstage/plugin-lighthouse": "^0.2.4",
"@backstage/plugin-newrelic": "^0.2.1",
+ "@backstage/plugin-pagerduty": "0.2.1",
"@backstage/plugin-register-component": "^0.2.3",
"@backstage/plugin-rollbar": "^0.2.5",
"@backstage/plugin-scaffolder": "^0.3.2",
@@ -36,10 +37,10 @@
"@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.16",
"@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/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 068019c6222d8..13cbe324c6efb 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -67,6 +67,10 @@ import {
PullRequestsStatsCard,
Router as PullRequestsRouter,
} from '@roadiehq/backstage-plugin-github-pull-requests';
+import {
+ isPluginApplicableToEntity as isPagerDutyAvailable,
+ PagerDutyCard,
+} from '@backstage/plugin-pagerduty';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
@@ -142,6 +146,11 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
+ {isPagerDutyAvailable(entity) && (
+
+
+
+ )}
{isGitHubAvailable(entity) && (
<>
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index a02365d17d6de..c6a2a0e7e08bb 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -39,5 +39,6 @@ export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
+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/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 606ad9028dc44..5949209128843 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -15,7 +15,7 @@
*/
export * from '@backstage/catalog-client';
-export { AboutCard } from './components/AboutCard';
+export { AboutCard, IconLinkVertical } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
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..1c22053b889b5
--- /dev/null
+++ b/plugins/pagerduty/README.md
@@ -0,0 +1,82 @@
+# PagerDuty
+
+## Overview
+
+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.
+
+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.
+
+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
+
+If you want to override the default URL for events, you can add it to `app-config.yaml`.
+
+In `app-config.yaml`:
+
+```yaml
+pagerduty:
+ eventsBaseUrl: '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]
+```
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..4d58a1da42ffa
--- /dev/null
+++ b/plugins/pagerduty/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@backstage/plugin-pagerduty",
+ "version": "0.2.1",
+ "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/catalog-model": "^0.4.0",
+ "@backstage/core": "^0.3.2",
+ "@backstage/theme": "^0.2.1",
+ "@material-ui/core": "^4.11.0",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "classnames": "^2.2.6",
+ "date-fns": "^2.15.0",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-router-dom": "6.0.0-beta.0",
+ "react-use": "^15.3.3"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.4.0",
+ "@backstage/dev-utils": "^0.1.5",
+ "@backstage/test-utils": "^0.1.4",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^12.0.0",
+ "msw": "^0.21.2",
+ "node-fetch": "^2.6.1",
+ "cross-fetch": "^3.0.6"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts
new file mode 100644
index 0000000000000..527a56b2af4ab
--- /dev/null
+++ b/plugins/pagerduty/src/api/client.ts
@@ -0,0 +1,143 @@
+/*
+ * 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 { createApiRef, DiscoveryApi, ConfigApi } from '@backstage/core';
+import { Service, Incident, OnCall } from '../components/types';
+import {
+ PagerDutyApi,
+ TriggerAlarmRequest,
+ ServicesResponse,
+ IncidentsResponse,
+ OnCallsResponse,
+ ClientApiConfig,
+ RequestOptions,
+} from './types';
+
+export class UnauthorizedError extends Error {}
+
+export const pagerDutyApiRef = createApiRef({
+ id: 'plugin.pagerduty.api',
+ description: 'Used to fetch data from PagerDuty API',
+});
+
+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 {
+ const params = `include[]=integrations&include[]=escalation_policies&query=${integrationKey}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/services?${params}`;
+ const { services } = await this.getByUrl(url);
+
+ return services;
+ }
+
+ async getIncidentsByServiceId(serviceId: string): Promise {
+ const params = `statuses[]=triggered&statuses[]=acknowledged&service_ids[]=${serviceId}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/incidents?${params}`;
+ const { incidents } = await this.getByUrl(url);
+
+ return incidents;
+ }
+
+ async getOnCallByPolicyId(policyId: string): Promise {
+ const params = `include[]=users&escalation_policy_ids[]=${policyId}`;
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/pagerduty/oncalls?${params}`;
+ const { oncalls } = await this.getByUrl(url);
+
+ return oncalls;
+ }
+
+ triggerAlarm({
+ integrationKey,
+ source,
+ description,
+ userName,
+ }: TriggerAlarmRequest): Promise {
+ const 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,
+ },
+ },
+ });
+
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ Accept: 'application/json, text/plain, */*',
+ },
+ body,
+ };
+
+ const url = this.config.eventsBaseUrl ?? 'https://events.pagerduty.com/v2';
+
+ return this.request(`${url}/enqueue`, options);
+ }
+
+ private async getByUrl(url: string): Promise {
+ const options = {
+ method: 'GET',
+ headers: {
+ Accept: 'application/vnd.pagerduty+json;version=2',
+ 'Content-Type': 'application/json',
+ },
+ };
+ const response = await this.request(url, options);
+
+ return response.json();
+ }
+
+ private async request(
+ url: string,
+ options: RequestOptions,
+ ): Promise {
+ 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..90604c40126be
--- /dev/null
+++ b/plugins/pagerduty/src/api/index.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 { 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
new file mode 100644
index 0000000000000..733f171489bef
--- /dev/null
+++ b/plugins/pagerduty/src/api/types.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.
+ */
+
+import { Incident, OnCall, Service } from '../components/types';
+import { DiscoveryApi } from '@backstage/core';
+
+export type TriggerAlarmRequest = {
+ integrationKey: string;
+ source: string;
+ description: string;
+ userName: string;
+};
+
+export interface PagerDutyApi {
+ /**
+ * 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;
+
+ /**
+ * Triggers an incident to whoever is on-call.
+ */
+ triggerAlarm(request: TriggerAlarmRequest): Promise;
+}
+
+export type ServicesResponse = {
+ services: Service[];
+};
+
+export type IncidentsResponse = {
+ incidents: Incident[];
+};
+
+export type OnCallsResponse = {
+ oncalls: OnCall[];
+};
+
+export type ClientApiConfig = {
+ eventsBaseUrl?: string;
+ discoveryApi: DiscoveryApi;
+};
+
+export type RequestOptions = {
+ method: string;
+ headers: HeadersInit;
+ body?: BodyInit;
+};
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/About/AboutCard.tsx b/plugins/pagerduty/src/components/About/AboutCard.tsx
new file mode 100644
index 0000000000000..7d5419908a02d
--- /dev/null
+++ b/plugins/pagerduty/src/components/About/AboutCard.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { Card, CardContent, CardHeader, Divider } from '@material-ui/core';
+import { SubHeader } from './SubHeader';
+import { SubHeaderLink } from '../types';
+
+type Props = {
+ title: string;
+ links: SubHeaderLink[];
+ content: React.ReactNode;
+};
+
+export const AboutCard = ({ title, links, content }: Props) => (
+
+ } />
+
+ {content}
+
+);
diff --git a/plugins/pagerduty/src/components/About/SubHeader.tsx b/plugins/pagerduty/src/components/About/SubHeader.tsx
new file mode 100644
index 0000000000000..5e4ea96fe0b44
--- /dev/null
+++ b/plugins/pagerduty/src/components/About/SubHeader.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { VerticalIcon } from './VerticalIcon';
+import { SubHeaderLink } from '../types';
+import { makeStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ links: {
+ margin: theme.spacing(2, 0),
+ display: 'grid',
+ gridAutoFlow: 'column',
+ gridAutoColumns: 'min-content',
+ gridGap: theme.spacing(3),
+ },
+}));
+
+type Props = {
+ links: SubHeaderLink[];
+};
+
+export const SubHeader = ({ links }: Props) => {
+ const classes = useStyles();
+ return (
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/About/VerticalIcon.tsx b/plugins/pagerduty/src/components/About/VerticalIcon.tsx
new file mode 100644
index 0000000000000..5463372ac0bae
--- /dev/null
+++ b/plugins/pagerduty/src/components/About/VerticalIcon.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import classnames from 'classnames';
+import { makeStyles, Link } from '@material-ui/core';
+import LinkIcon from '@material-ui/icons/Link';
+import { Link as RouterLink } from 'react-router-dom';
+
+export type VerticalIconProps = {
+ icon?: React.ReactNode;
+ href?: string;
+ title?: string;
+ label: string;
+ action?: React.ReactNode;
+};
+
+const useIconStyles = makeStyles(theme => ({
+ link: {
+ display: 'grid',
+ justifyItems: 'center',
+ gridGap: 4,
+ textAlign: 'center',
+ },
+ label: {
+ fontSize: '0.7rem',
+ textTransform: 'uppercase',
+ fontWeight: 600,
+ letterSpacing: 1.2,
+ },
+ linkStyle: {
+ color: theme.palette.secondary.main,
+ },
+}));
+
+export function VerticalIcon({
+ icon = ,
+ href = '#',
+ action,
+ ...props
+}: VerticalIconProps) {
+ const classes = useIconStyles();
+
+ if (action) {
+ return (
+
+ {icon}
+ {action}
+
+ );
+ }
+
+ // Absolute links should not be using RouterLink
+ if (href?.startsWith('//') || href?.includes('://')) {
+ return (
+
+ {icon}
+ {props.label}
+
+ );
+ }
+
+ return (
+
+ {icon}
+ {props.label}
+
+ );
+}
diff --git a/plugins/pagerduty/src/components/About/index.ts b/plugins/pagerduty/src/components/About/index.ts
new file mode 100644
index 0000000000000..4e495daa541c2
--- /dev/null
+++ b/plugins/pagerduty/src/components/About/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { AboutCard } from './AboutCard';
+// export { SubHeader } from './SubHeader';
+// export { VerticalIcon } from './VerticalIcon';
diff --git a/plugins/pagerduty/src/components/Errors/MissingTokenError.tsx b/plugins/pagerduty/src/components/Errors/MissingTokenError.tsx
new file mode 100644
index 0000000000000..c22552b7c6b7c
--- /dev/null
+++ b/plugins/pagerduty/src/components/Errors/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/Errors/index.ts b/plugins/pagerduty/src/components/Errors/index.ts
new file mode 100644
index 0000000000000..3c2dfa65f2fcc
--- /dev/null
+++ b/plugins/pagerduty/src/components/Errors/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { MissingTokenError } from './MissingTokenError';
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..15ff3278a0cad
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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 } 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 mockPagerDutyApi = {
+ getOnCallByPolicyId: () => [],
+};
+const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]);
+
+describe('Escalation', () => {
+ 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 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/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
new file mode 100644
index 0000000000000..5e67d5217221b
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
+import { EscalationUser } from './EscalationUser';
+import { useAsync } from 'react-use';
+import { pagerDutyApiRef } from '../../api';
+import { useApi, Progress } from '@backstage/core';
+import { Alert } from '@material-ui/lab';
+
+type Props = {
+ policyId: string;
+};
+
+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 ;
+ }
+
+ if (!users?.length) {
+ return ;
+ }
+
+ return (
+ ON CALL}>
+ {users!.map((user, index) => (
+
+ ))}
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
new file mode 100644
index 0000000000000..41995c86f5835
--- /dev/null
+++ b/plugins/pagerduty/src/components/Escalation/EscalationUser.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+ Typography,
+} from '@material-ui/core';
+import Avatar from '@material-ui/core/Avatar';
+import EmailIcon from '@material-ui/icons/Email';
+import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
+import { User } from '../types';
+
+const useStyles = makeStyles({
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
+});
+
+type Props = {
+ user: User;
+};
+
+export const EscalationUser = ({ user }: Props) => {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+ {user.name}
+
+ }
+ secondary={user.email}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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/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/IncidentEmptyState.tsx b/plugins/pagerduty/src/components/Incident/IncidentEmptyState.tsx
new file mode 100644
index 0000000000000..b697422b0f91d
--- /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, Typography } from '@material-ui/core';
+import EmptyStateImage from '../../assets/emptystate.svg';
+
+export const IncidentsEmptyState = () => {
+ return (
+
+
+ Nice! No incidents found!
+
+
+
+
+
+ );
+};
diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
new file mode 100644
index 0000000000000..860623dcd4733
--- /dev/null
+++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+ Link,
+ Typography,
+} from '@material-ui/core';
+import { StatusError, StatusWarning } from '@backstage/core';
+import { formatDistanceToNowStrict } from 'date-fns';
+import { Incident } from '../types';
+import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
+
+const useStyles = makeStyles({
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
+ listItemIcon: {
+ minWidth: '1em',
+ },
+});
+
+type Props = {
+ incident: Incident;
+};
+
+export const IncidentListItem = ({ incident }: Props) => {
+ const classes = useStyles();
+ const user = incident.assignments[0]?.assignee;
+ const createdAt = formatDistanceToNowStrict(new Date(incident.created_at));
+
+ return (
+
+
+
+