diff --git a/.changeset/itchy-camels-grin.md b/.changeset/itchy-camels-grin.md
new file mode 100644
index 0000000000..eac3c6a102
--- /dev/null
+++ b/.changeset/itchy-camels-grin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-splunk-on-call': patch
+---
+
+Added splunk-on-call plugin.
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 519216ecd8..3bfa0dd155 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -213,6 +213,7 @@ Sneha
Snyk
sourcemaps
sparklines
+Splunk
Spotifiers
spotify
Spotify
diff --git a/microsite/data/plugins/splunk-on-call.yaml b/microsite/data/plugins/splunk-on-call.yaml
new file mode 100644
index 0000000000..2d6deb0180
--- /dev/null
+++ b/microsite/data/plugins/splunk-on-call.yaml
@@ -0,0 +1,14 @@
+---
+title: Splunk On-Call
+author: Spotify
+authorUrl: https://github.com/spotify
+category: Monitoring
+description: Splunk On-Call offers a simple way to identify incidents and escalation policies.
+documentation: https://github.com/backstage/backstage/tree/master/plugins/splunk-on-call
+iconUrl: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIiBjbGFzcz0idm8tbGVhZi1ibGFjayI+PHBhdGggZD0iTTE1Ljk5OTcgMzJDMjQuODM2MSAzMiAzMiAyNC44MzY4IDMyIDE2LjAwMDJDMzIgNy4xNjM1NiAyNC44MzYxIDAgMTUuOTk5NyAwQzcuMTYzNCAwIDAgNy4xNjM1NiAwIDE2LjAwMDJDMCAyNC44MzY4IDcuMTYzNCAzMiAxNS45OTk3IDMyWk0yMi41Njc1IDE0LjY5MDRMMjMuNjI1OSAxNi41MTU4TDE4Ljg4NTEgMTguOTE0NUwxOC44MSAxOS43MTQxTDIyLjQwMzMgMTguNTc2MUwyMC41OTcyIDIwLjg0MzJMMjAuNDQ2MiAyMS41NjI4TDE4LjI0MzkgMjIuNTUyNUwxOC4wMTc2IDIzLjIzMjJMMTkuMDA0NSAyMi44MTU3TDE2LjM0MjIgMjUuOTEwNFYyOS42MDA0SDE1LjY1NzdWMjUuOTEwNEwxMi45OTU5IDIyLjgxNTdMMTMuOTgyOCAyMy4yMzIyTDEzLjc1NjYgMjIuNTUyNUwxMS41NTQ1IDIxLjU2MjhMMTEuNDAzNiAyMC44NDMyTDkuNTk2OTIgMTguNTc2MUwxMy4xOTA2IDE5LjcxNDFMMTMuMTE0OSAxOC45MTQ1TDguMzc0NDkgMTYuNTE1OEw5LjQzMzM5IDE0LjY5MDRMNy4yMDAyIDExLjUxODlMMTMuMzcxOCAxNC41MDA3TDEzLjQ5NyAxMy42MDM3TDExLjY5MzYgMTAuNjYyTDExLjk1MiAxMC4xNzc0TDExLjExOTUgNi43MTg2N0wxNC4zNTUzIDEwLjEyNjdMMTQuNTQ5MyA4LjI0NDU0TDEzLjU4MTggNS40MjI1M0wxNC44NzMgNS44NTYyNEwxNiAyLjQwMDM5TDE3LjEyNzMgNS44NTYyNEwxOC40MTg2IDUuNDIyNTNMMTcuNDUxMiA4LjI0NDU0TDE3LjY0NTQgMTAuMTI2N0wyMC44ODA3IDYuNzE4NjdMMjAuMDQ4MiAxMC4xNzc0TDIwLjMwNjggMTAuNjYyTDE4LjUwMzQgMTMuNjAzN0wxOC42Mjg4IDE0LjUwMDdMMjQuODAwMiAxMS41MTg5TDIyLjU2NzUgMTQuNjkwNFoiIGZpbGw9JyMyQzJDMkMnLz48L3N2Zz4K
+npmPackageName: '@backstage/plugin-splunk-on-call'
+tags:
+ - monitoring
+ - errors
+ - alerting
+ - splunk
diff --git a/plugins/splunk-on-call/.eslintrc.js b/plugins/splunk-on-call/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/splunk-on-call/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md
new file mode 100644
index 0000000000..73ee05c181
--- /dev/null
+++ b/plugins/splunk-on-call/README.md
@@ -0,0 +1,120 @@
+# Splunk On-Call
+
+## Overview
+
+This plugin displays Splunk On-Call, formerly VictorOps, information about an entity.
+
+There is a way to trigger an new incident directly to specific users or/and specific teams.
+
+This plugin requires that entities are annotated with a team name. See more further down in this document.
+
+This plugin provides:
+
+- A list of incidents
+- A way to trigger a new incident to specific users or/and teams
+- A way to acknowledge/resolve an incident
+- Information details about the persons on-call
+
+## Setup instructions
+
+Install the plugin:
+
+```bash
+yarn add @backstage/plugin-splunk-on-call
+```
+
+Add it to the app in `plugins.ts`:
+
+```ts
+export { plugin as SplunkOnCall } from '@backstage/plugin-splunk-on-call';
+```
+
+Add it to the `EntityPage.tsx`:
+
+```ts
+import {
+ isPluginApplicableToEntity as isSplunkOnCallAvailable,
+ SplunkOnCallCard,
+} from '@backstage/plugin-splunk-on-call';
+// ...
+{
+ isSplunkOnCallAvailable(entity) && (
+
+
+
+ );
+}
+```
+
+## Client configuration
+
+In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action.
+The user supplied must be a valid Splunk On-Call user and a member of your organization.
+
+In `app-config.yaml`:
+
+```yaml
+splunkOnCall:
+ username:
+```
+
+The user supplied must be a valid Splunk On-Call user and a member of your organization.
+
+In order to make the API calls, you need to provide a new proxy config which will redirect to the Splunk On-Call API endpoint and add authentication information in the headers:
+
+```yaml
+# app-config.yaml
+proxy:
+ # ...
+ '/splunk-on-call':
+ target: https://api.victorops.com/api-public
+ headers:
+ X-VO-Api-Id:
+ $env: SPLUNK_ON_CALL_API_ID
+ X-VO-Api-Key:
+ $env: SPLUNK_ON_CALL_API_KEY
+```
+
+In addition, to make certain API calls (trigger-resolve-acknowledge an incident) you need to add the `PATCH` method to the backend `cors` methods list: `[GET, POST, PUT, DELETE, PATCH]`.
+
+### Adding your team name to the entity annotation
+
+The information displayed for each entity is based on the team name.
+If you want to use this plugin for an entity, you need to label it with the below annotation:
+
+```yaml
+annotations:
+ splunk.com/on-call-team':
+```
+
+## Providing the API key and API id
+
+In order for the client to make requests to the [Splunk On-Call API](https://portal.victorops.com/public/api-docs.html#/) it needs an [API ID and an API Key](https://help.victorops.com/knowledge-base/api/).
+
+Then start the backend passing the values as an environment variable:
+
+```bash
+$ SPLUNK_ON_CALL_API_KEY='' SPLUNK_ON_CALL_API_ID='' yarn start
+```
+
+This will proxy the request by adding `X-VO-Api-Id` and `X-VO-Api-Key` headers with the provided values.
+
+You can also add the values in your helm template:
+
+```yaml
+# backend-secret.yaml
+stringData:
+ # ...
+ SPLUNK_ON_CALL_API_ID: { { .Values.auth.splunkOnCallApiId } }
+ SPLUNK_ON_CALL_API_KEY: { { .Values.auth.splunkOnCallApiKey } }
+```
+
+To enable it you need to provide them in the chart's values:
+
+```yaml
+# values.yaml
+auth:
+ # ...
+ splunkOnCallApiId: h
+ splunkOnCallApiKey: h
+```
diff --git a/plugins/splunk-on-call/dev/index.tsx b/plugins/splunk-on-call/dev/index.tsx
new file mode 100644
index 0000000000..346c92c37e
--- /dev/null
+++ b/plugins/splunk-on-call/dev/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 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 { createDevApp } from '@backstage/dev-utils';
+import { splunkOnCallPlugin, SplunkOnCallPage } from '../src/plugin';
+
+createDevApp()
+ .registerPlugin(splunkOnCallPlugin)
+ .addPage({
+ title: 'Splunk On-Call',
+ element: ,
+ })
+ .render();
diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json
new file mode 100644
index 0000000000..c0542bd64c
--- /dev/null
+++ b/plugins/splunk-on-call/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@backstage/plugin-splunk-on-call",
+ "version": "0.1.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"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/splunk-on-call"
+ },
+ "keywords": [
+ "backstage",
+ "splunk-on-call"
+ ],
+ "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.7.1",
+ "@backstage/core": "^0.6.1",
+ "@backstage/plugin-catalog-react": "^0.0.2",
+ "@backstage/theme": "^0.2.3",
+ "@material-ui/core": "^4.11.0",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "classnames": "^2.2.6",
+ "luxon": "^1.25.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.6.0",
+ "@backstage/dev-utils": "^0.1.10",
+ "@backstage/test-utils": "^0.1.7",
+ "@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/luxon": "^1.25.0",
+ "@types/node": "^12.0.0",
+ "cross-fetch": "^3.0.6",
+ "msw": "^0.21.2",
+ "node-fetch": "^2.6.1"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts
new file mode 100644
index 0000000000..501dad80a2
--- /dev/null
+++ b/plugins/splunk-on-call/src/api/client.ts
@@ -0,0 +1,204 @@
+/*
+ * 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 {
+ Incident,
+ OnCall,
+ User,
+ EscalationPolicyInfo,
+ Team,
+} from '../components/types';
+import {
+ SplunkOnCallApi,
+ TriggerAlarmRequest,
+ IncidentsResponse,
+ OnCallsResponse,
+ ClientApiConfig,
+ RequestOptions,
+ ListUserResponse,
+ EscalationPolicyResponse,
+ PatchIncidentRequest,
+} from './types';
+
+export class UnauthorizedError extends Error {}
+
+export const splunkOnCallApiRef = createApiRef({
+ id: 'plugin.splunk-on-call.api',
+ description: 'Used to fetch data from Splunk On-Call API',
+});
+
+export class SplunkOnCallClient implements SplunkOnCallApi {
+ static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
+ const usernameFromConfig: string | null =
+ configApi.getOptionalString('splunkOnCall.username') || null;
+ return new SplunkOnCallClient({
+ username: usernameFromConfig,
+ discoveryApi,
+ });
+ }
+ constructor(private readonly config: ClientApiConfig) {}
+
+ async getIncidents(): Promise {
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/incidents`;
+
+ const { incidents } = await this.getByUrl(url);
+
+ return incidents;
+ }
+
+ async getOnCallUsers(): Promise {
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/oncall/current`;
+ const { teamsOnCall } = await this.getByUrl(url);
+
+ return teamsOnCall;
+ }
+
+ async getTeams(): Promise {
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/team`;
+ const teams = await this.getByUrl(url);
+
+ return teams;
+ }
+
+ async acknowledgeIncident({
+ incidentNames,
+ }: PatchIncidentRequest): Promise {
+ const options = {
+ method: 'PATCH',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userName: this.config.username,
+ incidentNames,
+ }),
+ };
+
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/incidents/ack`;
+
+ return this.request(url, options);
+ }
+
+ async resolveIncident({
+ incidentNames,
+ }: PatchIncidentRequest): Promise {
+ const options = {
+ method: 'PATCH',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userName: this.config.username,
+ incidentNames,
+ }),
+ };
+
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/incidents/resolve`;
+
+ return this.request(url, options);
+ }
+
+ async getUsers(): Promise {
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v2/user`;
+ const { users } = await this.getByUrl(url);
+
+ return users;
+ }
+
+ async getEscalationPolicies(): Promise {
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/policies`;
+ const { policies } = await this.getByUrl(url);
+
+ return policies;
+ }
+
+ async triggerAlarm({
+ summary,
+ details,
+ userName,
+ targets,
+ isMultiResponder,
+ }: TriggerAlarmRequest): Promise {
+ const body = JSON.stringify({
+ summary,
+ details,
+ userName: this.config.username || userName,
+ targets,
+ isMultiResponder,
+ });
+
+ const options = {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body,
+ };
+
+ const url = `${await this.config.discoveryApi.getBaseUrl(
+ 'proxy',
+ )}/splunk-on-call/v1/incidents`;
+
+ return this.request(url, options);
+ }
+
+ private async getByUrl(url: string): Promise {
+ const options = {
+ method: 'GET',
+ headers: {
+ '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 === 403) {
+ 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/splunk-on-call/src/api/index.ts b/plugins/splunk-on-call/src/api/index.ts
new file mode 100644
index 0000000000..1e4056fbc0
--- /dev/null
+++ b/plugins/splunk-on-call/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 {
+ SplunkOnCallClient,
+ splunkOnCallApiRef,
+ UnauthorizedError,
+} from './client';
+export type { SplunkOnCallApi } from './types';
diff --git a/plugins/splunk-on-call/src/api/mocks.ts b/plugins/splunk-on-call/src/api/mocks.ts
new file mode 100644
index 0000000000..86df04275d
--- /dev/null
+++ b/plugins/splunk-on-call/src/api/mocks.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 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 {
+ EscalationPolicyInfo,
+ Incident,
+ Team,
+ User,
+} from '../components/types';
+
+export const MOCKED_USER: User = {
+ createdAt: '2021-02-01T23:38:38Z',
+ displayName: 'Test User',
+ email: 'test@example.com',
+ firstName: 'FirstNameTest',
+ lastName: 'LastNameTest',
+ passwordLastUpdated: '2021-02-01T23:38:38Z',
+ username: 'test_user',
+ verified: true,
+ _selfUrl: '/api-public/v1/user/test_user',
+};
+
+export const MOCKED_ON_CALL = [
+ {
+ team: { name: 'team_example', slug: 'team-zEalMCgwYSA0Lt40' },
+ oncallNow: [
+ {
+ escalationPolicy: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' },
+ users: [{ onCalluser: { username: 'test_user' } }],
+ },
+ ],
+ },
+];
+
+export const MOCK_INCIDENT: Incident = {
+ alertCount: 1,
+ currentPhase: 'ACKED',
+ entityDisplayName: 'test-incident',
+ entityId: 'entityId',
+ entityState: 'CRITICAL',
+ entityType: 'SERVICE',
+ incidentNumber: '1',
+ lastAlertId: 'lastAlertId',
+ lastAlertTime: '2021-02-03T00:13:11Z',
+ routingKey: 'routingdefault',
+ service: 'test',
+ startTime: '2021-02-03T00:13:11Z',
+ pagedTeams: ['team-O9SqT13fsnCstjMi'],
+ pagedUsers: [],
+ pagedPolicies: [
+ {
+ policy: {
+ name: 'Generated Direct User Policy for test_user',
+ slug: 'directUserPolicySlug-test',
+ _selfUrl: '/test',
+ },
+ },
+ ],
+ transitions: [{ name: 'ACKED', at: '2021-02-03T01:20:00Z', by: 'test' }],
+ monitorName: 'vouser-user',
+ monitorType: 'Manual',
+ firstAlertUuid: 'firstAlertUuid',
+ incidentLink: 'https://portal.victorops.com/example',
+};
+
+export const MOCK_TEAM: Team = {
+ _selfUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi',
+ _membersUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/members',
+ _policiesUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/policies',
+ _adminsUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/admins',
+ name: 'test',
+ slug: 'team-O9SqT13fsnCstjMi',
+ memberCount: 1,
+ version: 1,
+ isDefaultTeam: false,
+};
+
+export const ESCALATION_POLICIES: EscalationPolicyInfo[] = [
+ {
+ policy: {
+ name: 'Example',
+ slug: 'team-zEalMCgwYSA0Lt40',
+ _selfUrl: '/api-public/v1/policies/team-zEalMCgwYSA0Lt40',
+ },
+ team: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' },
+ },
+];
diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts
new file mode 100644
index 0000000000..500f2a6291
--- /dev/null
+++ b/plugins/splunk-on-call/src/api/types.ts
@@ -0,0 +1,117 @@
+/*
+ * 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 {
+ EscalationPolicyInfo,
+ Incident,
+ OnCall,
+ Team,
+ User,
+} from '../components/types';
+import { DiscoveryApi } from '@backstage/core';
+
+export enum TargetType {
+ UserValue = 'User',
+ EscalationPolicyValue = 'EscalationPolicy',
+}
+
+export type IncidentTarget = {
+ type: TargetType;
+ slug: string;
+};
+
+export type TriggerAlarmRequest = {
+ targets: IncidentTarget[];
+ details: string;
+ summary: string;
+ userName: string;
+ isMultiResponder?: boolean;
+};
+
+export interface SplunkOnCallApi {
+ /**
+ * Fetches a list of incidents
+ */
+ getIncidents(): Promise;
+
+ /**
+ * Fetches the list of users in an escalation policy.
+ */
+ getOnCallUsers(): Promise;
+
+ /**
+ * Triggers an incident to specific users and/or specific teams.
+ */
+ triggerAlarm(request: TriggerAlarmRequest): Promise;
+
+ /**
+ * Resolves an incident.
+ */
+ resolveIncident(request: PatchIncidentRequest): Promise;
+
+ /**
+ * Acknowledge an incident.
+ */
+ acknowledgeIncident(request: PatchIncidentRequest): Promise;
+
+ /**
+ * Get a list of users for your organization.
+ */
+ getUsers(): Promise;
+
+ /**
+ * Get a list of teams for your organization.
+ */
+ getTeams(): Promise;
+
+ /**
+ * Get a list of escalation policies for your organization.
+ */
+ getEscalationPolicies(): Promise;
+}
+
+export type PatchIncidentRequest = {
+ incidentNames: string[];
+ message?: string;
+};
+
+export type EscalationPolicyResponse = {
+ policies: EscalationPolicyInfo[];
+};
+
+export type ListUserResponse = {
+ users: User[];
+ _selfUrl?: string;
+};
+
+export type IncidentsResponse = {
+ incidents: Incident[];
+};
+
+export type OnCallsResponse = {
+ teamsOnCall: OnCall[];
+};
+
+export type ClientApiConfig = {
+ username: string | null;
+ discoveryApi: DiscoveryApi;
+};
+
+export type RequestOptions = {
+ method: string;
+ headers: HeadersInit;
+ body?: BodyInit;
+};
diff --git a/plugins/splunk-on-call/src/assets/emptystate.svg b/plugins/splunk-on-call/src/assets/emptystate.svg
new file mode 100644
index 0000000000..fa7f19123e
--- /dev/null
+++ b/plugins/splunk-on-call/src/assets/emptystate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.tsx b/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.tsx
new file mode 100644
index 0000000000..ea72b6ca51
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.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 MissingApiKeyOrApiIdError = () => (
+
+ Read More
+
+ }
+ />
+);
diff --git a/plugins/splunk-on-call/src/components/Errors/index.ts b/plugins/splunk-on-call/src/components/Errors/index.ts
new file mode 100644
index 0000000000..9698b0bd14
--- /dev/null
+++ b/plugins/splunk-on-call/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 { MissingApiKeyOrApiIdError } from './MissingApiKeyOrApiIdError';
diff --git a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx
new file mode 100644
index 0000000000..41157a2b5e
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 { ApiProvider, ApiRegistry } from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+import { MOCKED_ON_CALL, MOCKED_USER } from '../../api/mocks';
+
+const mockSplunkOnCallApi = {
+ getOnCallUsers: () => [],
+};
+const apis = ApiRegistry.from([[splunkOnCallApiRef, mockSplunkOnCallApi]]);
+
+describe('Escalation', () => {
+ it('Handles an empty response', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => []);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled();
+ });
+
+ it('Render a list of users', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => MOCKED_ON_CALL);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+
+ expect(getByText('FirstNameTest LastNameTest')).toBeInTheDocument();
+ expect(getByText('test@example.com')).toBeInTheDocument();
+ expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled();
+ });
+
+ it('Handles errors', async () => {
+ mockSplunkOnCallApi.getOnCallUsers = 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/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx
new file mode 100644
index 0000000000..9538a0e73b
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 { splunkOnCallApiRef } from '../../api';
+import { useApi, Progress } from '@backstage/core';
+import { Alert } from '@material-ui/lab';
+import { User } from '../types';
+
+type Props = {
+ users: { [key: string]: User };
+ team: string;
+};
+
+export const EscalationPolicy = ({ users, team }: Props) => {
+ const api = useApi(splunkOnCallApiRef);
+
+ const { value: userNames, loading, error } = useAsync(async () => {
+ const oncalls = await api.getOnCallUsers();
+ const teamUsernames = oncalls
+ .filter(oncall => oncall.team?.name === team)
+ .flatMap(oncall => {
+ return oncall.oncallNow?.flatMap(oncallNow => {
+ return oncallNow.users?.flatMap(user => {
+ return user?.onCalluser?.username;
+ });
+ });
+ });
+ return teamUsernames;
+ });
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ if (!userNames?.length) {
+ return ;
+ }
+
+ return (
+ ON CALL}>
+ {userNames &&
+ userNames.map(
+ (userName, index) =>
+ userName &&
+ userName in users && (
+
+ ),
+ )}
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx
new file mode 100644
index 0000000000..4df0719696
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { User } from '../types';
+
+const useStyles = makeStyles({
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
+});
+
+type Props = {
+ user: User;
+};
+
+export const EscalationUser = ({ user }: Props) => {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+ {user.firstName} {user.lastName}
+
+ }
+ secondary={user.email}
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationUsersEmptyState.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationUsersEmptyState.tsx
new file mode 100644
index 0000000000..d587011601
--- /dev/null
+++ b/plugins/splunk-on-call/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/splunk-on-call/src/components/Escalation/index.ts b/plugins/splunk-on-call/src/components/Escalation/index.ts
new file mode 100644
index 0000000000..ac2db62cd9
--- /dev/null
+++ b/plugins/splunk-on-call/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/splunk-on-call/src/components/Incident/IncidentEmptyState.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentEmptyState.tsx
new file mode 100644
index 0000000000..f7a0398c55
--- /dev/null
+++ b/plugins/splunk-on-call/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/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx
new file mode 100644
index 0000000000..1bf233de86
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx
@@ -0,0 +1,241 @@
+/*
+ * 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, { useEffect } from 'react';
+import {
+ ListItem,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ Tooltip,
+ ListItemText,
+ makeStyles,
+ IconButton,
+ Typography,
+} from '@material-ui/core';
+import DoneIcon from '@material-ui/icons/Done';
+import DoneAllIcon from '@material-ui/icons/DoneAll';
+import {
+ StatusError,
+ StatusWarning,
+ StatusOK,
+ useApi,
+ alertApiRef,
+} from '@backstage/core';
+import { DateTime, Duration } from 'luxon';
+import { Incident, IncidentPhase } from '../types';
+import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
+import { splunkOnCallApiRef } from '../../api/client';
+import { useAsyncFn } from 'react-use';
+import { PatchIncidentRequest } from '../../api/types';
+
+const useStyles = makeStyles({
+ denseListIcon: {
+ marginRight: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ listItemPrimary: {
+ fontWeight: 'bold',
+ },
+ listItemIcon: {
+ minWidth: '1em',
+ },
+ secondaryAction: {
+ paddingRight: 48,
+ },
+});
+
+type Props = {
+ incident: Incident;
+ onIncidentAction: () => void;
+};
+
+const IncidentPhaseStatus = ({
+ currentPhase,
+}: {
+ currentPhase: IncidentPhase;
+}) => {
+ switch (currentPhase) {
+ case 'UNACKED':
+ return ;
+ case 'ACKED':
+ return ;
+ default:
+ return ;
+ }
+};
+
+const incidentPhaseTooltip = (currentPhase: IncidentPhase) => {
+ switch (currentPhase) {
+ case 'UNACKED':
+ return 'Triggered';
+ case 'ACKED':
+ return 'Acknowledged';
+ default:
+ return 'Resolved';
+ }
+};
+
+const IncidentAction = ({
+ currentPhase,
+ incidentNames,
+ resolveAction,
+ acknowledgeAction,
+}: {
+ currentPhase: string;
+ incidentNames: string[];
+ resolveAction: (args: PatchIncidentRequest) => void;
+ acknowledgeAction: (args: PatchIncidentRequest) => void;
+}) => {
+ switch (currentPhase) {
+ case 'UNACKED':
+ return (
+
+ acknowledgeAction({ incidentNames })}>
+
+
+
+ );
+ case 'ACKED':
+ return (
+
+ resolveAction({ incidentNames })}>
+
+
+
+ );
+ default:
+ return <>>;
+ }
+};
+
+export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
+ const classes = useStyles();
+ const duration =
+ new Date().getTime() - new Date(incident.startTime!).getTime();
+ const createdAt = DateTime.local()
+ .minus(Duration.fromMillis(duration))
+ .toRelative({ locale: 'en' });
+ const alertApi = useApi(alertApiRef);
+ const api = useApi(splunkOnCallApiRef);
+
+ const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-');
+
+ const user = hasBeenManuallyTriggered
+ ? incident.monitorName?.replace('vouser-', '')
+ : incident.monitorName;
+
+ const [
+ { value: resolveValue, error: resolveError },
+ handleResolveIncident,
+ ] = useAsyncFn(
+ async ({ incidentNames }: PatchIncidentRequest) =>
+ await api.resolveIncident({
+ incidentNames,
+ }),
+ );
+
+ const [
+ { value: acknowledgeValue, error: acknowledgeError },
+ handleAcknowledgeIncident,
+ ] = useAsyncFn(
+ async ({ incidentNames }: PatchIncidentRequest) =>
+ await api.acknowledgeIncident({
+ incidentNames,
+ }),
+ );
+
+ useEffect(() => {
+ if (acknowledgeValue) {
+ alertApi.post({
+ message: `Incident successfully acknowledged`,
+ });
+ }
+
+ if (resolveValue) {
+ alertApi.post({
+ message: `Incident successfully resolved`,
+ });
+ }
+ if (resolveValue || acknowledgeValue) {
+ onIncidentAction();
+ }
+ }, [acknowledgeValue, resolveValue, alertApi, onIncidentAction]);
+
+ if (acknowledgeError) {
+ alertApi.post({
+ message: `Failed to acknowledge incident. ${acknowledgeError.message}`,
+ severity: 'error',
+ });
+ }
+
+ if (resolveError) {
+ alertApi.post({
+ message: `Failed to resolve incident. ${resolveError.message}`,
+ severity: 'error',
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ Created {createdAt} {user && `by ${user}`}
+
+ }
+ />
+
+ {incident.incidentLink && incident.incidentNumber && (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx
new file mode 100644
index 0000000000..d1f973735c
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.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 { render, waitFor } from '@testing-library/react';
+import { Incidents } from './Incidents';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ alertApiRef,
+ ApiProvider,
+ ApiRegistry,
+ createApiRef,
+ IdentityApi,
+ identityApiRef,
+} from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks';
+
+const mockIdentityApi: Partial = {
+ getUserId: () => 'test',
+};
+
+const mockSplunkOnCallApi = {
+ getIncidents: () => [],
+ getTeams: () => [],
+};
+const apis = ApiRegistry.from([
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+ [identityApiRef, mockIdentityApi],
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+]);
+
+describe('Incidents', () => {
+ it('Renders an empty state when there are no incidents', async () => {
+ mockSplunkOnCallApi.getTeams = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_TEAM]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
+ });
+
+ it('Renders all incidents', async () => {
+ mockSplunkOnCallApi.getIncidents = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_INCIDENT]);
+
+ mockSplunkOnCallApi.getTeams = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCK_TEAM]);
+ const {
+ getByText,
+ getByTitle,
+ getAllByTitle,
+ getByLabelText,
+ queryByTestId,
+ } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(
+ getByText('user', {
+ exact: false,
+ }),
+ ).toBeInTheDocument();
+ expect(getByText('test-incident')).toBeInTheDocument();
+ expect(getByTitle('Acknowledged')).toBeInTheDocument();
+ expect(getByLabelText('Status warning')).toBeInTheDocument();
+
+ // assert links, mailto and hrefs, date calculation
+ expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1);
+ });
+
+ it('Handle errors', async () => {
+ mockSplunkOnCallApi.getIncidents = 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/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx
new file mode 100644
index 0000000000..e1a79314b9
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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, { useEffect } from 'react';
+import { List, ListSubheader } from '@material-ui/core';
+import { IncidentListItem } from './IncidentListItem';
+import { IncidentsEmptyState } from './IncidentEmptyState';
+import { useAsyncFn } from 'react-use';
+import { splunkOnCallApiRef } from '../../api';
+import { useApi, Progress } from '@backstage/core';
+import { Alert } from '@material-ui/lab';
+
+type Props = {
+ refreshIncidents: boolean;
+ team: string;
+};
+
+export const Incidents = ({ refreshIncidents, team }: Props) => {
+ const api = useApi(splunkOnCallApiRef);
+
+ const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
+ async () => {
+ const allIncidents = await api.getIncidents();
+ const teams = await api.getTeams();
+ const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug;
+ const filteredIncidents = teamSlug
+ ? allIncidents.filter(incident =>
+ incident.pagedTeams?.includes(teamSlug),
+ )
+ : [];
+ return filteredIncidents;
+ },
+ );
+
+ useEffect(() => {
+ getIncidents();
+ }, [refreshIncidents, getIncidents]);
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ if (!incidents?.length) {
+ return ;
+ }
+
+ return (
+ INCIDENTS}>
+ {incidents!.map((incident, index) => (
+ getIncidents()}
+ key={index}
+ incident={incident}
+ />
+ ))}
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/Incident/index.ts b/plugins/splunk-on-call/src/components/Incident/index.ts
new file mode 100644
index 0000000000..fb2702602b
--- /dev/null
+++ b/plugins/splunk-on-call/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/splunk-on-call/src/components/SplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx
new file mode 100644
index 0000000000..b1280ab21c
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx
@@ -0,0 +1,155 @@
+/*
+ * 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, act } from '@testing-library/react';
+import { SplunkOnCallCard } from './SplunkOnCallCard';
+import { Entity } from '@backstage/catalog-model';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ alertApiRef,
+ ApiProvider,
+ ApiRegistry,
+ ConfigApi,
+ configApiRef,
+ ConfigReader,
+ createApiRef,
+} from '@backstage/core';
+import {
+ splunkOnCallApiRef,
+ UnauthorizedError,
+ SplunkOnCallClient,
+} from '../api';
+import {
+ ESCALATION_POLICIES,
+ MOCKED_ON_CALL,
+ MOCKED_USER,
+ MOCK_INCIDENT,
+ MOCK_TEAM,
+} from '../api/mocks';
+
+const mockSplunkOnCallApi: Partial = {
+ getUsers: async () => [],
+ getIncidents: async () => [MOCK_INCIDENT],
+ getOnCallUsers: async () => MOCKED_ON_CALL,
+ getTeams: async () => [MOCK_TEAM],
+ getEscalationPolicies: async () => ESCALATION_POLICIES,
+};
+
+const configApi: ConfigApi = new ConfigReader({
+ splunkOnCall: {
+ username: MOCKED_USER.username,
+ },
+});
+
+const apis = ApiRegistry.from([
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+ [configApiRef, configApi],
+ [
+ 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: 'splunkoncall-test',
+ annotations: {
+ 'splunk.com/on-call-team': 'Example',
+ },
+ },
+};
+
+describe('SplunkOnCallCard', () => {
+ it('Render splunkoncall', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCKED_USER]);
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Create Incident')).toBeInTheDocument();
+ expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
+ expect(getByText('Empty escalation policy')).toBeInTheDocument();
+ });
+
+ it('Handles custom error for missing token', async () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockRejectedValueOnce(new UnauthorizedError());
+
+ const { getByText, queryByTestId } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(
+ getByText('Missing or invalid Splunk On-Call API key and/or API id'),
+ ).toBeInTheDocument();
+ });
+
+ it('handles general error', async () => {
+ mockSplunkOnCallApi.getUsers = 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 () => {
+ mockSplunkOnCallApi.getUsers = jest
+ .fn()
+ .mockImplementationOnce(async () => [MOCKED_USER]);
+
+ const { getByText, queryByTestId, getByTestId, getByRole } = render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+ await waitFor(() => !queryByTestId('progress'));
+ expect(getByText('Create Incident')).toBeInTheDocument();
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(getByRole('dialog')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx
new file mode 100644
index 0000000000..84d94d60d3
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx
@@ -0,0 +1,194 @@
+/*
+ * 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, useCallback } from 'react';
+import {
+ useApi,
+ Progress,
+ HeaderIconLinkRow,
+ MissingAnnotationEmptyState,
+ configApiRef,
+ EmptyState,
+} from '@backstage/core';
+import { Entity } from '@backstage/catalog-model';
+import {
+ Button,
+ makeStyles,
+ Card,
+ CardHeader,
+ Divider,
+ CardContent,
+ Typography,
+} from '@material-ui/core';
+import { Incidents } from './Incident';
+import { EscalationPolicy } from './Escalation';
+import { useAsync } from 'react-use';
+import { Alert } from '@material-ui/lab';
+import { splunkOnCallApiRef, UnauthorizedError } from '../api';
+import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
+import { TriggerDialog } from './TriggerDialog';
+import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError';
+import { User } from './types';
+
+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',
+ },
+ },
+});
+
+export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team';
+
+export const MissingTeamAnnotation = () => (
+
+);
+
+export const MissingUsername = () => (
+
+
+
+);
+
+export const isPluginApplicableToEntity = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]);
+
+type Props = {
+ entity: Entity;
+};
+
+export const SplunkOnCallCard = ({ entity }: Props) => {
+ const classes = useStyles();
+ const config = useApi(configApiRef);
+ const api = useApi(splunkOnCallApiRef);
+ const [showDialog, setShowDialog] = useState(false);
+ const [refreshIncidents, setRefreshIncidents] = useState(false);
+ const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM];
+
+ const username = config.getOptionalString('splunkOnCall.username');
+
+ const handleRefresh = useCallback(() => {
+ setRefreshIncidents(x => !x);
+ }, []);
+
+ const handleDialog = useCallback(() => {
+ setShowDialog(x => !x);
+ }, []);
+
+ const { value: users, loading, error } = useAsync(async () => {
+ const allUsers = await api.getUsers();
+ const usersHashMap = allUsers.reduce(
+ (map: Record, obj: User) => {
+ if (obj.username) {
+ map[obj.username] = obj;
+ }
+ return map;
+ },
+ {},
+ );
+ return { usersHashMap, userList: allUsers };
+ });
+
+ const incidentCreator =
+ username && users?.userList.find(user => user.username === username);
+
+ if (error instanceof UnauthorizedError) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+ Error encountered while fetching information. {error.message}
+
+ );
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ const Content = () => {
+ if (!team) {
+ return ;
+ }
+ if (!username || !incidentCreator) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {users?.usersHashMap && team && (
+
+ )}
+ {users && incidentCreator && (
+
+ )}
+ >
+ );
+ };
+
+ const triggerLink = {
+ label: 'Create Incident',
+ action: (
+
+ ),
+ icon: ,
+ };
+
+ return (
+
+ Team: {team},
+ username && (
+
+ ),
+ ]}
+ />
+
+
+
+
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx
new file mode 100644
index 0000000000..4222cbfbeb
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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, makeStyles } from '@material-ui/core';
+import {
+ Content,
+ ContentHeader,
+ Page,
+ Header,
+ SupportButton,
+} from '@backstage/core';
+import { SplunkOnCallCard } from './SplunkOnCallCard';
+import { useEntity } from '@backstage/plugin-catalog-react';
+
+const useStyles = makeStyles(() => ({
+ overflowXScroll: {
+ overflowX: 'scroll',
+ },
+}));
+
+export type SplunkOnCallPageProps = {
+ title?: string;
+ subtitle?: string;
+ pageTitle?: string;
+};
+
+export const SplunkOnCallPage = ({
+ title,
+ subtitle,
+ pageTitle,
+}: SplunkOnCallPageProps): JSX.Element => {
+ const classes = useStyles();
+ const { entity } = useEntity();
+
+ return (
+
+
+
+
+
+ This is used to help you automate incident management.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+SplunkOnCallPage.defaultProps = {
+ title: 'Splunk On-Call',
+ subtitle: 'Automate incident management',
+ pageTitle: 'Dashboard',
+};
diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx
new file mode 100644
index 0000000000..741a15fb41
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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, act } from '@testing-library/react';
+import { wrapInTestApp } from '@backstage/test-utils';
+import {
+ ApiRegistry,
+ alertApiRef,
+ createApiRef,
+ ApiProvider,
+} from '@backstage/core';
+import { splunkOnCallApiRef } from '../../api';
+import { TriggerDialog } from './TriggerDialog';
+import { ESCALATION_POLICIES, MOCKED_USER } from '../../api/mocks';
+
+describe('TriggerDialog', () => {
+ const mockTriggerAlarmFn = jest.fn();
+ const mockSplunkOnCallApi = {
+ triggerAlarm: mockTriggerAlarmFn,
+ getEscalationPolicies: async () => ESCALATION_POLICIES,
+ };
+
+ const apis = ApiRegistry.from([
+ [
+ alertApiRef,
+ createApiRef({
+ id: 'core.alert',
+ description: 'Used to report alerts and forward them to the app',
+ }),
+ ],
+ [splunkOnCallApiRef, mockSplunkOnCallApi],
+ ]);
+
+ it('open the dialog and trigger an alarm', async () => {
+ const { getByText, getByRole, getAllByRole, getByTestId } = render(
+ wrapInTestApp(
+
+ {}}
+ users={[MOCKED_USER]}
+ onIncidentCreated={() => {}}
+ />
+ ,
+ ),
+ );
+
+ expect(getByRole('dialog')).toBeInTheDocument();
+ expect(
+ getByText('This action will trigger an incident', {
+ exact: false,
+ }),
+ ).toBeInTheDocument();
+ const summary = getByTestId('trigger-summary-input');
+ const body = getByTestId('trigger-body-input');
+ const behavior = getByTestId('trigger-select-behavior');
+ const description = 'Test Trigger Alarm';
+ await act(async () => {
+ fireEvent.change(summary, { target: { value: description } });
+ fireEvent.change(body, { target: { value: description } });
+ fireEvent.change(behavior, { target: { value: '0' } });
+ fireEvent.mouseDown(getAllByRole('button')[0]);
+ });
+
+ // Trigger user targets select
+ const options = getAllByRole('option');
+ await act(async () => {
+ fireEvent.click(options[0]);
+ fireEvent.keyDown(options[0], {
+ key: 'Escape',
+ code: 'Escape',
+ keyCode: 27,
+ charCode: 27,
+ });
+ });
+
+ // Trigger policy targets select
+ await act(async () => {
+ fireEvent.mouseDown(getAllByRole('button')[1]);
+ });
+ const policiesOptions = getAllByRole('option');
+ await act(async () => {
+ fireEvent.click(policiesOptions[0]);
+ });
+
+ // Trigger incident creation button
+ const triggerButton = getByTestId('trigger-button');
+ await act(async () => {
+ fireEvent.click(triggerButton);
+ });
+ expect(mockTriggerAlarmFn).toHaveBeenCalled();
+ expect(mockTriggerAlarmFn).toHaveBeenCalledWith({
+ summary: description,
+ details: description,
+ userName: 'test_user',
+ targets: [
+ { slug: 'test_user', type: 'User' },
+ { slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' },
+ ],
+ isMultiResponder: false,
+ });
+ });
+});
diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx
new file mode 100644
index 0000000000..8132db985b
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx
@@ -0,0 +1,365 @@
+/*
+ * 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,
+ DialogActions,
+ Button,
+ DialogContent,
+ Typography,
+ CircularProgress,
+ Select,
+ MenuItem,
+ Input,
+ Chip,
+ createStyles,
+ makeStyles,
+ Theme,
+ FormControl,
+ InputLabel,
+} from '@material-ui/core';
+import { useApi, alertApiRef } from '@backstage/core';
+import { useAsync, useAsyncFn } from 'react-use';
+import { splunkOnCallApiRef } from '../../api';
+import { Alert } from '@material-ui/lab';
+import { User } from '../types';
+import { IncidentTarget, TargetType } from '../../api/types';
+
+const MenuProps = {
+ PaperProps: {
+ style: {
+ width: 250,
+ },
+ },
+};
+
+type Props = {
+ users: User[];
+ incidentCreator: User;
+ showDialog: boolean;
+ handleDialog: () => void;
+ onIncidentCreated: () => void;
+};
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ chips: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ },
+ chip: {
+ margin: 2,
+ },
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: `calc(100% - ${theme.spacing(2)}px)`,
+ },
+ targets: {
+ display: 'flex',
+ flexDirection: 'column',
+ width: '100%',
+ },
+ }),
+);
+
+export const TriggerDialog = ({
+ users,
+ incidentCreator,
+ showDialog,
+ handleDialog,
+ onIncidentCreated: onIncidentCreated,
+}: Props) => {
+ const alertApi = useApi(alertApiRef);
+ const api = useApi(splunkOnCallApiRef);
+ const classes = useStyles();
+
+ const [userTargets, setUserTargets] = useState([]);
+ const [policyTargets, setPolicyTargets] = useState([]);
+ const [detailsValue, setDetails] = useState('');
+ const [summaryValue, setSummary] = useState('');
+ const [isMultiResponderValue, setIsMultiResponder] = useState('1');
+
+ const [
+ { value, loading: triggerLoading, error: triggerError },
+ handleTriggerAlarm,
+ ] = useAsyncFn(
+ async (
+ summary: string,
+ details: string,
+ userName: string,
+ targets: IncidentTarget[],
+ isMultiResponder: boolean,
+ ) =>
+ await api.triggerAlarm({
+ summary,
+ details,
+ userName,
+ targets,
+ isMultiResponder,
+ }),
+ );
+
+ const {
+ value: policies,
+ loading: policiesLoaading,
+ error: policiesError,
+ } = useAsync(async () => {
+ const allPolicies = await api.getEscalationPolicies();
+ return allPolicies;
+ });
+
+ const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => {
+ setUserTargets(event.target.value as string[]);
+ };
+
+ const handlePolicyTargets = (
+ event: React.ChangeEvent<{ value: unknown }>,
+ ) => {
+ setPolicyTargets(event.target.value as string[]);
+ };
+
+ const detailsChanged = (event: React.ChangeEvent) => {
+ setDetails(event.target.value);
+ };
+
+ const summaryChanged = (event: React.ChangeEvent) => {
+ setSummary(event.target.value);
+ };
+
+ const isMultiResponderChanged = (
+ event: React.ChangeEvent<{ value: unknown }>,
+ ) => {
+ setIsMultiResponder(event.target.value as string);
+ };
+
+ const targets = (): IncidentTarget[] => [
+ ...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })),
+ ...policyTargets.map(user => ({
+ slug: user,
+ type: TargetType.EscalationPolicyValue,
+ })),
+ ];
+
+ useEffect(() => {
+ if (value) {
+ alertApi.post({
+ message: `Alarm successfully triggered`,
+ });
+ onIncidentCreated();
+ handleDialog();
+ }
+ }, [value, alertApi, handleDialog, onIncidentCreated]);
+
+ if (triggerError) {
+ alertApi.post({
+ message: `Failed to trigger alarm. ${triggerError.message}`,
+ severity: 'error',
+ });
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/index.ts b/plugins/splunk-on-call/src/components/TriggerDialog/index.ts
new file mode 100644
index 0000000000..655cef8504
--- /dev/null
+++ b/plugins/splunk-on-call/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/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts
new file mode 100644
index 0000000000..de5718cb28
--- /dev/null
+++ b/plugins/splunk-on-call/src/components/types.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 { IncidentTarget } from '../api/types';
+
+export type Team = {
+ name?: string;
+ slug?: string;
+ memberCount?: number;
+ version?: number;
+ isDefaultTeam?: boolean;
+ _selfUrl?: string;
+ _policiesUrl?: string;
+ _membersUrl?: string;
+ _adminsUrl?: string;
+};
+
+export type OnCall = {
+ team?: OnCallTeamResource;
+ oncallNow?: OnCallNowResource[];
+};
+
+export type OnCallTeamResource = {
+ name?: string;
+ slug?: string;
+};
+
+export type OnCallNowResource = {
+ escalationPolicy?: OnCallEscalationPolicyResource;
+ users?: OnCallUsersResource[];
+};
+
+export type OnCallEscalationPolicyResource = {
+ name?: string;
+ slug?: string;
+};
+
+export type OnCallUsersResource = {
+ onCalluser?: OnCallUser;
+};
+
+export type OnCallUser = {
+ username?: string;
+};
+
+export type User = {
+ firstName?: string;
+ lastName?: string;
+ displayName?: string;
+ username?: string;
+ email?: string;
+ createdAt?: string;
+ passwordLastUpdated?: string;
+ verified?: boolean;
+ _selfUrl?: string;
+};
+
+export type CreateIncidentRequest = {
+ summary: string;
+ details: string;
+ userName: string;
+ targets: IncidentTarget;
+ isMultiResponder: boolean;
+};
+
+export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED';
+
+export type Incident = {
+ incidentNumber?: string;
+ startTime?: string;
+ currentPhase: IncidentPhase;
+ entityState?: string;
+ entityType?: string;
+ routingKey?: string;
+ alertCount?: number;
+ lastAlertTime?: string;
+ lastAlertId?: string;
+ entityId?: string;
+ host?: string;
+ service?: string;
+ pagedUsers?: string[];
+ pagedTeams?: string[];
+ entityDisplayName?: string;
+ pagedPolicies?: EscalationPolicyInfo[];
+ transitions?: IncidentTransition[];
+ firstAlertUuid?: string;
+ monitorName?: string;
+ monitorType?: string;
+ incidentLink?: string;
+};
+
+export type EscalationPolicyInfo = {
+ policy: EscalationPolicySummary;
+ team?: EscalationPolicyTeam;
+};
+
+export type IncidentTransition = {
+ name?: string;
+ at?: string;
+ by?: string;
+ message?: string;
+ manually?: boolean;
+ alertId?: string;
+ alertUrl?: string;
+};
+
+export type EscalationPolicySummary = {
+ name: string;
+ slug: string;
+ _selfUrl: string;
+};
+
+export type EscalationPolicyTeam = {
+ name: string;
+ slug: string;
+};
diff --git a/plugins/splunk-on-call/src/index.ts b/plugins/splunk-on-call/src/index.ts
new file mode 100644
index 0000000000..c8de96f8ea
--- /dev/null
+++ b/plugins/splunk-on-call/src/index.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 {
+ splunkOnCallPlugin,
+ splunkOnCallPlugin as plugin,
+ SplunkOnCallPage,
+} from './plugin';
+export {
+ isPluginApplicableToEntity,
+ SplunkOnCallCard,
+} from './components/SplunkOnCallCard';
+export {
+ SplunkOnCallClient,
+ splunkOnCallApiRef,
+ UnauthorizedError,
+} from './api/client';
diff --git a/plugins/splunk-on-call/src/plugin.test.ts b/plugins/splunk-on-call/src/plugin.test.ts
new file mode 100644
index 0000000000..b846eba706
--- /dev/null
+++ b/plugins/splunk-on-call/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 { splunkOnCallPlugin } from './plugin';
+
+describe('splunk-on-call', () => {
+ it('should export plugin', () => {
+ expect(splunkOnCallPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/splunk-on-call/src/plugin.ts b/plugins/splunk-on-call/src/plugin.ts
new file mode 100644
index 0000000000..835cda48df
--- /dev/null
+++ b/plugins/splunk-on-call/src/plugin.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 {
+ createApiFactory,
+ createPlugin,
+ createRouteRef,
+ discoveryApiRef,
+ configApiRef,
+ createRoutableExtension,
+} from '@backstage/core';
+import { splunkOnCallApiRef, SplunkOnCallClient } from './api';
+
+export const rootRouteRef = createRouteRef({
+ title: 'splunk-on-call',
+});
+
+export const splunkOnCallPlugin = createPlugin({
+ id: 'splunk-on-call',
+ apis: [
+ createApiFactory({
+ api: splunkOnCallApiRef,
+ deps: { discoveryApi: discoveryApiRef, configApi: configApiRef },
+ factory: ({ configApi, discoveryApi }) =>
+ SplunkOnCallClient.fromConfig(configApi, discoveryApi),
+ }),
+ ],
+ routes: {
+ root: rootRouteRef,
+ },
+});
+
+export const SplunkOnCallPage = splunkOnCallPlugin.provide(
+ createRoutableExtension({
+ component: () =>
+ import('./components/SplunkOnCallPage').then(m => m.SplunkOnCallPage),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/splunk-on-call/src/setupTests.ts b/plugins/splunk-on-call/src/setupTests.ts
new file mode 100644
index 0000000000..0bfa67b49a
--- /dev/null
+++ b/plugins/splunk-on-call/src/setupTests.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.
+ */
+import '@testing-library/jest-dom';
diff --git a/yarn.lock b/yarn.lock
index e2c8103d8e..7c9295d77f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1865,6 +1865,21 @@
remark-gfm "^1.0.0"
zen-observable "^0.8.15"
+"@backstage/plugin-catalog-react@^0.0.2":
+ version "0.0.2"
+ resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52"
+ integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA==
+ dependencies:
+ "@backstage/catalog-client" "^0.3.5"
+ "@backstage/catalog-model" "^0.7.1"
+ "@backstage/core" "^0.6.0"
+ "@material-ui/core" "^4.11.0"
+ "@types/react" "^16.9"
+ react "^16.13.1"
+ react-router "6.0.0-beta.0"
+ react-router-dom "6.0.0-beta.0"
+ react-use "^15.3.3"
+
"@backstage/plugin-catalog@^0.2.0":
version "0.3.1"
dependencies: