Skip to content

Snow incidents plugin #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plugins/snow-incident-plugin/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
30 changes: 30 additions & 0 deletions plugins/snow-incident-plugin/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"standard-with-typescript",
"prettier",
],
overrides: [],
parserOptions: {
ecmaVersion: "latest",
project: "tsconfig.json",
sourceType: "module",
tsconfigRootDir: __dirname,
},
plugins: ["react"],
rules: {
// conflicts with no-extra-boolean-cast
"@typescript-eslint/strict-boolean-expressions": "off",
"no-console": ["error", { allow: ["warn", "error"] }],
},
settings: {
react: {
version: "detect",
},
},
};
12 changes: 12 additions & 0 deletions plugins/snow-incident-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# OSX
*.DS_Store

# IDEs
.idea
*.iml
.vscode

# This project
node_modules/
dist/
yarn-error.log
2 changes: 2 additions & 0 deletions plugins/snow-incident-plugin/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
81 changes: 81 additions & 0 deletions plugins/snow-incident-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ServiceNow Incidents Plugin

View ServiceNow incidents associated with a Cortex service!

<div align="center"><img src="img/incidents_cortex.png" width="550" /></div>

The ServiceNow Incidents plugin shows you recent incidents that have been filed against a given service in Cortex. It does this by associating the Cortex entity with a CMDB CI, and searching the ServiceNow `incident` table for incidents whose `cmdb_ci`, `business_service`, or `affected_ci` match the associated CI. Here are the ways the plugin looks for the CI for a Cortex entity:

- If the Cortex entity is already mapped to a ServiceNow CMDB CI using the Cortex ServiceNow integration, the plugin will show incidents pertaining to that CI.
- If it's not mapped, the plugin uses the service name in Cortex to find a corresponding service with the same name in the CMDB by searching `cmdb_ci_service` for objects with the same name as the entity's tag or name.
- If you don't want to search, and want to configure a specific CI without mapping the entity to ServiceNow, you can set a custom data key `servicenow-sys_id` on the entity to the sys_id of the CI for which you want to show incidents when the plugin is displayed for that entity.

<div align="center"><img src="img/incidents_snow.png" width="550" /></div>

## Setup

To see how to run the plugin inside of Cortex, see [our docs](https://docs.cortex.io/docs/plugins). This plugin will require a proxy to ServiceNow.

### Proxy Setup

- Define a Secret that is a base64 encoding of `username:password`. You can use a tool similar to [this](https://www.debugbear.com/basic-auth-header-generator) to convert it.
- Define a proxy that is pointed at your ServiceNow instance with the nescessary headers. For help figuring out which headers to use, refer to the REST API explorer in your servicenow instance. Here is an example of what your proxy may look like:

<div align="center"><img src="img/proxy_changes.png" width="550" /></div>

### Plugin Registration

- Create a Plugin and associate it with the proxy you created in the previous step
- This Plugin will not work in the Global context.
- Select the entity types that will have a corresponding CMDB CI. Below is what a configuration may look like. In this example the plugin will be available to both Services and Resources.
<div align="center"><img src="img/snow_changes_config.png" width="350" /></div>

- Follow the directions under **Getting Started** below to build `ui.html` and upload it

### Set your ServiceNow instance URL

Your ServiceNow instance URL should look like `https://something.service-now.com`. It's the same URL you used to set up the proxy above. We also want to create a new entity to tell the plugin what ServiceNow URL to load:

- Consider creating a new entity type, so that any existing scorecards are not affected by ths configuration entity. In this example, we have created a new entity type called `plugin-configuration`
- Create a new entity with the tag `servicenow-plugin-config`
- Set `x-cortex-definition.servicenow-url` to the value of your ServiceNow Instance URL. For example, if my ServiceNow Instance URL was `https://martindstone.service-now.com`, my `servicenow-plugin-config` entity would look like this:

```
openapi: 3.0.1
info:
title: ServiceNow Plugin Config
description: ""
x-cortex-tag: servicenow-plugin-config
x-cortex-type: plugin-configuration
x-cortex-definition:
servicenow-url: https://martindstone.service-now.com
```

Now when you load the plugin on an entity, you should see any ServiceNow incidents that were filed against that entity's CMDB CI.

## Setting up your dev environment

### 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/).
1 change: 1 addition & 0 deletions plugins/snow-incident-plugin/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "test-file-stub";
1 change: 1 addition & 0 deletions plugins/snow-incident-plugin/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
8 changes: 8 additions & 0 deletions plugins/snow-incident-plugin/babel.config.js
Original file line number Diff line number Diff line change
@@ -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" }],
],
};
12 changes: 12 additions & 0 deletions plugins/snow-incident-plugin/cortex.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
openapi: 3.0.1
info:
title: Plugins Snow Incident Plugin
description: ""
x-cortex-tag: plugins-snow-incident-plugin
x-cortex-git:
github:
repository: cortexapps/cortex-plugins
basepath: plugins/snow-incident-plugin
x-cortex-custom-metadata:
cortex-template-version: 0.1.0
cortex-generated-timestamp: 2023-08-14T18:41:53.747515527
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions plugins/snow-incident-plugin/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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)$":
"<rootDir>/__mocks__/fileMock.js",
// map style asset imports to a stub file under the assumption they are not important to our tests
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"@cortexapps/plugin-core/components":
"<rootDir>/../../node_modules/@cortexapps/plugin-core/dist/components.cjs.js",
"@cortexapps/plugin-core":
"<rootDir>/../../node_modules/@cortexapps/plugin-core/dist/index.cjs.js",
},
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
testEnvironment: "jsdom",
transform: {
"^.+\\.tsx?$": "babel-jest",
},
};
62 changes: 62 additions & 0 deletions plugins/snow-incident-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "ServiceNowIncidentsPlugin",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@cortexapps/plugin-core": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"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"
}
}
59 changes: 59 additions & 0 deletions plugins/snow-incident-plugin/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import "@testing-library/jest-dom/extend-expect";
import fetchMock from "jest-fetch-mock";

