diff --git a/app-config.yaml b/app-config.yaml
index eb51c59847..1c9bc66a45 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -114,6 +114,10 @@ proxy:
allowedHeaders: ['Authorization']
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 +135,9 @@ techdocs:
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives.
+dynatrace:
+ baseUrl: https://your-dynatrace-instance-here.dynatrace.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..30629928b3
--- /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/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 97b5d35f23..814b671c95 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.1",
+ "@backstage/plugin-dynatrace": "^0.1.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 = (
+
+
+
+
,
+ title: 'Root Page',
+ path: '/dynatrace',
+ })
+ .render();
diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json
new file mode 100644
index 0000000000..5ff08529e6
--- /dev/null
+++ b/plugins/dynatrace/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@backstage/plugin-dynatrace",
+ "version": "0.1.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.2",
+ "@backstage/core-components": "^0.9.3-next.0",
+ "@backstage/core-plugin-api": "^1.0.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.0.0",
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.16.1-next.0",
+ "@backstage/core-app-api": "^1.0.1-next.0",
+ "@backstage/dev-utils": "^1.0.1-next.0",
+ "@backstage/test-utils": "^1.0.1-next.0",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^13.1.8",
+ "@types/jest": "*",
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5",
+ "express": "^4.18.1",
+ "msw": "^0.35.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/dynatrace/src/api/DynatraceApi.ts b/plugins/dynatrace/src/api/DynatraceApi.ts
new file mode 100644
index 0000000000..40676e3d09
--- /dev/null
+++ b/plugins/dynatrace/src/api/DynatraceApi.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 Problem = {
+ problemId: string;
+ impactLevel: string;
+ status: string;
+ startTime: number;
+ endTime: number;
+ title: string;
+ severityLevel: string;
+ rootCauseEntity: DynatraceEntity;
+ affectedEntities: Array;
+};
+
+export interface Problems {
+ problems: Array;
+}
+
+export const dynatraceApiRef = createApiRef({
+ id: 'plugin.dynatrace.service',
+});
+
+export type DynatraceApi = {
+ getProblems(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..157e840749
--- /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 fetch from 'cross-fetch';
+import { Problems, DynatraceApi } from './DynatraceApi';
+import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+
+export class DynatraceClient implements DynatraceApi {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+
+ constructor({
+ discoveryApi,
+ identityApi,
+ }: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ }) {
+ this.discoveryApi = discoveryApi;
+ this.identityApi = identityApi;
+ }
+
+ private async callApi(
+ path: string,
+ query: { [key in string]: any },
+ ): Promise {
+ const { token: idToken } = await this.identityApi.getCredentials();
+
+ const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/dynatrace`;
+ const response = await fetch(
+ `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(idToken && { Authorization: `Bearer ${idToken}` }),
+ },
+ },
+ );
+ if (response.status === 200) {
+ return (await response.json()) as T;
+ }
+ return undefined;
+ }
+
+ async getProblems(dynatraceEntityId: string): Promise {
+ if (!dynatraceEntityId) {
+ return undefined;
+ }
+
+ 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..3924f373b5
--- /dev/null
+++ b/plugins/dynatrace/src/api/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { Problems, Problem, 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..8068ad7ad9
--- /dev/null
+++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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();
+ const dynatraceEntityId: string =
+ entity?.metadata.annotations?.[DYNATRACE_ID_ANNOTATION] ?? '';
+
+ if (!isDynatraceAvailable(entity)) {
+ return ;
+ }
+ 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..5605be6e44
--- /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 { Problem } 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..dfeb311c33
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 = {
+ getProblems: 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.getProblems = 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.getProblems = 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..b12b8fe41a
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.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 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 () => {
+ const r = await dynatraceApi.getProblems(dynatraceEntityId);
+ return r;
+ }, [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..ba1a2e19d9
--- /dev/null
+++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 { Problem } 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: Problem[];
+};
+
+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..74e5544341
--- /dev/null
+++ b/plugins/dynatrace/src/plugin.ts
@@ -0,0 +1,61 @@
+/*
+ * 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,
+ identityApiRef,
+ createRoutableExtension,
+} from '@backstage/core-plugin-api';
+
+import { Entity } from '@backstage/catalog-model';
+import { DYNATRACE_ID_ANNOTATION } from './constants';
+
+import { rootRouteRef } from './routes';
+
+export const dynatracePlugin = createPlugin({
+ id: 'dynatrace',
+ routes: {
+ root: rootRouteRef,
+ },
+ apis: [
+ createApiFactory({
+ api: dynatraceApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ identityApi: identityApiRef,
+ },
+ factory: ({ discoveryApi, identityApi }) =>
+ new DynatraceClient({
+ discoveryApi,
+ identityApi,
+ }),
+ }),
+ ],
+});
+
+export const isDynatraceAvailable = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]);
+
+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';