Merge pull request #11754 from isand3r/master

feat: add dynatrace plugin
This commit is contained in:
Fredrik Adelöw
2022-06-13 14:31:18 +02:00
committed by GitHub
35 changed files with 1162 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-dynatrace': minor
---
Adds Dynatrace plugin
+2
View File
@@ -85,6 +85,8 @@ dockerfiles
Dockerize
dockerode
Docusaurus
dynatrace
Dynatrace
ecco
env
Env
+8
View File
@@ -115,6 +115,11 @@ proxy:
headers:
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
'/dynatrace':
target: https://your.dynatrace.instance.com/api/v2
headers:
Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}'
organization:
name: My Company
@@ -131,6 +136,9 @@ techdocs:
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives.
dynatrace:
baseUrl: https://your.dynatrace.instance.com
sentry:
organization: my-company
+12
View File
@@ -0,0 +1,12 @@
---
title: Dynatrace
author: TELUS
authorUrl: https://github.com/telus
category: Monitoring
description: View monitoring info from dynatrace for services in your software catalog.
documentation: https://github.com/backstage/backstage/tree/master/plugins/dynatrace
iconUrl: img/dynatrace.svg
npmPackageName: '@backstage/plugin-dynatrace'
tags:
- dynatrace
- monitoring
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="370px"
height="344px" viewBox="0 0 370 344" enable-background="new 0 0 370 344" xml:space="preserve">
<g id="Background">
</g>
<g id="Vertical">
</g>
<g id="Horizontal">
</g>
<g id="Dynatrace_Color">
</g>
<g id="Variants">
<g>
<g>
<g>
<path fill="#FFFFFF" d="M241.9,282.2h-10.6c-3,0-5.1,0.6-6.3,1.8c-1.3,1.2-1.9,3.3-1.9,6.2l0,36.6h-6.4l0-37.1
c0-2.8,0.5-5.9,2-8.2c2.7-4.3,7.9-5.1,11.7-5.1h11.5V282.2z"/>
</g>
<g>
<path fill="#FFFFFF" d="M201.1,321.2c-3,0-5.1-0.6-6.3-1.8c-1.3-1.2-1.9-3.2-1.9-6.1v-31h13.3v-5.7h-13.3v-15.1h-6.4v52.2
c0,2.8,0.5,5.9,2,8.2c2.7,4.3,7.9,5.1,11.7,5.1h11.5v-5.7H201.1z"/>
</g>
<path fill="#FFFFFF" d="M35.1,261.4v15.1l-13.3,0c-8.1,0-13,2.4-16,5.3c-4.7,4.5-4.7,11-4.7,11.7c0,0.8,0,15.4,0,16.2
c0,0.7,0,7.2,4.7,11.7c3,2.9,7.9,5.3,16,5.3h5.9c3.8,0,9-0.8,11.7-5.1c1.5-2.3,2-5.4,2-8.2v-52.2H35.1z M33.2,319.3
c-1.3,1.2-3.3,1.8-6.3,1.8h-5.7c-5.2,0-8.5-1.5-10.5-3.5c-2.4-2.3-3.2-5.3-3.2-7.7v-16.6c0-2.4,0.8-5.4,3.2-7.7
c2-2,5.3-3.5,10.5-3.5l13.9,0l0,31C35.1,316.1,34.4,318.1,33.2,319.3z"/>
<path fill="#FFFFFF" d="M299.3,285.7c2-2,5.3-3.5,10.5-3.5h15.3v-5.7h-14.6c-8.1,0-13,2.4-16,5.3c-4.7,4.5-4.7,11-4.7,11.7
c0,0.4,0,15.8,0,16.2c0,0.7,0,7.2,4.7,11.7c3,2.9,7.9,5.3,16,5.3h14.6v-5.7h-15.3c-5.2,0-8.5-1.5-10.5-3.5
c-2.4-2.3-3.2-5.3-3.2-7.7v-16.6C296.2,291,297,288,299.3,285.7z"/>
<path fill="#FFFFFF" d="M180,293.6c0-0.7,0-7.2-4.7-11.7c-3-2.9-7.9-5.3-16-5.3h-14.1v5.7H160c5.2,0,8.5,1.5,10.5,3.5
c2.4,2.3,3.2,5.3,3.2,7.7v4.8h-20.2c-3.8,0-9,0.8-11.7,5.1c-1.5,2.3-2,5.4-2,8.2v2c0,2.8,0.5,5.9,2,8.2c2.7,4.3,7.9,5.1,11.7,5.1
h12.8c3.8,0,9-0.8,11.7-5.1c1.5-2.3,2-5.4,2-8.2C180,313.6,180,296.9,180,293.6z M171.7,319.3c-1.3,1.2-3.3,1.8-6.3,1.8h-11.1
c-3,0-5.1-0.6-6.3-1.8c-1.3-1.2-1.9-3.2-1.9-6.1v-1.4c0-2.9,0.6-4.9,1.9-6.1c1.3-1.2,3.3-1.8,6.3-1.8h19.4l0,9.3
C173.6,316.1,173,318.1,171.7,319.3z"/>
<path fill="#FFFFFF" d="M284.8,293.6c0-0.7,0-7.2-4.7-11.7c-3-2.9-7.9-5.3-16-5.3H250v5.7h14.7c5.2,0,8.5,1.5,10.5,3.5
c2.4,2.3,3.2,5.3,3.2,7.7v4.8h-20.2c-3.8,0-9,0.8-11.7,5.1c-1.5,2.3-2,5.4-2,8.2v2c0,2.8,0.5,5.9,2,8.2c2.7,4.3,7.9,5.1,11.7,5.1
H271c3.8,0,9-0.8,11.7-5.1c1.5-2.3,2-5.4,2-8.2C284.8,313.6,284.8,296.9,284.8,293.6z M276.5,319.3c-1.3,1.2-3.3,1.8-6.3,1.8H259
c-3,0-5.1-0.6-6.3-1.8c-1.3-1.2-1.9-3.2-1.9-6.1v-1.4c0-2.9,0.6-4.9,1.9-6.1c1.3-1.2,3.3-1.8,6.3-1.8h19.4l0,9.3
C278.4,316.1,277.8,318.1,276.5,319.3z"/>
<polygon fill="#FFFFFF" points="91.8,276.5 85.2,276.5 69.1,318.3 53.1,276.5 46.5,276.5 65.8,326.9 60,342 66.6,342 "/>
<path fill="#FFFFFF" d="M134.6,293.6c0-0.7,0-7.2-4.6-11.7c-2.9-2.9-7.6-5.2-15.2-5.3h-0.7c-7.6,0.1-12.3,2.5-15.2,5.3
c-4.6,4.5-4.6,11-4.6,11.7c0,0.8,0,30.7,0,33.3h6.3l0-33.5c0-2.4,0.8-5.4,3.1-7.7c2-1.9,5.7-3.4,10.7-3.4c5,0.1,8.8,1.5,10.7,3.4
c2.3,2.3,3.1,5.3,3.1,7.7l0,33.5h6.3C134.6,324.3,134.6,294.4,134.6,293.6z"/>
<path fill="#FFFFFF" d="M364.3,281.9c-2.9-2.9-7.6-5.2-15.2-5.3h-0.7c-7.6,0.1-12.3,2.5-15.2,5.3c-4.6,4.5-4.6,11-4.6,11.7v16.2
c0,0.7,0,7.2,4.6,11.7c2.9,2.9,7.6,5.2,15.2,5.3h14.9v-5.7l-14.6,0c-5-0.1-8.8-1.5-10.7-3.4c-2.3-2.3-3.1-5.3-3.1-7.7v-6h34
v-10.5C368.9,292.9,368.9,286.4,364.3,281.9z M334.9,298.3v-5c0-2.4,0.8-5.4,3.1-7.7c2-1.9,5.7-3.4,10.7-3.4
c5,0.1,8.8,1.5,10.7,3.4c2.3,2.3,3.1,5.3,3.1,7.7l0,5H334.9z"/>
</g>
<g>
<path fill="#1496FF" d="M151,19.8c-2.7,14.5-6.2,35.9-8,57.6c-3.2,38.4-1.2,64-1.2,64l-53.9,51.3c0,0-4.2-28.8-6.3-61.2
c-1.3-20-1.7-37.7-1.8-48.4c0-0.6,0.3-1.2,0.3-1.8c0-0.7,0.9-7.9,7.9-14.5C95.7,59.5,151.8,15.5,151,19.8z"/>
<path fill="#1284EA" d="M151,19.8c-2.7,14.5-6.2,35.9-8,57.6c0,0-59.7-7.1-63.2,7.3c0-0.7,1-9.5,8-16.2
C95.5,61.1,151.8,15.5,151,19.8z"/>
<path fill="#B4DC00" d="M79.8,81.3c0,1,0,2.1,0,3.3c0.6-2.6,1.6-4.5,3.8-7.2c4.4-5.6,11.6-7.1,14.5-7.5
c14.6-1.9,36.2-4.3,57.9-4.9c38.5-1.2,63.9,2,63.9,2l53.9-51.3c0,0-28.3-5.4-60.6-9.2c-21.1-2.5-39.7-3.8-50.1-4.4
c-0.7,0-8.2-1-15.2,5.7c-7.7,7.3-46.3,43.9-61.8,58.7C79.3,73.1,79.8,80.6,79.8,81.3z"/>
<path fill="#6F2DA8" d="M271.9,146.8c-14.6,2-36.1,4.4-57.9,5.1c-38.5,1.3-64-1.9-64-1.9L96,201.3c0,0,28.6,5.6,60.8,9.3
c19.7,2.3,37.3,3.5,47.9,4.1c0.7,0,2-0.6,2.7-0.6c0.7,0,8.2-1.3,15.2-8C230.4,198.8,276.2,146.3,271.9,146.8z"/>
<path fill="#591F91" d="M271.9,146.8c-14.6,2-36.1,4.4-57.9,5.1c0,0,4.2,60-10.4,62.7c0.7,0,10.6-0.5,17.6-7.1
C228.9,200.2,276.2,146.3,271.9,146.8z"/>
<path fill="#73BE28" d="M206.9,214.9c-1-0.1-2.1-0.1-3.3-0.2c2.7-0.5,4.5-1.4,7.4-3.4c5.8-4.2,7.7-11.2,8.2-14.1
c2.6-14.5,6-35.9,7.8-57.6c3.1-38.4,1.1-63.9,1.1-63.9l53.9-51.3c0,0,4,28.5,6.2,60.9c1.4,21.2,1.8,39.8,1.9,50.2
c0,0.7,0.6,8.2-6.4,14.9c-7.7,7.3-46.2,44.1-61.7,58.9C215.1,215.8,207.6,214.9,206.9,214.9z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+1
View File
@@ -29,6 +29,7 @@
"@backstage/plugin-cloudbuild": "^0.3.6-next.1",
"@backstage/plugin-code-coverage": "^0.1.33-next.1",
"@backstage/plugin-cost-insights": "^0.11.28-next.2",
"@backstage/plugin-dynatrace": "^0.0.0",
"@backstage/plugin-explore": "^0.3.37-next.1",
"@backstage/plugin-gcalendar": "^0.3.2-next.1",
"@backstage/plugin-gcp-projects": "^0.3.25-next.1",
@@ -73,6 +73,10 @@ import {
isCloudbuildAvailable,
} from '@backstage/plugin-cloudbuild';
import { EntityCodeCoverageContent } from '@backstage/plugin-code-coverage';
import {
DynatraceTab,
isDynatraceAvailable,
} from '@backstage/plugin-dynatrace';
import {
EntityGithubActionsContent,
EntityRecentGithubActionsRunsCard,
@@ -447,6 +451,14 @@ const serviceEntityPage = (
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
<EntityLayout.Route
path="/dynatrace"
title="Dynatrace"
if={isDynatraceAvailable}
>
<DynatraceTab />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
@@ -495,6 +507,14 @@ const websiteEntityPage = (
<EntityKubernetesContent />
</EntityLayout.Route>
<EntityLayout.Route
path="/dynatrace"
title="Dynatrace"
if={isDynatraceAvailable}
>
<DynatraceTab />
</EntityLayout.Route>
<EntityLayout.Route
if={isAzureDevOpsAvailable}
path="/git-tags"
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+56
View File
@@ -0,0 +1,56 @@
# Dynatrace
Welcome to the Dynatrace plugin!
## Getting started
This plugin uses the Backstage proxy to communicate with Dynatrace's REST APIs.
### Setup
#### Dynatrace API Key
The Dynatrace plugin will require the following information, to be used in the configuration options detailed below:
- Dynatrace API URL, e.g. `https://my-dynatrace-instance.dynatrace.com/api/v2`
- Dynatrace API access token (see [documentation](https://www.dynatrace.com/support/help/dynatrace-api/basics/dynatrace-api-authentication)), with the following permissions:
- `entities.read`
- `problems.read`
#### Plugin Configuration
This plugin requires a proxy endpoint for Dynatrace configured in `app-config.yaml` like so:
```yaml
proxy:
'/dynatrace':
target: 'https://example.dynatrace.com/api/v2'
headers:
Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}'
```
It also requires a baseUrl for rendering links to problems in the table like so:
```yaml
dynatrace:
baseUrl: 'https://example.dynatrace.com'
```
#### Catalog Configuration
To show information from Dynatrace for a catalog entity, add the following annotation to `catalog-info.yaml`:
```yaml
# catalog-info.yaml
# [...]
metadata:
annotations:
dynatrace.com/dynatrace-entity-id: DYNATRACE_ENTITY_ID
# [...]
```
The `DYNATRACE_ENTITY_ID` can be found in Dynatrace by browsing to the entity (a service, synthetic, frontend, workload, etc.). It will be located in the browser address bar in the `id` parameter and has the format `ENTITY_TYPE-ENTITY_ID`, where `ENTITY_TYPE` will be one of `SERVICE`, `SYNTHETIC_TEST`, or other, and `ENTITY_ID` will be a string of characters containing uppercase letters and numbers.
## Disclaimer
This plugin is not officially supported by Dynatrace.
+23
View File
@@ -0,0 +1,23 @@
## API Report File for "@backstage/plugin-dynatrace"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
// @public
export const dynatracePlugin: BackstagePlugin<{}, {}>;
// @public
export const DynatraceTab: () => JSX.Element;
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
//
// @public
export const isDynatraceAvailable: (entity: Entity) => boolean;
// (No @packageDocumentation comment for this package)
```
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 interface Config {
/**
* dynatrace config
* @visibility frontend
*/
dynatrace?: {
/**
* base url for links
* @visibility frontend
*/
baseUrl: string;
};
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { dynatracePlugin, DynatraceTab } from '../src/plugin';
createDevApp()
.registerPlugin(dynatracePlugin)
.addPage({
element: <DynatraceTab />,
title: 'Root Page',
path: '/dynatrace',
})
.render();
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@backstage/plugin-dynatrace",
"version": "0.0.0",
"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"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.0.3-next.0",
"@backstage/core-components": "^0.9.5-next.1",
"@backstage/core-plugin-api": "^1.0.3-next.0",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"cross-fetch": "^3.1.5",
"react-use": "^17.2.4"
},
"peerDependencies": {
"@backstage/plugin-catalog-react": "^1.1.1-next.1",
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.2-next.1",
"@backstage/core-app-api": "^1.0.3-next.0",
"@backstage/dev-utils": "^1.0.3-next.1",
"@backstage/test-utils": "^1.1.1-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/jest": "*",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"express": "^4.18.1",
"msw": "^0.42.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 } from '@backstage/core-plugin-api';
export type DynatraceEntity = {
entityId: {
id: string;
type: string;
};
name: string;
};
export type DynatraceProblem = {
problemId: string;
impactLevel: string;
status: string;
startTime: number;
endTime: number;
title: string;
severityLevel: string;
rootCauseEntity: DynatraceEntity;
affectedEntities: Array<DynatraceEntity>;
};
export interface DynatraceProblems {
problems: Array<DynatraceProblem>;
}
export const dynatraceApiRef = createApiRef<DynatraceApi>({
id: 'plugin.dynatrace.service',
});
export type DynatraceApi = {
getDynatraceProblems(
dynatraceEntityId: string,
): Promise<DynatraceProblems | undefined>;
};
@@ -0,0 +1,66 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { DynatraceProblems, DynatraceApi } from './DynatraceApi';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
export class DynatraceClient implements DynatraceApi {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
constructor({
discoveryApi,
fetchApi,
}: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = discoveryApi;
this.fetchApi = fetchApi;
}
private async callApi<T>(
path: string,
query: { [key in string]: any },
): Promise<T | undefined> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/dynatrace`;
const response = await this.fetchApi.fetch(
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
{
headers: {
'Content-Type': 'application/json',
},
},
);
if (response.status === 200) {
return (await response.json()) as T;
}
throw new Error(
`Dynatrace API call failed: ${response.status}:${response.statusText}`,
);
}
async getDynatraceProblems(
dynatraceEntityId: string,
): Promise<DynatraceProblems | undefined> {
if (!dynatraceEntityId) {
throw new Error('Dynatrace entity ID is required');
}
return this.callApi('problems', {
entitySelector: `entityId(${dynatraceEntityId})`,
});
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type {
DynatraceProblems,
DynatraceProblem,
DynatraceEntity,
} from './DynatraceApi';
export { dynatraceApiRef } from './DynatraceApi';
export { DynatraceClient } from './DynatraceClient';
@@ -0,0 +1,56 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Grid } from '@material-ui/core';
import {
Page,
Content,
ContentHeader,
SupportButton,
MissingAnnotationEmptyState,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { ProblemsList } from '../Problems/ProblemsList';
import { isDynatraceAvailable } from '../../plugin';
import { DYNATRACE_ID_ANNOTATION } from '../../constants';
export const DynatraceTab = () => {
const { entity } = useEntity();
if (!isDynatraceAvailable(entity)) {
return <MissingAnnotationEmptyState annotation={DYNATRACE_ID_ANNOTATION} />;
}
const dynatraceEntityId: string =
entity?.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]!;
return (
<Page themeId="tool">
<Content>
<ContentHeader title="Dynatrace">
<SupportButton>
Plugin to show information from Dynatrace
</SupportButton>
</ContentHeader>
<Grid container spacing={2}>
<Grid item xs={12} lg={12}>
<ProblemsList dynatraceEntityId={dynatraceEntityId} />
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { DynatraceTab } from './DynatraceTab';
@@ -0,0 +1,29 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemStatus } from './ProblemStatus';
import { renderInTestApp } from '@backstage/test-utils';
describe('ProblemStatus', () => {
it('renders StatusOK for a closed issue', async () => {
const rendered = await renderInTestApp(<ProblemStatus status="closed" />);
expect(await rendered.findByText('Closed')).toBeInTheDocument();
});
it('renders StatusError for an open issue', async () => {
const rendered = await renderInTestApp(<ProblemStatus status="open" />);
expect(await rendered.findByText('Open')).toBeInTheDocument();
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { DynatraceProblem } from '../../../api/DynatraceApi';
import { StatusError, StatusOK } from '@backstage/core-components';
export const ProblemStatus = ({ status }: Partial<DynatraceProblem>) => {
switch (status?.toLocaleLowerCase()) {
case 'open':
return (
<>
<StatusError />
Open
</>
);
case 'closed':
return (
<>
<StatusOK />
Closed
</>
);
default:
return <></>;
}
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemStatus } from './ProblemStatus';
@@ -0,0 +1,55 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemsList } from './ProblemsList';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { dynatraceApiRef } from '../../../api';
import { problems } from '../../../mocks/problems.json';
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
const mockDynatraceApi = {
getDynatraceProblems: jest.fn(),
};
const apis = TestApiRegistry.from(
[dynatraceApiRef, mockDynatraceApi],
[configApiRef, new ConfigReader({ dynatrace: { baseUrl: '__dynatrace__' } })],
);
describe('ProblemStatus', () => {
it('renders a table with problem data', async () => {
mockDynatraceApi.getDynatraceProblems = jest
.fn()
.mockResolvedValue({ problems });
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ProblemsList dynatraceEntityId="example-service-3" />
</ApiProvider>,
);
expect(await rendered.findByText('example-service')).toBeInTheDocument();
});
it('renders "nothing to report :)" if no problems are found', async () => {
mockDynatraceApi.getDynatraceProblems = jest.fn().mockResolvedValue({});
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ProblemsList dynatraceEntityId="example-service-3" />
</ApiProvider>,
);
expect(
await rendered.findByText('Nothing to report :)'),
).toBeInTheDocument();
});
});
@@ -0,0 +1,43 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 useAsync from 'react-use/lib/useAsync';
import { Progress } from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import { useApi } from '@backstage/core-plugin-api';
import { ProblemsTable } from '../ProblemsTable';
import { dynatraceApiRef } from '../../../api';
type ProblemsListProps = {
dynatraceEntityId: string;
};
export const ProblemsList = (props: ProblemsListProps) => {
const { dynatraceEntityId } = props;
const dynatraceApi = useApi(dynatraceApiRef);
const { value, loading, error } = useAsync(async () => {
return dynatraceApi.getDynatraceProblems(dynatraceEntityId);
}, [dynatraceApi, dynatraceEntityId]);
const problems = value?.problems;
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!problems) return <div>Nothing to report :)</div>;
return <ProblemsTable problems={problems} />;
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemsList } from './ProblemsList';
@@ -0,0 +1,44 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemsTable } from './ProblemsTable';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { problems } from '../../../mocks/problems.json';
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
describe('ProblemsTable', () => {
const apis = TestApiRegistry.from([
configApiRef,
new ConfigReader({ dynatrace: { baseUrl: '__dynatrace__' } }),
]);
it('renders the table with some problem data', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ProblemsTable problems={problems} />,
</ApiProvider>,
);
expect(await rendered.findByText('example-service')).toBeInTheDocument();
});
it('renders an empty table when no data is provided', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ProblemsTable problems={[]} />
</ApiProvider>,
);
expect(await rendered.findByText('Problems')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { Table, TableColumn } from '@backstage/core-components';
import { DynatraceProblem } from '../../../api/DynatraceApi';
import { ProblemStatus } from '../ProblemStatus';
import { configApiRef } from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
import { Link } from '@material-ui/core';
type ProblemsTableProps = {
problems: DynatraceProblem[];
};
export const ProblemsTable = ({ problems }: ProblemsTableProps) => {
const configApi = useApi(configApiRef);
const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl');
const columns: TableColumn[] = [
{
title: 'Title',
field: 'title',
render: (row: Partial<DynatraceProblem>) => (
<Link
href={`${dynatraceBaseUrl}/#problems/problemdetails;pid=${row.problemId}`}
>
{row.title}
</Link>
),
},
{
title: 'Status',
field: 'status',
render: (row: Partial<DynatraceProblem>) => (
<ProblemStatus status={row.status} />
),
},
{ title: 'Severity', field: 'severityLevel' },
{
title: 'Root Cause',
field: 'rootCauseEntity',
render: (row: Partial<DynatraceProblem>) => row.rootCauseEntity?.name,
},
{
title: 'Affected',
field: 'affectedEntities',
render: (row: Partial<DynatraceProblem>) =>
row.affectedEntities?.map(e => e.name),
},
{
title: 'Start Time',
field: 'startTime',
render: (row: Partial<DynatraceProblem>) =>
new Date(row.startTime || 0).toString(),
},
{
title: 'End Time',
field: 'endTime',
render: (row: Partial<DynatraceProblem>) =>
row.endTime === -1 ? 'ongoing' : new Date(row.endTime || 0).toString(),
},
];
return (
<Table
title="Problems"
options={{ search: true, paging: true }}
columns={columns}
data={problems.map(p => {
return { ...p, id: p.problemId };
})}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { ProblemsTable } from './ProblemsTable';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 const DYNATRACE_ID_ANNOTATION = 'dynatrace.com/dynatrace-entity-id';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { dynatracePlugin, DynatraceTab, isDynatraceAvailable } from './plugin';
+135
View File
@@ -0,0 +1,135 @@
{
"totalCount": 0,
"pageSize": 0,
"nextPageKey": "AQAAABQBAAAABQ==",
"problems": [
{
"managementZones": [
{
"name": "string",
"id": "string"
}
],
"severityLevel": "AVAILABILITY",
"entityTags": [
{
"stringRepresentation": "string",
"value": "string",
"key": "string",
"context": "string"
}
],
"problemId": "string",
"displayId": "string",
"affectedEntities": [
{
"entityId": {
"id": "string",
"type": "string"
},
"name": "my-service"
},
{
"entityId": {
"id": "string",
"type": "string"
},
"name": "example-service-3"
}
],
"rootCauseEntity": {
"entityId": {
"id": "string",
"type": "string"
},
"name": "example-service"
},
"impactedEntities": [
{
"entityId": {
"id": "string",
"type": "string"
},
"name": "HELP ME"
},
{
"entityId": {
"id": "string",
"type": "string"
},
"name": "HELP ME 2"
}
],
"linkedProblemInfo": {
"problemId": "string",
"displayId": "string"
},
"problemFilters": [
{
"name": "string",
"id": "string"
}
],
"evidenceDetails": {
"totalCount": 0,
"details": [
{
"evidenceType": "AVAILABILITY_EVIDENCE",
"displayName": "string",
"entity": {
"entityId": {
"id": "string",
"type": "string"
},
"name": "string"
},
"groupingEntity": {
"entityId": {
"id": "string",
"type": "string"
},
"name": "string"
},
"rootCauseRelevant": true,
"startTime": 0
}
]
},
"recentComments": {
"comments": [
{
"createdAtTimestamp": 0,
"authorName": "string",
"context": "string",
"id": "string",
"content": "string"
}
],
"totalCount": 0,
"pageSize": 0,
"nextPageKey": "AQAAABQBAAAABQ=="
},
"impactAnalysis": {
"impacts": [
{
"impactType": "APPLICATION",
"impactedEntity": {
"entityId": {
"id": "string",
"type": "string"
},
"name": "string"
},
"estimatedAffectedUsers": 0
}
]
},
"impactLevel": "APPLICATION",
"status": "OPEN",
"startTime": 0,
"endTime": 0,
"title": "this IS a big problem"
}
],
"warnings": ["string"]
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { dynatracePlugin } from './plugin';
describe('dynatrace', () => {
it('should export plugin', () => {
expect(dynatracePlugin).toBeDefined();
});
});
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { dynatraceApiRef, DynatraceClient } from './api';
import {
createApiFactory,
createPlugin,
discoveryApiRef,
fetchApiRef,
createRoutableExtension,
} from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { DYNATRACE_ID_ANNOTATION } from './constants';
import { rootRouteRef } from './routes';
/**
* Create the Dynatrace plugin.
* @public
*/
export const dynatracePlugin = createPlugin({
id: 'dynatrace',
apis: [
createApiFactory({
api: dynatraceApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new DynatraceClient({
discoveryApi,
fetchApi,
}),
}),
],
});
/**
* Checks if the entity has a dynatrace id annotation.
* @public
* @param entity {Entity} - The entity to check for the dynatrace id annotation.
*/
export const isDynatraceAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]);
/**
* Creates a routable extension for the dynatrace plugin tab.
* @public
*/
export const DynatraceTab = dynatracePlugin.provide(
createRoutableExtension({
name: 'DynatraceTab',
component: () =>
import('./components/DynatraceTab').then(m => m.DynatraceTab),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'dynatrace',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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';
import 'cross-fetch/polyfill';
+2 -1
View File
@@ -12407,6 +12407,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
"@backstage/plugin-cloudbuild" "^0.3.6-next.1"
"@backstage/plugin-code-coverage" "^0.1.33-next.1"
"@backstage/plugin-cost-insights" "^0.11.28-next.2"
"@backstage/plugin-dynatrace" "^0.0.0"
"@backstage/plugin-explore" "^0.3.37-next.1"
"@backstage/plugin-gcalendar" "^0.3.2-next.1"
"@backstage/plugin-gcp-projects" "^0.3.25-next.1"
@@ -12618,7 +12619,7 @@ express-xml-bodyparser@^0.3.0:
dependencies:
xml2js "^0.4.11"
express@^4.17.1, express@^4.17.3:
express@^4.17.1, express@^4.17.3, express@^4.18.1:
version "4.18.1"
resolved "https://registry.npmjs.org/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==