fetchMock.enableMocks();
const mockContext = {
apiBaseUrl: "https://api.cortex.dev",
entity: {
definition: null,
description: null,
groups: null,
name: "Inventory planner",
ownership: {
emails: [
{
description: null,
email: "[email protected]",
inheritance: null,
id: 1,
},
],
},
tag: "inventory-planner",
type: "service",
},
location: "ENTITY",
user: {
email: "[email protected]",
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;
},
},
};
});
7 changes: 7 additions & 0 deletions plugins/snow-incident-plugin/src/api/Cortex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CortexApi, type CortexContextResponse } from "@cortexapps/plugin-core";

export const getCortexContext = async (): Promise<CortexContextResponse> => {
const context = await CortexApi.getContext();

return context;
};
3 changes: 3 additions & 0 deletions plugins/snow-incident-plugin/src/baseStyles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
font: 14px sans-serif;
}
82 changes: 82 additions & 0 deletions plugins/snow-incident-plugin/src/components/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { render, screen, waitFor } from "@testing-library/react";

import fetchMock from "jest-fetch-mock";
import { successMockBodies } from "../mocks/mockBodies";

import App from "./App";

describe("App", () => {
beforeEach(() => {
// Reset fetchMock before each test to start with a clean slate
fetchMock.resetMocks();
});

it("tries to fetch", async () => {
fetchMock.mockResponse(async (req) => {
const url = req.url.split("?")[0];
return {
status: 200,
body: JSON.stringify(successMockBodies[url]),
};
});

render(<App />);

await waitFor(() => {
expect(fetch).toHaveBeenCalledWith(
"https://api.cortex.dev/catalog/servicenow-plugin-config/openapi"
);
expect(fetch).toHaveBeenCalledWith(
"https://api.cortex.dev/catalog/inventory-planner/openapi"
);
expect(fetch).toHaveBeenCalledWith(
expect.stringMatching(
/https:\/\/unit-testing-snow-instance\.service-now\.com\/api\/now\/table\/cmdb_ci_service/
)
);
expect(fetch).toHaveBeenCalledWith(
expect.stringMatching(
/https:\/\/unit-testing-snow-instance\.service-now\.com\/api\/now\/table\/incident/
)
);
});
});

it("displays incidents", async () => {
fetchMock.mockResponse(async (req) => {
const url = req.url.split("?")[0];
return {
status: 200,
body: JSON.stringify(successMockBodies[url]),
};
});

render(<App />);

await waitFor(() => {
const element = screen.getByText("Unable to connect to email", {
selector: "p",
});
expect(element).toBeInTheDocument();
});
});

it("fails to display incidents", async () => {
fetchMock.mockResponse(async () => {
return {
status: 200,
body: JSON.stringify({}),
};
});

render(<App />);

await waitFor(() => {
const element = screen.getByText(
"This plugin will fetch incidents from ServiceNow and display them here.",
{ selector: "p" }
);
expect(element).toBeInTheDocument();
});
});
});
Loading