+
+The PagerDuty Incidents plugin shows PagerDuty incidents associated with the PagerDuty service specified in the entity's `cortex.yaml`. If the `cortex.yaml` has a PagerDuty service defined in its `x-cortex-oncall` configuration, it will query for incidents in that service. For example:
+
+```yaml
+openapi: 3.0.1
+info:
+ title: funrepo
+ description: Description of funrepo
+ x-cortex-tag: funrepo
+ x-cortex-type: service
+ x-cortex-oncall:
+ pagerduty:
+ id: PXXXXXX
+ type: SERVICE
+```
+
+## Setup
+
+This plugin requires a proxy to PagerDuty. To set up:
+
+- In PagerDuty, create an API Token by clicking on Integrations > API Access Keys > Create New API Key
+- In Cortex, define a secret whose value is your new token. Name it `pagerduty_secret`.
+- Create a plugin proxy in Cortex:
+
+ - Navigate to Plugins, then click on the Proxies tab, then click on Create Proxy
+ - Give the proxy a name, then click on Add URL
+ - For the URL Prefix, type in `https://api.pagerduty.com`
+ - Click on Add Header and add a header whose name is `Authorization` and whose value is `Token token={{{secrets.pagerduty_secret}}}` (include the curly braces!)
+
+- Once you are done, the proxy should look like the below:
+
+
+
+Now, you can build and add the plugin.
+
+- Build the plugin:
+ - Make sure you have npm/yarn
+ - In your terminal, in the `pagerduty-incidents` directory, type `yarn` or `npm install` to install the dependencies
+ - Type `npm run build` or `yarn build` to build the plugin
+- The compiled plugin will be created in `dist/ui.html`
+- In Plugins > All, click **Register Plugin**
+- Give the plugin a name, like PagerDuty Incidents. This is the name users will see in the plugin listing.
+- Under **Associated Proxy**, choose the proxy you just created.
+- Under **Plugin Context**, click on Add another context; choose Selection type: Include, and Entity types: service.
+- This plugin does not work in the Global context. Turn off the switch labeled **Include in global context**
+- In The **Plugin code** section, upload the `dist/ui.html` file you just built.
+- Click on **Save plugin**
+
+Now, when you navigate to a Cortex entity that has a PagerDuty oncall associated with it, you should be able to click on Plugins > PagerDuty Incidents and see the PagerDuty incidents associated with the service that is linked to the entity.
+
+If no PagerDuty oncall is set for the entity, you will see a service chooser that allows you to map the entity to a PagerDuty service.
+
+
+
+If you don't want to see the PagerDuty plugin at all on entities that don't have a PagerDuty oncall, then you can adjust the plugin visibility via CQL to only show in entities where `x-cortex-oncall.pagerduty.type = SERVICE`.
+
+# Setting up your dev environment
+
+PagerDuty Incidents Plugin is a [Cortex](https://www.cortex.io/) plugin. To see how to run the plugin inside of Cortex, see [our docs](https://docs.cortex.io/docs/plugins).
+
+### Prerequisites
+
+Developing and building this plugin requires either [yarn](https://classic.yarnpkg.com/lang/en/docs/install/) or [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
+
+## Getting started
+
+1. Run `yarn` or `npm install` to download all dependencies
+2. Run `yarn build` or `npm run build` to compile the plugin code into `./dist/ui.html`
+3. Upload `ui.html` into Cortex on a create or edit plugin page
+4. Add or update the code and repeat steps 2-3 as necessary
+
+### Notable scripts
+
+The following commands come pre-configured in this repository. You can see all available commands in the `scripts` section of [package.json](./package.json). They can be run with npm via `npm run {script_name}` or with yarn via `yarn {script_name}`, depending on your package manager preference. For instance, the `build` command can be run with `npm run build` or `yarn build`.
+
+- `build` - compiles the plugin. The compiled code root is `./src/index.tsx` (or as defined by [webpack.config.js](webpack.config.js)) and the output is generated into `dist/ui.html`.
+- `test` - runs all tests defined in the repository using [jest](https://jestjs.io/)
+- `lint` - runs lint and format checking on the repository using [prettier](https://prettier.io/) and [eslint](https://eslint.org/)
+- `lintfix` - runs eslint in fix mode to fix any linting errors that can be fixed automatically
+- `formatfix` - runs Prettier in fix mode to fix any formatting errors that can be fixed automatically
+
+### Available React components
+
+See available UI components via our [Storybook](https://cortexapps.github.io/plugin-core/).
diff --git a/plugins/pagerduty-incidents/__mocks__/fileMock.js b/plugins/pagerduty-incidents/__mocks__/fileMock.js
new file mode 100644
index 0000000..0a445d0
--- /dev/null
+++ b/plugins/pagerduty-incidents/__mocks__/fileMock.js
@@ -0,0 +1 @@
+module.exports = "test-file-stub";
diff --git a/plugins/pagerduty-incidents/__mocks__/styleMock.js b/plugins/pagerduty-incidents/__mocks__/styleMock.js
new file mode 100644
index 0000000..f053ebf
--- /dev/null
+++ b/plugins/pagerduty-incidents/__mocks__/styleMock.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/plugins/pagerduty-incidents/babel.config.js b/plugins/pagerduty-incidents/babel.config.js
new file mode 100644
index 0000000..1442fdf
--- /dev/null
+++ b/plugins/pagerduty-incidents/babel.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ plugins: ["@babel/plugin-syntax-jsx"],
+ presets: [
+ ["@babel/preset-env", { targets: { node: "current" } }],
+ "@babel/preset-typescript",
+ ["@babel/preset-react", { runtime: "automatic" }],
+ ],
+};
diff --git a/plugins/pagerduty-incidents/img/pagerduty-chooser.png b/plugins/pagerduty-incidents/img/pagerduty-chooser.png
new file mode 100644
index 0000000..f0d1660
Binary files /dev/null and b/plugins/pagerduty-incidents/img/pagerduty-chooser.png differ
diff --git a/plugins/pagerduty-incidents/img/pagerduty-incidents.png b/plugins/pagerduty-incidents/img/pagerduty-incidents.png
new file mode 100644
index 0000000..8a92435
Binary files /dev/null and b/plugins/pagerduty-incidents/img/pagerduty-incidents.png differ
diff --git a/plugins/pagerduty-incidents/img/pagerduty-proxy.png b/plugins/pagerduty-incidents/img/pagerduty-proxy.png
new file mode 100644
index 0000000..542b26b
Binary files /dev/null and b/plugins/pagerduty-incidents/img/pagerduty-proxy.png differ
diff --git a/plugins/pagerduty-incidents/jest.config.js b/plugins/pagerduty-incidents/jest.config.js
new file mode 100644
index 0000000..3048b79
--- /dev/null
+++ b/plugins/pagerduty-incidents/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ moduleNameMapper: {
+ // map static asset imports to a stub file under the assumption they are not important to our tests
+ "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
+ "/__mocks__/fileMock.js",
+ // map style asset imports to a stub file under the assumption they are not important to our tests
+ "\\.(css|less)$": "/__mocks__/styleMock.js",
+ "@cortexapps/plugin-core/components":
+ "/../../node_modules/@cortexapps/plugin-core/dist/components.cjs.js",
+ "@cortexapps/plugin-core":
+ "/../../node_modules/@cortexapps/plugin-core/dist/index.cjs.js",
+ },
+ setupFilesAfterEnv: ["/setupTests.ts"],
+ testEnvironment: "jsdom",
+ transform: {
+ "^.+\\.tsx?$": "babel-jest",
+ "^.+\\.js$": "babel-jest",
+ },
+ transformIgnorePatterns: [
+ "/node_modules/(?!yaml/)", // Add this line to tell Jest to transform `yaml` module
+ ],
+};
diff --git a/plugins/pagerduty-incidents/package.json b/plugins/pagerduty-incidents/package.json
new file mode 100644
index 0000000..fc2a613
--- /dev/null
+++ b/plugins/pagerduty-incidents/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "pagerduty-incidents-cortex-plugin",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@chakra-ui/react": "^2.10.2",
+ "@cortexapps/plugin-core": "^2.0.0",
+ "@emotion/react": "^11.13.3",
+ "@emotion/styled": "^11.13.0",
+ "framer-motion": "^11.11.7",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "yaml": "^2.5.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.3",
+ "@babel/plugin-syntax-jsx": "^7.18.6",
+ "@babel/preset-env": "^7.20.2",
+ "@babel/preset-react": "^7.18.6",
+ "@babel/preset-typescript": "^7.21.0",
+ "@popperjs/core": "^2.11.8",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^14.0.0",
+ "@types/react": "^18.0.28",
+ "@types/react-dom": "^18.0.11",
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
+ "@typescript-eslint/parser": "^5.55.0",
+ "babel-jest": "^29.5.0",
+ "css-loader": "^6.7.3",
+ "eslint": "^8.0.1",
+ "eslint-config-prettier": "^8.7.0",
+ "eslint-config-standard-with-typescript": "^34.0.0",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.6.1",
+ "eslint-plugin-promise": "^6.0.0",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "html-webpack-plugin": "^5.5.0",
+ "jest": "^29.5.0",
+ "jest-environment-jsdom": "^29.5.0",
+ "jest-fetch-mock": "^3.0.3",
+ "npm-run-all": "^4.1.5",
+ "prettier": "^2.8.4",
+ "prop-types": "^15.8.1",
+ "react-dev-utils": "^12.0.1",
+ "style-loader": "^3.3.1",
+ "terser-webpack-plugin": "^5.3.7",
+ "ts-loader": "^9.4.2",
+ "typescript": "^4.9.5",
+ "url-loader": "^4.1.1",
+ "webpack": "^5.76.1",
+ "webpack-cli": "^5.0.1",
+ "webpack-dev-server": "^4.15.0"
+ },
+ "scripts": {
+ "build": "webpack --mode=production",
+ "clean": "rm -r ./dist",
+ "dev": "webpack serve --mode=development",
+ "fix": "run-p formatfix lintfix",
+ "formatfix": "yarn prettier . --write",
+ "formatcheck": "yarn prettier . --check",
+ "lint": "run-p formatcheck lintcheck",
+ "lintcheck": "yarn eslint src",
+ "lintfix": "yarn lintcheck --fix",
+ "test": "jest"
+ }
+}
diff --git a/plugins/pagerduty-incidents/setupTests.ts b/plugins/pagerduty-incidents/setupTests.ts
new file mode 100644
index 0000000..fa0a10d
--- /dev/null
+++ b/plugins/pagerduty-incidents/setupTests.ts
@@ -0,0 +1,60 @@
+import "@testing-library/jest-dom/extend-expect";
+import fetchMock from "jest-fetch-mock";
+
+fetchMock.enableMocks();
+
+const mockContext = {
+ apiBaseUrl: "https://api.getcortexapp.com",
+ entity: {
+ definition: null,
+ description: null,
+ groups: null,
+ name: "Inventory planner",
+ ownership: {
+ emails: [
+ {
+ description: null,
+ email: "nikhil@cortex.io",
+ inheritance: null,
+ id: 1,
+ },
+ ],
+ },
+ tag: "inventory-planner",
+ type: "service",
+ },
+ location: "ENTITY",
+ user: {
+ email: "ganesh@cortex.io",
+ name: "Ganesh Datta",
+ role: "ADMIN",
+ },
+};
+
+jest.mock("@cortexapps/plugin-core/components", () => {
+ const originalModule = jest.requireActual(
+ "@cortexapps/plugin-core/components"
+ );
+ return {
+ ...originalModule,
+ usePluginContext: () => {
+ return mockContext;
+ },
+ PluginProvider: ({ children }) => {
+ return children;
+ },
+ };
+});
+
+jest.mock("@cortexapps/plugin-core", () => {
+ const originalModule = jest.requireActual("@cortexapps/plugin-core");
+ return {
+ ...originalModule,
+ CortexApi: {
+ ...originalModule.CortexApi,
+ getContext: () => {
+ return mockContext;
+ },
+ },
+ };
+});
diff --git a/plugins/pagerduty-incidents/src/api/Cortex.ts b/plugins/pagerduty-incidents/src/api/Cortex.ts
new file mode 100644
index 0000000..01a0d12
--- /dev/null
+++ b/plugins/pagerduty-incidents/src/api/Cortex.ts
@@ -0,0 +1,8 @@
+export const getEntityYaml = async (
+ baseUrl: string,
+ entityTag: string
+): Promise> => {
+ const res = await fetch(`${baseUrl}/catalog/${entityTag}/openapi`);
+
+ return await res.json();
+};
diff --git a/plugins/pagerduty-incidents/src/baseStyles.css b/plugins/pagerduty-incidents/src/baseStyles.css
new file mode 100644
index 0000000..e9c5e5f
--- /dev/null
+++ b/plugins/pagerduty-incidents/src/baseStyles.css
@@ -0,0 +1,3 @@
+body {
+ font: 14px sans-serif;
+}
diff --git a/plugins/pagerduty-incidents/src/components/App.test.tsx b/plugins/pagerduty-incidents/src/components/App.test.tsx
new file mode 100644
index 0000000..bfcb46c
--- /dev/null
+++ b/plugins/pagerduty-incidents/src/components/App.test.tsx
@@ -0,0 +1,153 @@
+import { render, waitFor } from "@testing-library/react";
+
+import fetchMock from "jest-fetch-mock";
+
+import App from "./App";
+describe("App", () => {
+ beforeEach(() => {
+ // Reset fetchMock before each test to start with a clean slate
+ fetchMock.resetMocks();
+ });
+
+ it("gets no pd mapping", async () => {
+ const mockBodies = {
+ "https://api.getcortexapp.com/catalog/inventory-planner/openapi": {
+ info: {
+ title: "Inventory Planner",
+ description: "it is a inventory planner",
+ "x-cortex-tag": "inventory-planner",
+ "x-cortex-type": "service",
+ },
+ openapi: "3.0.1",
+ servers: [
+ {
+ url: "/",
+ },
+ ],
+ },
+ "https://api.pagerduty.com/services": {
+ services: [],
+ },
+ };
+
+ fetchMock.mockResponse(async (req) => {
+ const url = req.url.split("?")[0];
+ if (!mockBodies[url]) {
+ return {
+ status: 404,
+ };
+ }
+ const body = mockBodies[url] || {};
+ return {
+ status: 200,
+ body: JSON.stringify(body),
+ };
+ });
+
+ const { getByText } = render();
+
+ await waitFor(() => {
+ const element = getByText(
+ /This entity is not associated with any PagerDuty service./
+ );
+ expect(element).toBeInTheDocument();
+ expect(fetch).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /https:\/\/api\.getcortexapp\.com\/catalog\/inventory-planner\/openapi/
+ )
+ );
+ });
+ });
+
+ it("gets a PD mapping but no oncall", async () => {
+ const mockBodies = {
+ "https://api.getcortexapp.com/catalog/inventory-planner/openapi": {
+ info: {
+ title: "Inventory Planner",
+ description: "it is a inventory planner",
+ "x-cortex-tag": "inventory-planner",
+ "x-cortex-type": "service",
+ "x-cortex-oncall": {
+ pagerduty: {
+ id: "PXXXXXX",
+ type: "SERVICE",
+ },
+ },
+ },
+ openapi: "3.0.1",
+ servers: [
+ {
+ url: "/",
+ },
+ ],
+ },
+ "https://api.pagerduty.com/services/PXXXXXX": {
+ service: {
+ id: "PXXXXXX",
+ type: "service",
+ summary: "My Application Service",
+ self: "https://api.pagerduty.com/services/PXXXXXX",
+ html_url: "https://subdomain.pagerduty.com/service-directory/PXXXXXX",
+ name: "My Application Service",
+ auto_resolve_timeout: 14400,
+ acknowledgement_timeout: 600,
+ created_at: "2015-11-06T11:12:51-05:00",
+ status: "active",
+ alert_creation: "create_alerts_and_incidents",
+ integrations: [],
+ escalation_policy: {
+ id: "PYYYYYY",
+ type: "escalation_policy_reference",
+ summary: "Another Escalation Policy",
+ self: "https://api.pagerduty.com/escalation_policies/PYYYYYY",
+ html_url:
+ "https://subdomain.pagerduty.com/escalation_policies/PYYYYYY",
+ },
+ teams: [],
+ },
+ },
+ "https://api.pagerduty.com/oncalls": {
+ oncalls: [],
+ },
+ "https://api.pagerduty.com/services": {
+ services: [],
+ },
+ };
+
+ fetchMock.mockResponse(async (req) => {
+ const url = req.url.split("?")[0];
+ const body = mockBodies[url] || {};
+ return {
+ status: 200,
+ body: JSON.stringify(body),
+ };
+ });
+
+ const { getByText } = render();
+
+ await waitFor(() => {
+ const element = getByText(
+ /No PagerDuty oncalls were found for this entity/
+ );
+ expect(element).toBeInTheDocument();
+ expect(fetch).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /https:\/\/api\.getcortexapp\.com\/catalog\/inventory-planner\/gitops-logs/
+ )
+ );
+ expect(fetch).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /https:\/\/api\.getcortexapp\.com\/catalog\/inventory-planner\/openapi/
+ )
+ );
+ expect(fetch).toHaveBeenCalledWith(
+ "https://api.pagerduty.com/services/PXXXXXX",
+ {
+ headers: {
+ Accept: "application/vnd.pagerduty+json;version=2",
+ },
+ }
+ );
+ });
+ });
+});
diff --git a/plugins/pagerduty-incidents/src/components/App.tsx b/plugins/pagerduty-incidents/src/components/App.tsx
new file mode 100644
index 0000000..25c05f0
--- /dev/null
+++ b/plugins/pagerduty-incidents/src/components/App.tsx
@@ -0,0 +1,20 @@
+import type React from "react";
+import { PluginProvider } from "@cortexapps/plugin-core/components";
+import { ChakraProvider } from "@chakra-ui/react";
+import "../baseStyles.css";
+import ErrorBoundary from "./ErrorBoundary";
+import PagerDutyPlugin from "./PagerDutyPlugin";
+
+const App: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/plugins/pagerduty-incidents/src/components/ErrorBoundary.tsx b/plugins/pagerduty-incidents/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..f862bd3
--- /dev/null
+++ b/plugins/pagerduty-incidents/src/components/ErrorBoundary.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+
+interface ErrorBoundaryProps extends React.PropsWithChildren {}
+
+interface ErrorBoundaryState {
+ hasError: boolean;
+}
+
+class ErrorBoundary extends React.Component<
+ ErrorBoundaryProps,
+ ErrorBoundaryState
+> {
+ public state: ErrorBoundaryState = {
+ hasError: false,
+ };
+
+ public static getDerivedStateFromError(_: Error): ErrorBoundaryState {
+ // Update state so the next render will show the fallback UI.
+ return { hasError: true };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
+ console.error("Uncaught error:", error, errorInfo);
+ }
+
+ public render(): React.ReactNode {
+ if (this.state.hasError) {
+ return (
+
+ Oops! There was a runtime error. See the console for more details.
+