diff --git a/backstage-plugin/plugins/open-dora/package.json b/backstage-plugin/plugins/open-dora/package.json index 803736b..4b5a12a 100644 --- a/backstage-plugin/plugins/open-dora/package.json +++ b/backstage-plugin/plugins/open-dora/package.json @@ -33,6 +33,8 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@mui/x-charts": "^6.0.0-alpha.11", + "i18next": "^23.7.6", + "react-i18next": "^13.5.0", "react-use": "^17.2.4" }, "peerDependencies": { 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 a9e3d61..957205c 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.tsx @@ -6,13 +6,19 @@ 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 from 'react'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import '../../i18n'; +import { dfBenchmarkKey } from '../../models/DfBenchmarkData'; +import { groupDataServiceApiRef } from '../../services/GroupDataService'; import { MetricContext } from '../../services/MetricContext'; import { useMetricData } from '../../services/MetricDataHook'; import { BarChartComponent } from '../BarChartComponent/BarChartComponent'; import { DropdownComponent } from '../DropdownComponent/DropdownComponent'; +import { HighlightTextBoxComponent } from '../HighlightTextBoxComponent/HighlightTextBoxComponent'; import './DashboardComponent.css'; export interface DashboardComponentProps { @@ -49,8 +55,31 @@ export const DashboardComponent = ({ entityName, entityGroup, }: DashboardComponentProps) => { + // Overview + const [dfOverview, setDfOverview] = React.useState( + null, + ); + + const [t] = useTranslation(); const [selectedTimeUnit, setSelectedTimeUnit] = React.useState('weekly'); + const groupDataService = useApi(groupDataServiceApiRef); + + useEffect(() => { + groupDataService.retrieveBenchmarkData({ type: 'df' }).then( + response => { + setDfOverview(response.key); + }, + (error: Error) => { + console.error(error); + // dispatch({ + // type: 'dfBenchmarkError', + // error: error, + // }); + }, + ); + }, [entityGroup, entityName, selectedTimeUnit, groupDataService]); + return ( - - + +
+ + + + + +
+
+ diff --git a/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.test.tsx b/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.test.tsx index d723418..3e2575f 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.test.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { DropdownComponent } from './DropdownComponent'; +import '../../i18n'; describe('DropdownComponent', () => { it('should create show a dropdown with the aggregation choices', async () => { diff --git a/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.tsx b/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.tsx index a90191e..3777fb0 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DropdownComponent/DropdownComponent.tsx @@ -1,6 +1,8 @@ import { Box, MenuItem, TextField } from '@material-ui/core'; import React from 'react'; +import { useTranslation } from 'react-i18next'; + interface DropdownComponentProps { onSelect: (selection: string) => void; selection: string; @@ -10,6 +12,7 @@ export const DropdownComponent = ({ onSelect, selection, }: DropdownComponentProps) => { + const [t] = useTranslation(); return ( onSelect(e.target.value)} select - label="Time Unit" + label={t('dropdown_time_units.time_unit')} > - Weekly + {t('dropdown_time_units.weekly')} - Monthly + {t('dropdown_time_units.monthly')} - Quarterly + {t('dropdown_time_units.quarterly')} diff --git a/backstage-plugin/plugins/open-dora/src/i18n.ts b/backstage-plugin/plugins/open-dora/src/i18n.ts new file mode 100644 index 0000000..49e594a --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/i18n.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// Import all translation files +import translationEnglish from './locales/en/translation.json'; + +// Using translation +const resources = { + en: { + translation: translationEnglish, + }, +}; + +i18next.use(initReactI18next).init({ + resources, + lng: 'en', // default language +}); + +export default i18next; diff --git a/backstage-plugin/plugins/open-dora/src/locales/en/translation.json b/backstage-plugin/plugins/open-dora/src/locales/en/translation.json new file mode 100644 index 0000000..fa3916c --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/locales/en/translation.json @@ -0,0 +1,25 @@ +{ + "deployment_frequency": { + "labels": { + "deployment_frequency": "Deployment Frequency", + "deployment_frequency_average": "Deployment Frequency Average" + }, + "overall_labels": { + "lt-6month": "Fewer than once per six months", + "on-demand": "On-demand", + "week-month": "Between once per week and once per month", + "month-6month": "Between once per month and once every 6 months" + } + }, + + "dropdown_time_units": { + "time_unit": "Time Unit", + "weekly": "Weekly", + "monthly": "Monthly", + "quarterly": "Quarterly" + }, + + "custom_errors": { + "data_unavailable": "Data unavailable" + } +} diff --git a/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts b/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts index 9ebe69a..02dc586 100644 --- a/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts +++ b/backstage-plugin/plugins/open-dora/src/models/CustomErrors.ts @@ -1,4 +1,5 @@ export interface ChartErrors { countError: Error | null; averageError: Error | null; + dfBenchmarkError: Error | null; } diff --git a/backstage-plugin/plugins/open-dora/src/models/DfBenchmarkData.ts b/backstage-plugin/plugins/open-dora/src/models/DfBenchmarkData.ts new file mode 100644 index 0000000..74249d7 --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/models/DfBenchmarkData.ts @@ -0,0 +1,8 @@ +export interface dfBenchmarkData { + key: dfBenchmarkKey; +} +export type dfBenchmarkKey = + | 'on-demand' + | 'week-month' + | 'month-6month' + | 'lt-6month'; diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts index 58efcf3..2e3a67a 100644 --- a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts +++ b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts @@ -1,11 +1,28 @@ import { MockConfigApi } from '@backstage/test-utils'; import { rest } from 'msw'; -import { baseUrl, metricUrl } from '../../testing/mswHandlers'; +import { baseUrl, benchmarkUrl, metricUrl } from '../../testing/mswHandlers'; import { server } from '../setupTests'; import { GroupDataService } from './GroupDataService'; function createService() { server.use( + rest.get(benchmarkUrl, (req, res, ctx) => { + const params = req.url.searchParams; + const type = params.get('type'); + + switch (type) { + case 'df': { + return res( + ctx.json({ + key: 'lt-6month', + }), + ); + } + default: { + return res(ctx.status(400)); + } + } + }), rest.get(metricUrl, (req, res, ctx) => { const params = req.url.searchParams; const type = params.get('type'); @@ -119,3 +136,64 @@ describe('GroupDataService', () => { ).rejects.toEqual(new Error('Internal Server Error')); }); }); + +describe('BenchmarkService', () => { + it('should return deployment frequency overall data from the server', async () => { + const service = createService(); + + expect(await service.retrieveBenchmarkData({ type: 'df' })).toEqual({ + key: 'lt-6month', + }); + }); + + it('should throw an error if the response does not contain metric data', async () => { + const service = createService(); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.json({ other: 'data' })); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Unexpected response')); + }); + + it('should return 404 for invalid types', async () => { + const service = createService(); + + await expect( + service.retrieveBenchmarkData({ + type: 'invalid_type', + }), + ).rejects.toEqual(new Error('Bad Request')); + }); + + it('should throw an error when the server returns a non-ok status', async () => { + const service = createService(); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.status(401)); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Unauthorized')); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.status(500)); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Internal Server Error')); + }); +}); diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts index b5a7271..15b6496 100644 --- a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts +++ b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts @@ -1,5 +1,6 @@ import { ConfigApi, createApiRef } from '@backstage/core-plugin-api'; import { MetricData } from '../models/MetricData'; +import { dfBenchmarkData } from '../models/DfBenchmarkData'; export const groupDataServiceApiRef = createApiRef({ id: 'plugin.open-dora.group-data', @@ -41,4 +42,30 @@ export class GroupDataService { return response as MetricData; } + + async retrieveBenchmarkData(params: { type: string }) { + const baseUrl = this.options.configApi.getString('open-dora.apiBaseUrl'); + + const url = new URL(baseUrl); + url.pathname = 'dora/api/benchmark'; + for (const [key, value] of Object.entries(params)) { + if (value) { + url.searchParams.append(key, value); + } + } + const data = await fetch(url.toString(), { + method: 'GET', + }); + + if (!data.ok) { + throw new Error(data.statusText); + } + + const response = await data.json(); + if (response.key === undefined) { + throw new Error('Unexpected response'); + } + + return response as dfBenchmarkData; + } } diff --git a/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts b/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts index f4df50b..b1d7238 100644 --- a/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts +++ b/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts @@ -2,6 +2,7 @@ import { rest } from 'msw'; export const baseUrl = 'http://localhost:10666'; export const metricUrl = `${baseUrl}/dora/api/metric`; +export const benchmarkUrl = `${baseUrl}/dora/api/benchmark`; export const handlers = [ rest.get(metricUrl, (_, res, ctx) => { @@ -12,4 +13,12 @@ export const handlers = [ }), ); }), + + rest.get(benchmarkUrl, (_, res, ctx) => { + return res( + ctx.json({ + key: 'on-demand', + }), + ); + }), ]; diff --git a/backstage-plugin/yarn.lock b/backstage-plugin/yarn.lock index a0f7882..94a8398 100644 --- a/backstage-plugin/yarn.lock +++ b/backstage-plugin/yarn.lock @@ -1137,6 +1137,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -9180,6 +9187,13 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-webpack-plugin@^5.3.1: version "5.5.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" @@ -9321,6 +9335,13 @@ i18next@^22.4.15: dependencies: "@babel/runtime" "^7.20.6" +i18next@^23.7.6: + version "23.7.6" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.7.6.tgz#7328e76c899052d5d33d930164612dd21e575f74" + integrity sha512-O66BhXBw0fH4bEJMA0/klQKPEbcwAp5wjXEL803pdAynNbg2f4qhLIYlNHJyE7icrL6XmSZKPYaaXwy11kJ6YQ== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -13479,6 +13500,14 @@ react-hook-form@^7.12.2: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d" integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ== +react-i18next@^13.5.0: + version "13.5.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.5.0.tgz#44198f747628267a115c565f0c736a50a76b1ab0" + integrity sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA== + dependencies: + "@babel/runtime" "^7.22.5" + html-parse-stringify "^3.0.1" + react-idle-timer@5.6.2: version "5.6.2" resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.6.2.tgz#0342b381ca26ea46e8232dbdc7f2b948bc4ddb0d" @@ -15869,6 +15898,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"