From e4e92abbda91e10af3a091f844bd847eb2b3c469 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 23 Jun 2022 10:41:16 -0700 Subject: [PATCH 01/14] feat(dynatrace): improved look when problems table is empty Signed-off-by: Isaiah Thiessen --- .../src/components/EmptyState/EmptyState.tsx | 41 +++++++++++++++++++ .../src/components/EmptyState/index.ts | 17 ++++++++ .../Problems/ProblemsList/ProblemsList.tsx | 13 +++++- .../Problems/ProblemsTable/ProblemsTable.tsx | 1 - 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 plugins/dynatrace/src/components/EmptyState/EmptyState.tsx create mode 100644 plugins/dynatrace/src/components/EmptyState/index.ts diff --git a/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx b/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx new file mode 100644 index 0000000000..ab6eecdb7e --- /dev/null +++ b/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx @@ -0,0 +1,41 @@ +/* + * 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, Typography } from '@material-ui/core'; +import EmptyStateImage from '../../assets/emptystate.svg'; + +export const EmptyState = ({ message }) => { + return ( + + + {message} + + + EmptyState + + + ); +}; diff --git a/plugins/dynatrace/src/components/EmptyState/index.ts b/plugins/dynatrace/src/components/EmptyState/index.ts new file mode 100644 index 0000000000..0037560be4 --- /dev/null +++ b/plugins/dynatrace/src/components/EmptyState/index.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. + */ + +export { EmptyState } from './EmptyState'; diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index ac69abf93e..9922fb0f67 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -20,6 +20,8 @@ import Alert from '@material-ui/lab/Alert'; import { useApi } from '@backstage/core-plugin-api'; import { ProblemsTable } from '../ProblemsTable'; import { dynatraceApiRef } from '../../../api'; +import { EmptyState } from '../../EmptyState'; +import { InfoCard } from '@backstage/core-components'; type ProblemsListProps = { dynatraceEntityId: string; @@ -38,6 +40,13 @@ export const ProblemsList = (props: ProblemsListProps) => { } else if (error) { return {error.message}; } - if (!problems) return
Nothing to report :)
; - return ; + return ( + + {value?.totalCount ? ( + + ) : ( + + )} + + ); }; diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx index 8e3d330c37..c04eb62beb 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx @@ -75,7 +75,6 @@ export const ProblemsTable = ({ problems }: ProblemsTableProps) => { return ( { From 3e9b0cbcd95fc1a9c9a38254455ed1e2b43bc9fe Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 23 Jun 2022 12:45:17 -0700 Subject: [PATCH 02/14] feat: pass down dynatraceBaseUrl as prop Signed-off-by: Isaiah Thiessen --- .../Problems/ProblemsList/ProblemsList.tsx | 17 ++++++++++++++--- .../Problems/ProblemsTable/ProblemsTable.tsx | 8 +++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index 9922fb0f67..fa49ef91d1 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -25,10 +25,11 @@ import { InfoCard } from '@backstage/core-components'; type ProblemsListProps = { dynatraceEntityId: string; + dynatraceBaseUrl: string; }; export const ProblemsList = (props: ProblemsListProps) => { - const { dynatraceEntityId } = props; + const { dynatraceEntityId, dynatraceBaseUrl } = props; const dynatraceApi = useApi(dynatraceApiRef); const { value, loading, error } = useAsync(async () => { return dynatraceApi.getDynatraceProblems(dynatraceEntityId); @@ -41,9 +42,19 @@ export const ProblemsList = (props: ProblemsListProps) => { return {error.message}; } return ( - + {value?.totalCount ? ( - + ) : ( )} diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx index c04eb62beb..a9c474d863 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx @@ -17,17 +17,15 @@ 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[]; + dynatraceBaseUrl: string; }; -export const ProblemsTable = ({ problems }: ProblemsTableProps) => { - const configApi = useApi(configApiRef); - const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl'); +export const ProblemsTable = (props: ProblemsTableProps) => { + const { problems, dynatraceBaseUrl } = props; const columns: TableColumn[] = [ { title: 'Title', From 4e34828773234749fc9fbee91353dee2928e6cf7 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 23 Jun 2022 12:46:27 -0700 Subject: [PATCH 03/14] feat(dynatrace): add view recent synthetic monitor activity Signed-off-by: Isaiah Thiessen --- microsite/data/plugins/dynatrace.yaml | 2 +- plugins/dynatrace/src/api/DynatraceApi.ts | 20 +++++ plugins/dynatrace/src/api/DynatraceClient.ts | 18 +++- plugins/dynatrace/src/assets/emptystate.svg | 1 + .../components/DynatraceTab/DynatraceTab.tsx | 35 ++++++-- .../components/EmptyState/EmptyState.test.tsx | 15 ++++ .../SyntheticsCard/SyntheticsCard.test.tsx | 18 ++++ .../SyntheticsCard/SyntheticsCard.tsx | 85 +++++++++++++++++++ .../Synthetics/SyntheticsCard/index.ts | 16 ++++ plugins/dynatrace/src/constants.ts | 2 + 10 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 plugins/dynatrace/src/assets/emptystate.svg create mode 100644 plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsCard/index.ts diff --git a/microsite/data/plugins/dynatrace.yaml b/microsite/data/plugins/dynatrace.yaml index 3ab85ae1eb..f204c4105c 100644 --- a/microsite/data/plugins/dynatrace.yaml +++ b/microsite/data/plugins/dynatrace.yaml @@ -3,7 +3,7 @@ title: Dynatrace author: TELUS authorUrl: https://github.com/telus category: Monitoring -description: View monitoring info from dynatrace for services in your software catalog. +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' diff --git a/plugins/dynatrace/src/api/DynatraceApi.ts b/plugins/dynatrace/src/api/DynatraceApi.ts index 1865b20363..8993860e61 100644 --- a/plugins/dynatrace/src/api/DynatraceApi.ts +++ b/plugins/dynatrace/src/api/DynatraceApi.ts @@ -35,8 +35,25 @@ export type DynatraceProblem = { affectedEntities: Array; }; +type SyntheticRequestResults = { + startTimestamp: number; +}; + +export type DynatraceSyntheticResults = { + monitorId: string; + locationsExecutionResults: [ + { + locationId: number; + executionId: string; + requestResults: Array; + }, + ]; +}; + export interface DynatraceProblems { problems: Array; + totalCount: number; + pageSize: number; } export const dynatraceApiRef = createApiRef({ @@ -47,4 +64,7 @@ export type DynatraceApi = { getDynatraceProblems( dynatraceEntityId: string, ): Promise; + getDynatraceSyntheticFailures( + syntheticId: string, + ): Promise; }; diff --git a/plugins/dynatrace/src/api/DynatraceClient.ts b/plugins/dynatrace/src/api/DynatraceClient.ts index 27476ddbac..728f94de7b 100644 --- a/plugins/dynatrace/src/api/DynatraceClient.ts +++ b/plugins/dynatrace/src/api/DynatraceClient.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DynatraceProblems, DynatraceApi } from './DynatraceApi'; +import { + DynatraceProblems, + DynatraceApi, + DynatraceSyntheticResults, +} from './DynatraceApi'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; export class DynatraceClient implements DynatraceApi { @@ -52,11 +56,21 @@ export class DynatraceClient implements DynatraceApi { ); } + async getDynatraceSyntheticFailures( + syntheticId: string, + ): Promise { + if (!syntheticId) { + throw new Error('Dynatrace synthetic Id is required'); + } + + return this.callApi(`synthetic/execution/${syntheticId}/FAILED`, {}); + } + async getDynatraceProblems( dynatraceEntityId: string, ): Promise { if (!dynatraceEntityId) { - throw new Error('Dynatrace entity ID is required'); + throw new Error('Dynatrace entity Id is required'); } return this.callApi('problems', { diff --git a/plugins/dynatrace/src/assets/emptystate.svg b/plugins/dynatrace/src/assets/emptystate.svg new file mode 100644 index 0000000000..8a0490727f --- /dev/null +++ b/plugins/dynatrace/src/assets/emptystate.svg @@ -0,0 +1 @@ + diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx index c284eaba0f..071fc5d256 100644 --- a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx +++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx @@ -18,18 +18,24 @@ import { Grid } from '@material-ui/core'; import { Page, Content, - ContentHeader, - SupportButton, MissingAnnotationEmptyState, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { ProblemsList } from '../Problems/ProblemsList'; +import { SyntheticsCard } from '../Synthetics/SyntheticsCard'; import { isDynatraceAvailable } from '../../plugin'; -import { DYNATRACE_ID_ANNOTATION } from '../../constants'; +import { + DYNATRACE_ID_ANNOTATION, + DYNATRACE_SYNTHETICS_ANNOTATION, +} from '../../constants'; export const DynatraceTab = () => { const { entity } = useEntity(); + const configApi = useApi(configApiRef); + const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl'); + if (!isDynatraceAvailable(entity)) { return ; } @@ -37,18 +43,29 @@ export const DynatraceTab = () => { const dynatraceEntityId: string = entity?.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]!; + const syntheticsIds: string = + entity?.metadata.annotations?.[DYNATRACE_SYNTHETICS_ANNOTATION]!; + return ( - - - Plugin to show information from Dynatrace - - - + + {syntheticsIds ? ( + + + + ) : ( + <> + )} diff --git a/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx b/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx new file mode 100644 index 0000000000..b61d59e88d --- /dev/null +++ b/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx new file mode 100644 index 0000000000..89f10c5750 --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx @@ -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. + */ +import React from 'react'; + +describe('SyntheticsCard', () => {}); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx new file mode 100644 index 0000000000..491a752b09 --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx @@ -0,0 +1,85 @@ +/* + * 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 { InfoCard } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { Typography } from '@material-ui/core'; +import { dynatraceApiRef } from '../../../api'; + +type SyntheticsCardProps = { + syntheticsIds: string; + dynatraceBaseUrl: string; +}; + +const dynatraceMonitorPrefixes = (idPrefix: string): string => { + switch (idPrefix) { + case 'HTTP_CHECK': + return 'ui/http-monitor'; + case 'BROWSER_MONITOR': + return 'ui/browser-monitor'; + case 'SYNTHETIC_TEST': + return 'ui/browser-monitor'; + default: + throw new Error('Invalid synthetic Id'); + } +}; + +export const SyntheticsCard = (props: SyntheticsCardProps) => { + const { syntheticsIds, dynatraceBaseUrl } = props; + const dynatraceApi = useApi(dynatraceApiRef); + const { value, loading, error } = useAsync(async () => { + return dynatraceApi.getDynatraceSyntheticFailures(syntheticsIds); + }, [dynatraceApi, syntheticsIds]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const deepLinkPrefix = dynatraceMonitorPrefixes( + `${syntheticsIds.match(/(.+)-/)![1]}`, + ); + const timestamps = value?.locationsExecutionResults.map(l => { + return l.requestResults[0].startTimestamp; + }); + + return ( + + + Locations: {JSON.stringify(value?.locationsExecutionResults.length)} + + + Last Failures:{' '} + {JSON.stringify( + timestamps?.map(t => { + return new Date(t).toString(); + }), + )} + + + ); +}; diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/index.ts b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/index.ts new file mode 100644 index 0000000000..5284e0cfab --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/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 { SyntheticsCard } from './SyntheticsCard'; diff --git a/plugins/dynatrace/src/constants.ts b/plugins/dynatrace/src/constants.ts index 21453e0338..8f504db1f4 100644 --- a/plugins/dynatrace/src/constants.ts +++ b/plugins/dynatrace/src/constants.ts @@ -14,3 +14,5 @@ * limitations under the License. */ export const DYNATRACE_ID_ANNOTATION = 'dynatrace.com/dynatrace-entity-id'; +export const DYNATRACE_SYNTHETICS_ANNOTATION = + 'dynatrace.com/dynatrace-synthetics-ids'; From 10db751ed4356e894436401607723538e8cce51c Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Wed, 29 Jun 2022 14:26:53 -0700 Subject: [PATCH 04/14] fix: use toLocaleString in ProblemsTable Signed-off-by: Isaiah Thiessen --- .../src/components/Problems/ProblemsTable/ProblemsTable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx index a9c474d863..41470959db 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.tsx @@ -61,13 +61,15 @@ export const ProblemsTable = (props: ProblemsTableProps) => { title: 'Start Time', field: 'startTime', render: (row: Partial) => - new Date(row.startTime || 0).toString(), + new Date(row.startTime || 0).toLocaleString(), }, { title: 'End Time', field: 'endTime', render: (row: Partial) => - row.endTime === -1 ? 'ongoing' : new Date(row.endTime || 0).toString(), + row.endTime === -1 + ? 'ongoing' + : new Date(row.endTime || 0).toLocaleString(), }, ]; From 4224a43e1c0031c11a60000cdc3a60f129e0e4ca Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Wed, 29 Jun 2022 14:27:36 -0700 Subject: [PATCH 05/14] feat: prettier SyntheticsCard Signed-off-by: Isaiah Thiessen --- plugins/dynatrace/src/api/DynatraceApi.ts | 16 +++- plugins/dynatrace/src/api/DynatraceClient.ts | 19 ++++- .../components/DynatraceTab/DynatraceTab.tsx | 2 +- .../SyntheticsCard/SyntheticsCard.tsx | 43 +++++----- .../SyntheticsLocation.test.tsx | 18 ++++ .../SyntheticsLocation/SyntheticsLocation.tsx | 82 +++++++++++++++++++ .../Synthetics/SyntheticsLocation/index.ts | 16 ++++ 7 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx create mode 100644 plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/index.ts diff --git a/plugins/dynatrace/src/api/DynatraceApi.ts b/plugins/dynatrace/src/api/DynatraceApi.ts index 8993860e61..4cb5f105bf 100644 --- a/plugins/dynatrace/src/api/DynatraceApi.ts +++ b/plugins/dynatrace/src/api/DynatraceApi.ts @@ -39,7 +39,7 @@ type SyntheticRequestResults = { startTimestamp: number; }; -export type DynatraceSyntheticResults = { +export interface DynatraceSyntheticResults { monitorId: string; locationsExecutionResults: [ { @@ -48,7 +48,14 @@ export type DynatraceSyntheticResults = { requestResults: Array; }, ]; -}; +} + +export interface DynatraceSyntheticLocationInfo { + entityId: string; + name: string; + city: string; + browserType: string; +} export interface DynatraceProblems { problems: Array; @@ -65,6 +72,9 @@ export type DynatraceApi = { dynatraceEntityId: string, ): Promise; getDynatraceSyntheticFailures( - syntheticId: string, + syntheticsId: string, ): Promise; + getDynatraceSyntheticLocationInfo( + syntheticLocationId: string, + ): Promise; }; diff --git a/plugins/dynatrace/src/api/DynatraceClient.ts b/plugins/dynatrace/src/api/DynatraceClient.ts index 728f94de7b..1aabd3fb6d 100644 --- a/plugins/dynatrace/src/api/DynatraceClient.ts +++ b/plugins/dynatrace/src/api/DynatraceClient.ts @@ -17,6 +17,7 @@ import { DynatraceProblems, DynatraceApi, DynatraceSyntheticResults, + DynatraceSyntheticLocationInfo, } from './DynatraceApi'; import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; @@ -57,13 +58,23 @@ export class DynatraceClient implements DynatraceApi { } async getDynatraceSyntheticFailures( - syntheticId: string, + syntheticsId: string, ): Promise { - if (!syntheticId) { - throw new Error('Dynatrace synthetic Id is required'); + if (!syntheticsId) { + throw new Error('Dynatrace syntheticId is required'); } - return this.callApi(`synthetic/execution/${syntheticId}/FAILED`, {}); + return this.callApi(`synthetic/execution/${syntheticsId}/FAILED`, {}); + } + + async getDynatraceSyntheticLocationInfo( + syntheticLocationId: string, + ): Promise { + if (!syntheticLocationId) { + throw new Error('Dynatrace syntheticLocationId is required'); + } + + return this.callApi(`synthetic/locations/${syntheticLocationId}`, {}); } async getDynatraceProblems( diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx index 071fc5d256..1be005908f 100644 --- a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx +++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx @@ -59,7 +59,7 @@ export const DynatraceTab = () => { {syntheticsIds ? ( diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx index 491a752b09..3a77db3d50 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx @@ -19,11 +19,11 @@ import { Progress } from '@backstage/core-components'; import Alert from '@material-ui/lab/Alert'; import { InfoCard } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { Typography } from '@material-ui/core'; import { dynatraceApiRef } from '../../../api'; +import { SyntheticsLocation } from '../SyntheticsLocation'; type SyntheticsCardProps = { - syntheticsIds: string; + syntheticsId: string; dynatraceBaseUrl: string; }; @@ -41,11 +41,11 @@ const dynatraceMonitorPrefixes = (idPrefix: string): string => { }; export const SyntheticsCard = (props: SyntheticsCardProps) => { - const { syntheticsIds, dynatraceBaseUrl } = props; + const { syntheticsId, dynatraceBaseUrl } = props; const dynatraceApi = useApi(dynatraceApiRef); const { value, loading, error } = useAsync(async () => { - return dynatraceApi.getDynatraceSyntheticFailures(syntheticsIds); - }, [dynatraceApi, syntheticsIds]); + return dynatraceApi.getDynatraceSyntheticFailures(syntheticsId); + }, [dynatraceApi, syntheticsId]); if (loading) { return ; @@ -54,32 +54,33 @@ export const SyntheticsCard = (props: SyntheticsCardProps) => { } const deepLinkPrefix = dynatraceMonitorPrefixes( - `${syntheticsIds.match(/(.+)-/)![1]}`, + `${syntheticsId.match(/(.+)-/)![1]}`, ); - const timestamps = value?.locationsExecutionResults.map(l => { - return l.requestResults[0].startTimestamp; + const lastFailed = value?.locationsExecutionResults.map(l => { + return { + timestamp: l.requestResults[0].startTimestamp, + location: Number(l.locationId).toString(16), + }; }); return ( - - Locations: {JSON.stringify(value?.locationsExecutionResults.length)} - - - Last Failures:{' '} - {JSON.stringify( - timestamps?.map(t => { - return new Date(t).toString(); - }), - )} - + {lastFailed?.map(l => { + return ( + + ); + })} ); }; diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx new file mode 100644 index 0000000000..0fa1a148e9 --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx @@ -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. + */ +import React from 'react'; + +describe('SyntheticsLocation', () => {}); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx new file mode 100644 index 0000000000..957f1c34f5 --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx @@ -0,0 +1,82 @@ +/* + * 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 { Chip } from '@material-ui/core'; +import { dynatraceApiRef } from '../../../api'; + +type SyntheticsLocationProps = { + lastFailedTimestamp: Date; + locationId: string; + key: string; +}; + +const failedInLast24Hours = (timestamp: Date): Boolean => { + return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * 24); +}; + +const failedInLast6Hours = (timestamp: Date): Boolean => { + return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * 6); +}; + +const failedinLastHour = (timestamp: Date): Boolean => { + return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60); +}; + +const chipColor = (timestamp: Date): string => { + if (failedinLastHour(timestamp)) { + return 'salmon'; + } + if (failedInLast6Hours(timestamp)) { + return 'sandybrown'; + } + if (failedInLast24Hours(timestamp)) { + return 'palegoldenrod'; + } + return 'lightgreen'; +}; + +export const SyntheticsLocation = (props: SyntheticsLocationProps) => { + const { lastFailedTimestamp, locationId } = props; + const dynatraceApi = useApi(dynatraceApiRef); + const { value, loading, error } = useAsync(async () => { + return dynatraceApi.getDynatraceSyntheticLocationInfo( + `SYNTHETIC_LOCATION-00000000000000${locationId}`, + ); + }); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + ); +}; diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/index.ts b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/index.ts new file mode 100644 index 0000000000..1dc8a783fe --- /dev/null +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/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 { SyntheticsLocation } from './SyntheticsLocation'; From 91d816d7ab23fc92d2e75509a781cc0eb98ae956 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Wed, 29 Jun 2022 14:40:00 -0700 Subject: [PATCH 06/14] feat: show dynatrace entity id in card subtitle Signed-off-by: Isaiah Thiessen --- .../src/components/Problems/ProblemsList/ProblemsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index fa49ef91d1..6a6ade9520 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -44,7 +44,7 @@ export const ProblemsList = (props: ProblemsListProps) => { return ( Date: Thu, 21 Jul 2022 15:22:15 -0700 Subject: [PATCH 07/14] feat: support multiple synthetic ids Signed-off-by: Isaiah Thiessen --- .../components/DynatraceTab/DynatraceTab.tsx | 23 +++++++++++-------- .../SyntheticsCard/SyntheticsCard.tsx | 3 ++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx index 1be005908f..3e137f4109 100644 --- a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx +++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx @@ -56,16 +56,19 @@ export const DynatraceTab = () => { dynatraceBaseUrl={dynatraceBaseUrl} /> - {syntheticsIds ? ( - - - - ) : ( - <> - )} + {syntheticsIds + .replace(' ', '') + .split(',') + .map(id => { + return ( + + + + ); + })} diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx index 3a77db3d50..1b73e51313 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx @@ -56,6 +56,7 @@ export const SyntheticsCard = (props: SyntheticsCardProps) => { const deepLinkPrefix = dynatraceMonitorPrefixes( `${syntheticsId.match(/(.+)-/)![1]}`, ); + const lastFailed = value?.locationsExecutionResults.map(l => { return { timestamp: l.requestResults[0].startTimestamp, @@ -68,7 +69,7 @@ export const SyntheticsCard = (props: SyntheticsCardProps) => { title="Synthetics" subheader={`Recent Activity for Monitor ${syntheticsId}`} deepLink={{ - title: 'View Synthetics in Dynatrace', + title: 'View this Synthetic in Dynatrace', link: `${dynatraceBaseUrl}/${deepLinkPrefix}/${syntheticsId}`, }} > From e44c0b3811e1df2bdd2c0d0755b713262876c5b9 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 4 Aug 2022 10:28:43 -0700 Subject: [PATCH 08/14] feat: unit tests for Synthetics Card, update ProblemsList tests Signed-off-by: Isaiah Thiessen --- .changeset/fresh-rabbits-juggle.md | 9 ++++ plugins/dynatrace/README.md | 19 +++++++ .../components/EmptyState/EmptyState.test.tsx | 15 ------ .../src/components/EmptyState/EmptyState.tsx | 7 ++- .../ProblemsList/ProblemsList.test.tsx | 14 ++++-- .../Problems/ProblemsList/ProblemsList.tsx | 20 +++++--- .../ProblemsTable/ProblemsTable.test.tsx | 12 +---- .../SyntheticsCard/SyntheticsCard.test.tsx | 40 ++++++++++++++- .../SyntheticsLocation.test.tsx | 49 ++++++++++++++++++- .../SyntheticsLocation/SyntheticsLocation.tsx | 24 ++++----- plugins/dynatrace/src/mocks/problems.json | 2 +- 11 files changed, 155 insertions(+), 56 deletions(-) create mode 100644 .changeset/fresh-rabbits-juggle.md delete mode 100644 plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx diff --git a/.changeset/fresh-rabbits-juggle.md b/.changeset/fresh-rabbits-juggle.md new file mode 100644 index 0000000000..804ae70853 --- /dev/null +++ b/.changeset/fresh-rabbits-juggle.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-dynatrace': minor +--- + +New features: + +- Some visual improvements to the table that displays Problems +- Added support for viewing recent Synthetics results using +- Added some additional linking to the configured Dynatrace instance diff --git a/plugins/dynatrace/README.md b/plugins/dynatrace/README.md index 150c9768af..2f5d30b88a 100644 --- a/plugins/dynatrace/README.md +++ b/plugins/dynatrace/README.md @@ -38,6 +38,8 @@ dynatrace: #### Catalog Configuration +##### View Recent Application Problems + To show information from Dynatrace for a catalog entity, add the following annotation to `catalog-info.yaml`: ```yaml @@ -51,6 +53,23 @@ metadata: 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. +##### Viewing Recent Synthetics Results + +To show recent results from a Synthetic Monitor, add the following annotation to `catalog-info.yaml`: + +```yaml +# catalog-info.yaml +# [...] +metadata: + annotations: + dynatrace.com/dynatrace-synthetics-ids: SYNTHETIC_ID, SYNTHETIC_ID_2, ... +# [...] +``` + +The annotation can also contain a comma separated list of Synthetic Ids to surface details for multiple monitors! + +The `SYNTHETIC_ID` can be found in Dynatrace by browsing to the Synthetic monitor. It will be located in the browser address bar in the resource path - `https://example.dynatrace.com/ui/http-monitor/HTTP_CHECK-1234` for an Http check, or `https://example.dynatrace.com/ui/browser-monitor/SYNTHETIC_TEST-1234` for a browser clickpath. + ## Disclaimer This plugin is not officially supported by Dynatrace. diff --git a/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx b/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx deleted file mode 100644 index b61d59e88d..0000000000 --- a/plugins/dynatrace/src/components/EmptyState/EmptyState.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ diff --git a/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx b/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx index ab6eecdb7e..87d0099499 100644 --- a/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx +++ b/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx @@ -17,7 +17,12 @@ import React from 'react'; import { Grid, Typography } from '@material-ui/core'; import EmptyStateImage from '../../assets/emptystate.svg'; -export const EmptyState = ({ message }) => { +type EmptyStateProps = { + message: string; +}; + +export const EmptyState = (props: EmptyStateProps) => { + const { message } = props; return ( { .mockResolvedValue({ problems }); const rendered = await renderInTestApp( - + , ); expect(await rendered.findByText('example-service')).toBeInTheDocument(); }); - it('renders "nothing to report :)" if no problems are found', async () => { + it('returns "No Problems to Report!" if no problems are found', async () => { mockDynatraceApi.getDynatraceProblems = jest.fn().mockResolvedValue({}); const rendered = await renderInTestApp( - + , ); expect( - await rendered.findByText('Nothing to report :)'), + await rendered.findByText('No Problems to Report!'), ).toBeInTheDocument(); }); }); diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index 6a6ade9520..17ab1842e9 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -28,6 +28,17 @@ type ProblemsListProps = { dynatraceBaseUrl: string; }; +const cardContents = (problems: Array, dynatraceBaseUrl: string) => { + return problems?.length ? ( + + ) : ( + + ); +}; + export const ProblemsList = (props: ProblemsListProps) => { const { dynatraceEntityId, dynatraceBaseUrl } = props; const dynatraceApi = useApi(dynatraceApiRef); @@ -50,14 +61,7 @@ export const ProblemsList = (props: ProblemsListProps) => { link: `${dynatraceBaseUrl}/#serviceOverview;id=${dynatraceEntityId}`, }} > - {value?.totalCount ? ( - - ) : ( - - )} + {cardContents(problems, dynatraceBaseUrl)} ); }; diff --git a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx index e4adc43656..aaa3ea0024 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsTable/ProblemsTable.test.tsx @@ -28,17 +28,9 @@ describe('ProblemsTable', () => { 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(); + expect(await rendered.findByTitle('Search')).toBeInTheDocument(); }); }); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx index 89f10c5750..8a06a140b4 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx @@ -14,5 +14,43 @@ * limitations under the License. */ import React from 'react'; +import { SyntheticsCard } from './SyntheticsCard'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { dynatraceApiRef } from '../../../api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { configApiRef } from '@backstage/core-plugin-api'; -describe('SyntheticsCard', () => {}); +const mockDynatraceApi = { + getDynatraceSyntheticFailures: jest.fn(), +}; +const apis = TestApiRegistry.from( + [dynatraceApiRef, mockDynatraceApi], + [configApiRef, new ConfigReader({ dynatrace: { baseUrl: '__dynatrace__' } })], +); + +describe('SyntheticsCard', () => { + it('renders the card with Synthetics data', async () => { + mockDynatraceApi.getDynatraceSyntheticFailures = jest + .fn() + .mockResolvedValue({ + locationsExecutionResults: [ + { + locationId: '__location__', + requestResults: [{ startTimestamp: 0 }], + }, + ], + }); + const rendered = await renderInTestApp( + + + , + , + ); + expect( + await rendered.findByText('View this Synthetic in Dynatrace'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx index 0fa1a148e9..de04f73e36 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx @@ -14,5 +14,52 @@ * limitations under the License. */ import React from 'react'; +import { SyntheticsLocation } from './SyntheticsLocation'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { dynatraceApiRef } from '../../../api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { configApiRef } from '@backstage/core-plugin-api'; -describe('SyntheticsLocation', () => {}); +const mockDynatraceApi = { + getDynatraceSyntheticLocationInfo: jest.fn(), +}; +const apis = TestApiRegistry.from( + [dynatraceApiRef, mockDynatraceApi], + [configApiRef, new ConfigReader({ dynatrace: { baseUrl: '__dynatrace__' } })], +); + +describe('SyntheticsLocation', () => { + it('renders the SyntheticsLocation chip - recent failure', async () => { + mockDynatraceApi.getDynatraceSyntheticLocationInfo = jest + .fn() + .mockResolvedValue({ name: '__location__' }); + const rendered = await renderInTestApp( + + + , + , + ); + expect(await rendered.findByText(/failed/)).toBeInTheDocument(); + }); + it('renders the SyntheticsLocation chip - no failures', async () => { + mockDynatraceApi.getDynatraceSyntheticLocationInfo = jest + .fn() + .mockResolvedValue({ name: '__location__' }); + const rendered = await renderInTestApp( + + + , + , + ); + expect(await rendered.findByText(/__location__/)).toBeInTheDocument(); + expect(await rendered.queryByText(/failed/)).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx index 957f1c34f5..e72995158c 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx @@ -28,26 +28,20 @@ type SyntheticsLocationProps = { key: string; }; -const failedInLast24Hours = (timestamp: Date): Boolean => { - return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * 24); -}; - -const failedInLast6Hours = (timestamp: Date): Boolean => { - return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * 6); -}; - -const failedinLastHour = (timestamp: Date): Boolean => { - return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60); +const failedInLastXHours = (timestamp: Date, offset: number): Boolean => { + if (offset < 0 || offset > 24) + throw new Error('offset must be between 0 and 24'); + return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * offset); }; const chipColor = (timestamp: Date): string => { - if (failedinLastHour(timestamp)) { + if (failedInLastXHours(timestamp, 1)) { return 'salmon'; } - if (failedInLast6Hours(timestamp)) { + if (failedInLastXHours(timestamp, 6)) { return 'sandybrown'; } - if (failedInLast24Hours(timestamp)) { + if (failedInLastXHours(timestamp, 24)) { return 'palegoldenrod'; } return 'lightgreen'; @@ -71,8 +65,8 @@ export const SyntheticsLocation = (props: SyntheticsLocationProps) => { return ( Date: Thu, 4 Aug 2022 12:23:57 -0700 Subject: [PATCH 09/14] fix: yarn tsc error Signed-off-by: Isaiah Thiessen --- .../components/Problems/ProblemsList/ProblemsList.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index 17ab1842e9..ef6a2ed173 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -19,7 +19,7 @@ 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'; +import { dynatraceApiRef, DynatraceProblem } from '../../../api'; import { EmptyState } from '../../EmptyState'; import { InfoCard } from '@backstage/core-components'; @@ -28,8 +28,11 @@ type ProblemsListProps = { dynatraceBaseUrl: string; }; -const cardContents = (problems: Array, dynatraceBaseUrl: string) => { - return problems?.length ? ( +const cardContents = ( + problems: DynatraceProblem[], + dynatraceBaseUrl: string, +) => { + return problems.length ? ( { link: `${dynatraceBaseUrl}/#serviceOverview;id=${dynatraceEntityId}`, }} > - {cardContents(problems, dynatraceBaseUrl)} + {cardContents(problems || [], dynatraceBaseUrl)} ); }; From 444a188d95ea196c9781f3a5f54f04ce97b1e10d Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 11 Aug 2022 14:43:37 -0700 Subject: [PATCH 10/14] fix: use @backstage/core-components, address PR feedback from @freben Signed-off-by: Isaiah Thiessen --- plugins/dynatrace/src/api/DynatraceClient.ts | 12 +++-- .../components/DynatraceTab/DynatraceTab.tsx | 18 +++++--- .../src/components/EmptyState/EmptyState.tsx | 46 ------------------- .../src/components/EmptyState/index.ts | 17 ------- .../Problems/ProblemsList/ProblemsList.tsx | 12 +++-- .../Problems/ProblemsTable/ProblemsTable.tsx | 11 +++-- .../SyntheticsCard/SyntheticsCard.tsx | 7 ++- .../SyntheticsLocation/SyntheticsLocation.tsx | 7 ++- plugins/dynatrace/src/constants.ts | 3 +- plugins/dynatrace/src/plugin.ts | 8 +++- 10 files changed, 46 insertions(+), 95 deletions(-) delete mode 100644 plugins/dynatrace/src/components/EmptyState/EmptyState.tsx delete mode 100644 plugins/dynatrace/src/components/EmptyState/index.ts diff --git a/plugins/dynatrace/src/api/DynatraceClient.ts b/plugins/dynatrace/src/api/DynatraceClient.ts index 1aabd3fb6d..a31253bcb5 100644 --- a/plugins/dynatrace/src/api/DynatraceClient.ts +++ b/plugins/dynatrace/src/api/DynatraceClient.ts @@ -64,7 +64,10 @@ export class DynatraceClient implements DynatraceApi { throw new Error('Dynatrace syntheticId is required'); } - return this.callApi(`synthetic/execution/${syntheticsId}/FAILED`, {}); + return this.callApi( + `synthetic/execution/${encodeURIComponent(syntheticsId)}/FAILED`, + {}, + ); } async getDynatraceSyntheticLocationInfo( @@ -74,14 +77,17 @@ export class DynatraceClient implements DynatraceApi { throw new Error('Dynatrace syntheticLocationId is required'); } - return this.callApi(`synthetic/locations/${syntheticLocationId}`, {}); + return this.callApi( + `synthetic/locations/${encodeURIComponent(syntheticLocationId)}`, + {}, + ); } async getDynatraceProblems( dynatraceEntityId: string, ): Promise { if (!dynatraceEntityId) { - throw new Error('Dynatrace entity Id is required'); + throw new Error('Dynatrace entity id is required'); } return this.callApi('problems', { diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx index 3e137f4109..59886a121d 100644 --- a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx +++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx @@ -50,14 +50,18 @@ export const DynatraceTab = () => { - - - + {dynatraceEntityId ? ( + + + + ) : ( + '' + )} {syntheticsIds - .replace(' ', '') + ?.replace(' ', '') .split(',') .map(id => { return ( diff --git a/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx b/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx deleted file mode 100644 index 87d0099499..0000000000 --- a/plugins/dynatrace/src/components/EmptyState/EmptyState.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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, Typography } from '@material-ui/core'; -import EmptyStateImage from '../../assets/emptystate.svg'; - -type EmptyStateProps = { - message: string; -}; - -export const EmptyState = (props: EmptyStateProps) => { - const { message } = props; - return ( - - - {message} - - - EmptyState - - - ); -}; diff --git a/plugins/dynatrace/src/components/EmptyState/index.ts b/plugins/dynatrace/src/components/EmptyState/index.ts deleted file mode 100644 index 0037560be4..0000000000 --- a/plugins/dynatrace/src/components/EmptyState/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { EmptyState } from './EmptyState'; diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index ef6a2ed173..8fb3f67934 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -15,12 +15,14 @@ */ 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 { + Progress, + ResponseErrorPanel, + EmptyState, +} from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { ProblemsTable } from '../ProblemsTable'; import { dynatraceApiRef, DynatraceProblem } from '../../../api'; -import { EmptyState } from '../../EmptyState'; import { InfoCard } from '@backstage/core-components'; type ProblemsListProps = { @@ -38,7 +40,7 @@ const cardContents = ( dynatraceBaseUrl={dynatraceBaseUrl} /> ) : ( - + ); }; @@ -53,7 +55,7 @@ export const ProblemsList = (props: ProblemsListProps) => { if (loading) { return ; } else if (error) { - return {error.message}; + return ; } return ( { + return timestamp ? new Date(timestamp).toLocaleString() : 'N/A'; +}; + export const ProblemsTable = (props: ProblemsTableProps) => { const { problems, dynatraceBaseUrl } = props; const columns: TableColumn[] = [ @@ -60,16 +64,13 @@ export const ProblemsTable = (props: ProblemsTableProps) => { { title: 'Start Time', field: 'startTime', - render: (row: Partial) => - new Date(row.startTime || 0).toLocaleString(), + render: (row: Partial) => parseTimestamp(row.startTime), }, { title: 'End Time', field: 'endTime', render: (row: Partial) => - row.endTime === -1 - ? 'ongoing' - : new Date(row.endTime || 0).toLocaleString(), + row.endTime === -1 ? 'ongoing' : parseTimestamp(row.endTime), }, ]; diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx index 1b73e51313..531d7ef72f 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx @@ -15,8 +15,7 @@ */ 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 { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { InfoCard } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { dynatraceApiRef } from '../../../api'; @@ -50,11 +49,11 @@ export const SyntheticsCard = (props: SyntheticsCardProps) => { if (loading) { return ; } else if (error) { - return {error.message}; + return ; } const deepLinkPrefix = dynatraceMonitorPrefixes( - `${syntheticsId.match(/(.+)-/)![1]}`, + `${syntheticsId.split('-')[0]}`, ); const lastFailed = value?.locationsExecutionResults.map(l => { diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx index e72995158c..4d328210cb 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.tsx @@ -16,8 +16,7 @@ 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 { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { Chip } from '@material-ui/core'; import { dynatraceApiRef } from '../../../api'; @@ -28,7 +27,7 @@ type SyntheticsLocationProps = { key: string; }; -const failedInLastXHours = (timestamp: Date, offset: number): Boolean => { +const failedInLastXHours = (timestamp: Date, offset: number): boolean => { if (offset < 0 || offset > 24) throw new Error('offset must be between 0 and 24'); return timestamp > new Date(new Date().getTime() - 1000 * 60 * 60 * offset); @@ -59,7 +58,7 @@ export const SyntheticsLocation = (props: SyntheticsLocationProps) => { if (loading) { return ; } else if (error) { - return {error.message}; + return ; } return ( diff --git a/plugins/dynatrace/src/constants.ts b/plugins/dynatrace/src/constants.ts index 8f504db1f4..c4cc48d48a 100644 --- a/plugins/dynatrace/src/constants.ts +++ b/plugins/dynatrace/src/constants.ts @@ -14,5 +14,4 @@ * limitations under the License. */ export const DYNATRACE_ID_ANNOTATION = 'dynatrace.com/dynatrace-entity-id'; -export const DYNATRACE_SYNTHETICS_ANNOTATION = - 'dynatrace.com/dynatrace-synthetics-ids'; +export const DYNATRACE_SYNTHETICS_ANNOTATION = 'dynatrace.com/synthetics-ids'; diff --git a/plugins/dynatrace/src/plugin.ts b/plugins/dynatrace/src/plugin.ts index fc1778b45c..26ecb1aba1 100644 --- a/plugins/dynatrace/src/plugin.ts +++ b/plugins/dynatrace/src/plugin.ts @@ -23,7 +23,10 @@ import { } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { DYNATRACE_ID_ANNOTATION } from './constants'; +import { + DYNATRACE_ID_ANNOTATION, + DYNATRACE_SYNTHETICS_ANNOTATION, +} from './constants'; import { rootRouteRef } from './routes'; @@ -55,7 +58,8 @@ export const dynatracePlugin = createPlugin({ * @param entity {Entity} - The entity to check for the dynatrace id annotation. */ export const isDynatraceAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]); + Boolean(entity.metadata.annotations?.[DYNATRACE_ID_ANNOTATION]) || + Boolean(entity.metadata.annotations?.[DYNATRACE_SYNTHETICS_ANNOTATION]); /** * Creates a routable extension for the dynatrace plugin tab. From 392f99a3dfc7979b0cbba4270aa109eefe6e5512 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 11 Aug 2022 14:45:36 -0700 Subject: [PATCH 11/14] fix: remove extra svg file Signed-off-by: Isaiah Thiessen --- plugins/dynatrace/src/assets/emptystate.svg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 plugins/dynatrace/src/assets/emptystate.svg diff --git a/plugins/dynatrace/src/assets/emptystate.svg b/plugins/dynatrace/src/assets/emptystate.svg deleted file mode 100644 index 8a0490727f..0000000000 --- a/plugins/dynatrace/src/assets/emptystate.svg +++ /dev/null @@ -1 +0,0 @@ - From dde444346d45e4017b7c581173b9f9b98c9e10ed Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Thu, 11 Aug 2022 15:00:07 -0700 Subject: [PATCH 12/14] fix: pull dynatraceBaseUrl from config, support [, ] for synthetics annotation Signed-off-by: Isaiah Thiessen --- .../components/DynatraceTab/DynatraceTab.tsx | 18 ++++-------------- .../Problems/ProblemsList/ProblemsList.tsx | 8 +++++--- .../SyntheticsCard/SyntheticsCard.tsx | 8 +++++--- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx index 59886a121d..3f58eed9dc 100644 --- a/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx +++ b/plugins/dynatrace/src/components/DynatraceTab/DynatraceTab.tsx @@ -21,7 +21,6 @@ import { MissingAnnotationEmptyState, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { ProblemsList } from '../Problems/ProblemsList'; import { SyntheticsCard } from '../Synthetics/SyntheticsCard'; import { isDynatraceAvailable } from '../../plugin'; @@ -33,9 +32,6 @@ import { export const DynatraceTab = () => { const { entity } = useEntity(); - const configApi = useApi(configApiRef); - const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl'); - if (!isDynatraceAvailable(entity)) { return ; } @@ -52,24 +48,18 @@ export const DynatraceTab = () => { {dynatraceEntityId ? ( - + ) : ( '' )} {syntheticsIds - ?.replace(' ', '') - .split(',') + ?.split(/[ ,]/) + .filter(Boolean) .map(id => { return ( - + ); })} diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx index 8fb3f67934..2ee0dda48d 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.tsx @@ -20,14 +20,13 @@ import { ResponseErrorPanel, EmptyState, } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { ProblemsTable } from '../ProblemsTable'; import { dynatraceApiRef, DynatraceProblem } from '../../../api'; import { InfoCard } from '@backstage/core-components'; type ProblemsListProps = { dynatraceEntityId: string; - dynatraceBaseUrl: string; }; const cardContents = ( @@ -45,8 +44,11 @@ const cardContents = ( }; export const ProblemsList = (props: ProblemsListProps) => { - const { dynatraceEntityId, dynatraceBaseUrl } = props; + const { dynatraceEntityId } = props; + const configApi = useApi(configApiRef); const dynatraceApi = useApi(dynatraceApiRef); + const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl'); + const { value, loading, error } = useAsync(async () => { return dynatraceApi.getDynatraceProblems(dynatraceEntityId); }, [dynatraceApi, dynatraceEntityId]); diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx index 531d7ef72f..6b51a1725c 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.tsx @@ -17,13 +17,12 @@ import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { InfoCard } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { dynatraceApiRef } from '../../../api'; import { SyntheticsLocation } from '../SyntheticsLocation'; type SyntheticsCardProps = { syntheticsId: string; - dynatraceBaseUrl: string; }; const dynatraceMonitorPrefixes = (idPrefix: string): string => { @@ -40,8 +39,11 @@ const dynatraceMonitorPrefixes = (idPrefix: string): string => { }; export const SyntheticsCard = (props: SyntheticsCardProps) => { - const { syntheticsId, dynatraceBaseUrl } = props; + const { syntheticsId } = props; + const configApi = useApi(configApiRef); const dynatraceApi = useApi(dynatraceApiRef); + const dynatraceBaseUrl = configApi.getString('dynatrace.baseUrl'); + const { value, loading, error } = useAsync(async () => { return dynatraceApi.getDynatraceSyntheticFailures(syntheticsId); }, [dynatraceApi, syntheticsId]); From ad9ff7194bdbe3c74a48c6b6bf75723df5ced853 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Mon, 15 Aug 2022 10:38:37 -0700 Subject: [PATCH 13/14] fix: update docs for changes to synthetics annotation Signed-off-by: Isaiah Thiessen --- plugins/dynatrace/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/dynatrace/README.md b/plugins/dynatrace/README.md index 2f5d30b88a..8ed72ebb5d 100644 --- a/plugins/dynatrace/README.md +++ b/plugins/dynatrace/README.md @@ -62,11 +62,11 @@ To show recent results from a Synthetic Monitor, add the following annotation to # [...] metadata: annotations: - dynatrace.com/dynatrace-synthetics-ids: SYNTHETIC_ID, SYNTHETIC_ID_2, ... + dynatrace.com/synthetics-ids: SYNTHETIC_ID, SYNTHETIC_ID_2, ... # [...] ``` -The annotation can also contain a comma separated list of Synthetic Ids to surface details for multiple monitors! +The annotation can also contain a comma or space separated list of Synthetic Ids to surface details for multiple monitors! The `SYNTHETIC_ID` can be found in Dynatrace by browsing to the Synthetic monitor. It will be located in the browser address bar in the resource path - `https://example.dynatrace.com/ui/http-monitor/HTTP_CHECK-1234` for an Http check, or `https://example.dynatrace.com/ui/browser-monitor/SYNTHETIC_TEST-1234` for a browser clickpath. From 9d18c0c382b28d27484a299ee736f384b3b71fe7 Mon Sep 17 00:00:00 2001 From: Isaiah Thiessen Date: Tue, 16 Aug 2022 09:27:55 -0700 Subject: [PATCH 14/14] fix: update component props in unit tests Signed-off-by: Isaiah Thiessen --- .../Problems/ProblemsList/ProblemsList.test.tsx | 10 ++-------- .../Synthetics/SyntheticsCard/SyntheticsCard.test.tsx | 6 +----- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx index ff5d20dc28..9b9c51dca1 100644 --- a/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx +++ b/plugins/dynatrace/src/components/Problems/ProblemsList/ProblemsList.test.tsx @@ -36,10 +36,7 @@ describe('ProblemStatus', () => { .mockResolvedValue({ problems }); const rendered = await renderInTestApp( - + , ); expect(await rendered.findByText('example-service')).toBeInTheDocument(); @@ -48,10 +45,7 @@ describe('ProblemStatus', () => { mockDynatraceApi.getDynatraceProblems = jest.fn().mockResolvedValue({}); const rendered = await renderInTestApp( - + , ); expect( diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx index 8a06a140b4..e9083a3f82 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsCard/SyntheticsCard.test.tsx @@ -42,11 +42,7 @@ describe('SyntheticsCard', () => { }); const rendered = await renderInTestApp( - - , + , , ); expect(