diff --git a/.changeset/many-yaks-occur.md b/.changeset/many-yaks-occur.md
new file mode 100644
index 0000000000..d8f617bf11
--- /dev/null
+++ b/.changeset/many-yaks-occur.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-dynatrace': minor
+---
+
+Adds Dynatrace plugin
diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt
index 4594b40b3e..8428dc2d0e 100644
--- a/.github/vale/Vocab/Backstage/accept.txt
+++ b/.github/vale/Vocab/Backstage/accept.txt
@@ -85,6 +85,8 @@ dockerfiles
Dockerize
dockerode
Docusaurus
+dynatrace
+Dynatrace
ecco
env
Env
diff --git a/app-config.yaml b/app-config.yaml
index eb51c59847..5919deb1ee 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -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
diff --git a/microsite/data/plugins/dynatrace.yaml b/microsite/data/plugins/dynatrace.yaml
new file mode 100644
index 0000000000..3ab85ae1eb
--- /dev/null
+++ b/microsite/data/plugins/dynatrace.yaml
@@ -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
diff --git a/microsite/static/img/dynatrace.svg b/microsite/static/img/dynatrace.svg
new file mode 100644
index 0000000000..de114c0005
--- /dev/null
+++ b/microsite/static/img/dynatrace.svg
@@ -0,0 +1,68 @@
+
+
+
diff --git a/packages/app/package.json b/packages/app/package.json
index f07a4e590d..db53e99e0f 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 799fe3e4d9..1958aabd2e 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -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 = (
+
+
+
+
);
@@ -495,6 +507,14 @@ const websiteEntityPage = (
+
+
+
+
Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+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)
+```
diff --git a/plugins/dynatrace/config.d.ts b/plugins/dynatrace/config.d.ts
new file mode 100644
index 0000000000..b06d0c7362
--- /dev/null
+++ b/plugins/dynatrace/config.d.ts
@@ -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;
+ };
+}
diff --git a/plugins/dynatrace/dev/index.tsx b/plugins/dynatrace/dev/index.tsx
new file mode 100644
index 0000000000..d44f4381ec
--- /dev/null
+++ b/plugins/dynatrace/dev/index.tsx
@@ -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: ,
+ title: 'Root Page',
+ path: '/dynatrace',
+ })
+ .render();
diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json
new file mode 100644
index 0000000000..267db810ce
--- /dev/null
+++ b/plugins/dynatrace/package.json
@@ -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"
+}
diff --git a/plugins/dynatrace/src/api/DynatraceApi.ts b/plugins/dynatrace/src/api/DynatraceApi.ts
new file mode 100644
index 0000000000..1865b20363
--- /dev/null
+++ b/plugins/dynatrace/src/api/DynatraceApi.ts
@@ -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;
+};
+
+export interface DynatraceProblems {
+ problems: Array;
+}
+
+export const dynatraceApiRef = createApiRef({
+ id: 'plugin.dynatrace.service',
+});
+
+export type DynatraceApi = {
+ getDynatraceProblems(
+ dynatraceEntityId: string,
+ ): Promise;
+};
diff --git a/plugins/dynatrace/src/api/DynatraceClient.ts b/plugins/dynatrace/src/api/DynatraceClient.ts
new file mode 100644
index 0000000000..27476ddbac
--- /dev/null
+++ b/plugins/dynatrace/src/api/DynatraceClient.ts
@@ -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(
+ path: string,
+ query: { [key in string]: any },
+ ): Promise {
+ 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 {
+ if (!dynatraceEntityId) {
+ throw new Error('Dynatrace entity ID is required');
+ }
+
+ return this.callApi('problems', {
+ entitySelector: `entityId(${dynatraceEntityId})`,
+ });
+ }
+}
diff --git a/plugins/dynatrace/src/api/index.ts b/plugins/dynatrace/src/api/index.ts
new file mode 100644
index 0000000000..97e33c9281
--- /dev/null
+++ b/plugins/dynatrace/src/api/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx
new file mode 100644
index 0000000000..c284eaba0f
--- /dev/null
+++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx
@@ -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 ;
+ }
+
+ const dynatraceEntityId: string =
+ entity?.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]!;
+
+ return (
+
+
+
+
+ Plugin to show information from Dynatrace
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/dynatrace/src/components/DynatraceTab/index.ts b/plugins/dynatrace/src/components/DynatraceTab/index.ts
new file mode 100644
index 0000000000..cd3a1b8fc6
--- /dev/null
+++ b/plugins/dynatrace/src/components/DynatraceTab/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.test.tsx b/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.test.tsx
new file mode 100644
index 0000000000..9ac20ea1a2
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.test.tsx
@@ -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();
+ expect(await rendered.findByText('Closed')).toBeInTheDocument();
+ });
+ it('renders StatusError for an open issue', async () => {
+ const rendered = await renderInTestApp();
+ expect(await rendered.findByText('Open')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.tsx b/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.tsx
new file mode 100644
index 0000000000..2ed861963a
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemStatus/ProblemStatus.tsx
@@ -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) => {
+ switch (status?.toLocaleLowerCase()) {
+ case 'open':
+ return (
+ <>
+
+ Open
+ >
+ );
+ case 'closed':
+ return (
+ <>
+
+ Closed
+ >
+ );
+ default:
+ return <>>;
+ }
+};
diff --git a/plugins/dynatrace/src/components/Problems/ProblemStatus/index.ts b/plugins/dynatrace/src/components/Problems/ProblemStatus/index.ts
new file mode 100644
index 0000000000..14124f8d46
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemStatus/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx
new file mode 100644
index 0000000000..1249f4e886
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx
@@ -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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ expect(
+ await rendered.findByText('Nothing to report :)'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx
new file mode 100644
index 0000000000..ac69abf93e
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx
@@ -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 ;
+ } else if (error) {
+ return {error.message};
+ }
+ if (!problems) return
Nothing to report :)
;
+ return ;
+};
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/index.ts b/plugins/dynatrace/src/components/Problems/ProblemsList/index.ts
new file mode 100644
index 0000000000..f70c9c6f20
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsList/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx
new file mode 100644
index 0000000000..e4adc43656
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx
@@ -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(
+
+ ,
+ ,
+ );
+ expect(await rendered.findByText('example-service')).toBeInTheDocument();
+ });
+ it('renders an empty table when no data is provided', async () => {
+ const rendered = await renderInTestApp(
+
+
+ ,
+ );
+ expect(await rendered.findByText('Problems')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx
new file mode 100644
index 0000000000..8e3d330c37
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx
@@ -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) => (
+
+ {row.title}
+
+ ),
+ },
+ {
+ title: 'Status',
+ field: 'status',
+ render: (row: Partial) => (
+
+ ),
+ },
+ { title: 'Severity', field: 'severityLevel' },
+ {
+ title: 'Root Cause',
+ field: 'rootCauseEntity',
+ render: (row: Partial) => row.rootCauseEntity?.name,
+ },
+ {
+ title: 'Affected',
+ field: 'affectedEntities',
+ render: (row: Partial) =>
+ row.affectedEntities?.map(e => e.name),
+ },
+ {
+ title: 'Start Time',
+ field: 'startTime',
+ render: (row: Partial) =>
+ new Date(row.startTime || 0).toString(),
+ },
+ {
+ title: 'End Time',
+ field: 'endTime',
+ render: (row: Partial) =>
+ row.endTime === -1 ? 'ongoing' : new Date(row.endTime || 0).toString(),
+ },
+ ];
+
+ return (
+
{
+ return { ...p, id: p.problemId };
+ })}
+ />
+ );
+};
diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/index.ts b/plugins/dynatrace/src/components/Problems/ProblemsTable/index.ts
new file mode 100644
index 0000000000..d1cd52e511
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/constants.ts b/plugins/dynatrace/src/constants.ts
new file mode 100644
index 0000000000..21453e0338
--- /dev/null
+++ b/plugins/dynatrace/src/constants.ts
@@ -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';
diff --git a/plugins/dynatrace/src/index.ts b/plugins/dynatrace/src/index.ts
new file mode 100644
index 0000000000..c96c16201d
--- /dev/null
+++ b/plugins/dynatrace/src/index.ts
@@ -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';
diff --git a/plugins/dynatrace/src/mocks/problems.json b/plugins/dynatrace/src/mocks/problems.json
new file mode 100644
index 0000000000..b1894bef56
--- /dev/null
+++ b/plugins/dynatrace/src/mocks/problems.json
@@ -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"]
+}
diff --git a/plugins/dynatrace/src/plugin.test.ts b/plugins/dynatrace/src/plugin.test.ts
new file mode 100644
index 0000000000..94dc2da765
--- /dev/null
+++ b/plugins/dynatrace/src/plugin.test.ts
@@ -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();
+ });
+});
diff --git a/plugins/dynatrace/src/plugin.ts b/plugins/dynatrace/src/plugin.ts
new file mode 100644
index 0000000000..fc1778b45c
--- /dev/null
+++ b/plugins/dynatrace/src/plugin.ts
@@ -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,
+ }),
+);
diff --git a/plugins/dynatrace/src/routes.ts b/plugins/dynatrace/src/routes.ts
new file mode 100644
index 0000000000..73f7b573b7
--- /dev/null
+++ b/plugins/dynatrace/src/routes.ts
@@ -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',
+});
diff --git a/plugins/dynatrace/src/setupTests.ts b/plugins/dynatrace/src/setupTests.ts
new file mode 100644
index 0000000000..9bb3e72355
--- /dev/null
+++ b/plugins/dynatrace/src/setupTests.ts
@@ -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';
diff --git a/yarn.lock b/yarn.lock
index 92532270c6..0902601073 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==