From ec4f07ea0badfa27a25b2d709ed8d31099fdd081 Mon Sep 17 00:00:00 2001 From: Kyle Watson Date: Mon, 27 Nov 2023 15:17:24 +0100 Subject: [PATCH] refactor(backstage-plugin): Extract logic to hooks (#108) * refactor(backstage-plugin): Extract logic to hooks Extract out error/response logic from component to a reusable hook to avoid complicated state in the component. Use a context to provide metric details for the request to avoid prop drilling. Create a reusable component to reuse logic for switching between a graph, loading bar or error. Addresses #71 * refactor(backstage-plugin): Remove comment for test Remove the comment about extracting the test for the hook. Tests should follow user usage. Addresses #71 * refactor(backstage-plugin): Extract overview to hook Extract logic to download and show progress/error to a hook/separate component. Remove unused error file. Addresses #71 * refactor(backstage-plugin): Move hook to own file Move benchmark hook to it's own file. Rename overview to benchmark for consistency. Addresses #71 --- .../DashboardComponent.test.tsx | 16 + .../DashboardComponent/DashboardComponent.tsx | 291 +++++++----------- .../src/hooks/MetricBenchmarkHook.ts | 18 ++ .../open-dora/src/hooks/MetricDataHook.ts | 31 ++ .../open-dora/src/models/CustomErrors.ts | 5 - .../open-dora/src/services/MetricContext.ts | 11 + 6 files changed, 187 insertions(+), 185 deletions(-) create mode 100644 backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts create mode 100644 backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts delete mode 100644 backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts create mode 100644 backstage-plugin/plugins/open-dora/src/services/MetricContext.ts diff --git a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx index 249df57..1bf5dbb 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx @@ -177,6 +177,22 @@ describe('DashboardComponent', () => { expect(queryAllByText('Error: Failed to fetch')).toHaveLength(2); }); + + it('should show error if there are no datapoints', async () => { + server.use( + rest.get(metricUrl, async (_, res, ctx) => { + return res( + ctx.json({ + aggregation: 'weekly', + dataPoints: [], + }), + ); + }), + ); + const { queryAllByText } = await renderDashboardComponent(); + expect(queryAllByText('No data found')).not.toBeNull(); + expect(queryAllByText('No data found')).toHaveLength(2); + }); }); describe('EntityDashboardComponent', () => { diff --git a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx index 2c88b30..372e400 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx @@ -6,213 +6,144 @@ import { ResponseErrorPanel, SupportButton, } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; import { getEntityRelations, useEntity } from '@backstage/plugin-catalog-react'; -import { Grid } from '@material-ui/core'; -import React, { useEffect, useReducer } from 'react'; -import { MetricData } from '../../models/MetricData'; -import { groupDataServiceApiRef } from '../../services/GroupDataService'; +import { CircularProgress, Grid } from '@material-ui/core'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMetricBenchmark } from '../../hooks/MetricBenchmarkHook'; +import { useMetricData } from '../../hooks/MetricDataHook'; +import '../../i18n'; +import { MetricContext } from '../../services/MetricContext'; import { BarChartComponent } from '../BarChartComponent/BarChartComponent'; import { DropdownComponent } from '../DropdownComponent/DropdownComponent'; -import './DashboardComponent.css'; -import { ChartErrors } from '../../models/CustomErrors'; -import { dfBenchmarkKey } from '../../models/DfBenchmarkData'; import { HighlightTextBoxComponent } from '../HighlightTextBoxComponent/HighlightTextBoxComponent'; -import '../../i18n'; -import { useTranslation } from 'react-i18next'; +import './DashboardComponent.css'; export interface DashboardComponentProps { entityName?: string; entityGroup?: string; } -function dataErrorReducer( - currentErrors: ChartErrors, - action: { type: keyof ChartErrors; error: Error }, -) { - return { - ...currentErrors, - [action.type]: action.error, - }; -} +const ChartGridItem = ({ type, label }: { type: string; label: string }) => { + const { chartData, error } = useMetricData(type); -export const DashboardComponent = ({ - entityName, - entityGroup, -}: DashboardComponentProps) => { - // Overview - const [dfOverview, setDfOverview] = React.useState( - null, + const chartOrProgressComponent = chartData ? ( + + ) : ( + ); - const [t] = useTranslation(); - - // Charts - const [chartData, setChartData] = React.useState(null); - const [chartDataAverage, setChartDataAverage] = - React.useState(null); - const [selectedTimeUnit, setSelectedTimeUnit] = React.useState('weekly'); - - const initialErrors: ChartErrors = { - countError: null, - averageError: null, - dfBenchmarkError: null, - }; - - const [dataError, dispatch] = useReducer(dataErrorReducer, initialErrors); - - const groupDataService = useApi(groupDataServiceApiRef); - - useEffect(() => { - groupDataService - .retrieveMetricDataPoints({ - type: 'df_count', - team: entityGroup, - aggregation: selectedTimeUnit, - project: entityName, - }) - .then( - response => { - setChartData(response); - }, - (error: Error) => { - dispatch({ - type: 'countError', - error: error, - }); - }, - ); - - groupDataService - .retrieveMetricDataPoints({ - type: 'df_average', - team: entityGroup, - aggregation: selectedTimeUnit, - project: entityName, - }) - .then( - response => { - setChartDataAverage(response); - }, - (error: Error) => { - dispatch({ - type: 'averageError', - error: error, - }); - }, - ); + const errorOrResponse = error ? ( + + ) : ( + chartOrProgressComponent + ); - groupDataService.retrieveBenchmarkData({ type: 'df' }).then( - response => { - setDfOverview(response.key); - }, - (error: Error) => { - dispatch({ - type: 'dfBenchmarkError', - error: error, - }); - }, - ); - }, [entityGroup, entityName, selectedTimeUnit, groupDataService]); + return ( + +
+

{label}

+ {errorOrResponse} +
+
+ ); +}; - const chartOrProgressComponent = chartData ? ( - +const BenchmarkGridItem = ({ type }: { type: string }) => { + const [t] = useTranslation(); + const { benchmark, error } = useMetricBenchmark(type); + + const testOrProgressComponent = benchmark ? ( + ) : ( - + ); - const chartOrProgressComponentAverage = chartDataAverage ? ( - + const errorOrResponse = error ? ( + ) : ( - + testOrProgressComponent ); return ( - -
- Plugin for displaying DORA Metrics -
- - - - -
- - - - - -
-
+ +
+ + + {errorOrResponse} + + +
+
+ ); +}; - -
- - - - - -
-
+export const DashboardComponent = ({ + entityName, + entityGroup, +}: DashboardComponentProps) => { + const [t] = useTranslation(); + const [selectedTimeUnit, setSelectedTimeUnit] = React.useState('weekly'); - -
-

{t('deployment_frequency.labels.deployment_frequency')}

- {dataError.countError ? ( - - ) : ( - chartOrProgressComponent - )} -
-
- -
-

- {t( - 'deployment_frequency.labels.deployment_frequency_average', - )} -

- {dataError.averageError ? ( - - ) : ( - chartOrProgressComponentAverage + return ( + + +
+ Plugin for displaying DORA Metrics +
+ + + + +
+ + + + + +
+
+ + + + />
+ - - - -
-
+ + +
); }; diff --git a/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts b/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts new file mode 100644 index 0000000..b3679d5 --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts @@ -0,0 +1,18 @@ +import { useApi } from '@backstage/core-plugin-api'; +import { useEffect, useState } from 'react'; +import { dfBenchmarkKey } from '../models/DfBenchmarkData'; +import { groupDataServiceApiRef } from '../services/GroupDataService'; + +export const useMetricBenchmark = (type: string) => { + const groupDataService = useApi(groupDataServiceApiRef); + const [benchmark, setDfBenchmark] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + groupDataService.retrieveBenchmarkData({ type: type }).then(response => { + setDfBenchmark(response.key); + }, setError); + }, [groupDataService, type]); + + return { error: error, benchmark: benchmark }; +}; diff --git a/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts b/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts new file mode 100644 index 0000000..99c2a5d --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts @@ -0,0 +1,31 @@ +import { useApi } from '@backstage/core-plugin-api'; +import { useContext, useEffect, useState } from 'react'; +import { MetricData } from '../models/MetricData'; +import { groupDataServiceApiRef } from '../services/GroupDataService'; +import { MetricContext } from '../services/MetricContext'; + +export const useMetricData = (type: string) => { + const groupDataService = useApi(groupDataServiceApiRef); + const [chartData, setChartData] = useState(); + const [error, setError] = useState(); + const { aggregation, team, project } = useContext(MetricContext); + + useEffect(() => { + groupDataService + .retrieveMetricDataPoints({ + type: type, + team: team, + aggregation: aggregation, + project: project, + }) + .then(response => { + if (response.dataPoints.length > 0) { + setChartData(response); + } else { + setError(new Error('No data found')); + } + }, setError); + }, [aggregation, team, project, groupDataService, type]); + + return { error: error, chartData: chartData }; +}; diff --git a/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts b/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts deleted file mode 100644 index 02dc586..0000000 --- a/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ChartErrors { - countError: Error | null; - averageError: Error | null; - dfBenchmarkError: Error | null; -} diff --git a/backstage-plugin/plugins/open-dora/src/services/MetricContext.ts b/backstage-plugin/plugins/open-dora/src/services/MetricContext.ts new file mode 100644 index 0000000..89d9ae0 --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/services/MetricContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +export const MetricContext = createContext<{ + aggregation: string; + team?: string; + project?: string; +}>({ + aggregation: 'weekly', + team: undefined, + project: undefined, +});