Skip to content

Commit

Permalink
refactor(backstage-plugin): Extract logic to hooks (#108)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kylejwatson authored Nov 27, 2023
1 parent 710a775 commit ec4f07e
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<dfBenchmarkKey | null>(
null,
const chartOrProgressComponent = chartData ? (
<BarChartComponent metricData={chartData} />
) : (
<Progress variant="indeterminate" />
);

const [t] = useTranslation();

// Charts
const [chartData, setChartData] = React.useState<MetricData | null>(null);
const [chartDataAverage, setChartDataAverage] =
React.useState<MetricData | null>(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 ? (
<ResponseErrorPanel error={error} />
) : (
chartOrProgressComponent
);

groupDataService.retrieveBenchmarkData({ type: 'df' }).then(
response => {
setDfOverview(response.key);
},
(error: Error) => {
dispatch({
type: 'dfBenchmarkError',
error: error,
});
},
);
}, [entityGroup, entityName, selectedTimeUnit, groupDataService]);
return (
<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<h1>{label}</h1>
{errorOrResponse}
</div>
</Grid>
);
};

const chartOrProgressComponent = chartData ? (
<BarChartComponent metricData={chartData} />
const BenchmarkGridItem = ({ type }: { type: string }) => {
const [t] = useTranslation();
const { benchmark, error } = useMetricBenchmark(type);

const testOrProgressComponent = benchmark ? (
<HighlightTextBoxComponent
title=""
text=""
highlight={t(`deployment_frequency.overall_labels.${benchmark}`)}
healthStatus={
{
'on-demand': 'positive',
'lt-6month': 'critical',
'week-month': 'neutral',
'month-6month': 'negative',
}[benchmark]
}
/>
) : (
<Progress variant="indeterminate" />
<CircularProgress />
);

const chartOrProgressComponentAverage = chartDataAverage ? (
<BarChartComponent metricData={chartDataAverage} />
const errorOrResponse = error ? (
<ResponseErrorPanel error={error} />
) : (
<Progress variant="indeterminate" />
testOrProgressComponent
);

return (
<Page themeId="tool">
<Header
title="OpenDORA (by Devoteam)"
subtitle="Through insight to perfection"
>
<SupportButton>Plugin for displaying DORA Metrics</SupportButton>
</Header>
<Content>
<Grid container spacing={3} direction="column">
<Grid container>
<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<Grid container>
<Grid item xs={4}>
<DropdownComponent
onSelect={setSelectedTimeUnit}
selection={selectedTimeUnit}
/>
</Grid>
</Grid>
</div>
</Grid>
<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<Grid container>
<Grid item xs={3}>
{errorOrResponse}
</Grid>
</Grid>
</div>
</Grid>
);
};

<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<Grid container>
<Grid item xs={3}>
<HighlightTextBoxComponent
title={t(
'deployment_frequency.labels.deployment_frequency_overall',
)}
text=""
highlight={
dfOverview
? t(
`deployment_frequency.overall_labels.${dfOverview}`,
)
: t('custom_errors.data_unavailable')
}
healthStatus={
(dfOverview &&
{
'on-demand': 'positive',
'lt-6month': 'critical',
'week-month': 'neutral',
'month-6month': 'negative',
}[dfOverview]) ||
'neutral'
}
/>
</Grid>
</Grid>
</div>
</Grid>
export const DashboardComponent = ({
entityName,
entityGroup,
}: DashboardComponentProps) => {
const [t] = useTranslation();
const [selectedTimeUnit, setSelectedTimeUnit] = React.useState('weekly');

<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<h1>{t('deployment_frequency.labels.deployment_frequency')}</h1>
{dataError.countError ? (
<ResponseErrorPanel error={dataError.countError} />
) : (
chartOrProgressComponent
)}
</div>
</Grid>
<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<h1>
{t(
'deployment_frequency.labels.deployment_frequency_average',
)}
</h1>
{dataError.averageError ? (
<ResponseErrorPanel error={dataError.averageError} />
) : (
chartOrProgressComponentAverage
return (
<MetricContext.Provider
value={{
aggregation: selectedTimeUnit,
team: entityGroup,
project: entityName,
}}
>
<Page themeId="tool">
<Header
title="OpenDORA (by Devoteam)"
subtitle="Through insight to perfection"
>
<SupportButton>Plugin for displaying DORA Metrics</SupportButton>
</Header>
<Content>
<Grid container spacing={3} direction="column">
<Grid container>
<Grid item xs={12} className="gridBorder">
<div className="gridBoxText">
<Grid container>
<Grid item xs={4}>
<DropdownComponent
onSelect={setSelectedTimeUnit}
selection={selectedTimeUnit}
/>
</Grid>
</Grid>
</div>
</Grid>
<BenchmarkGridItem type="df" />
<ChartGridItem
type="df_count"
label={t('deployment_frequency.labels.deployment_frequency')}
/>
<ChartGridItem
type="df_average"
label={t(
'deployment_frequency.labels.deployment_frequency_average',
)}
</div>
/>
</Grid>
<Grid item />
</Grid>

<Grid item />
</Grid>
</Content>
</Page>
</Content>
</Page>
</MetricContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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<dfBenchmarkKey | undefined>();
const [error, setError] = useState<Error | undefined>();

useEffect(() => {
groupDataService.retrieveBenchmarkData({ type: type }).then(response => {
setDfBenchmark(response.key);
}, setError);
}, [groupDataService, type]);

return { error: error, benchmark: benchmark };
};
31 changes: 31 additions & 0 deletions backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts
Original file line number Diff line number Diff line change
@@ -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<MetricData | undefined>();
const [error, setError] = useState<Error | undefined>();
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 };
};
Loading

0 comments on commit ec4f07e

Please sign in to comment.