From 889c0572795abfe4254e4944a02efc27b3225a23 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Thu, 11 Jul 2024 09:08:16 +0800 Subject: [PATCH 01/20] ADM-975 [frontend]: show the all pipeline in the lead time for change details --- .../ReportStep/BoardMetricsChart/index.tsx | 3 +- .../ReportStep/DoraMetricsChart/index.tsx | 10 ++++-- .../ReportStep/ReportDetail/dora.tsx | 6 ++-- frontend/src/containers/ReportStep/index.tsx | 6 ++-- frontend/src/containers/ReportStep/style.tsx | 8 +++++ .../hooks/reportMapper/leadTimeForChanges.ts | 35 ++++++++++++++++--- frontend/src/hooks/reportMapper/report.ts | 28 ++++++++------- 7 files changed, 72 insertions(+), 24 deletions(-) diff --git a/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx b/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx index 9125562f07..0f5a981d18 100644 --- a/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx @@ -258,7 +258,8 @@ export const BoardMetricsChart = ({ data, dateRanges, metrics }: BoardMetricsCha const rework = useRef(null); const mappedData: ReportResponse[] | undefined = - data && data.map((item) => (item.reportData?.boardMetricsCompleted ? reportMapper(item.reportData) : emptyData)); + data && + data.map((item) => (item.reportData?.boardMetricsCompleted ? reportMapper(item.reportData, null) : emptyData)); const cycleTimeAllocationData = extractCycleTimeAllocationData(dateRanges, mappedData); const cycleTimeAllocationDataOption = cycleTimeAllocationData && stackedBarOptionMapper(cycleTimeAllocationData); diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index be2c1114bb..a4a68dfa54 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { ChartType, @@ -19,9 +19,12 @@ import { LEFT_RIGHT_ALIGN_LABEL, NO_LABEL, } from '@src/containers/ReportStep/BoardMetricsChart'; +import { SingleSelection } from '@src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection'; import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper'; +import { PipelinesSelectContainer } from '@src/containers/ReportStep/style'; import { calculateTrendInfo, percentageFormatter } from '@src/utils/util'; +import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { ChartContainer } from '@src/containers/MetricsStep/style'; import { reportMapper } from '@src/hooks/reportMapper/report'; import { showChart } from '@src/containers/ReportStep'; @@ -32,6 +35,7 @@ interface DoraMetricsChartProps { dateRanges: string[]; data: (ReportResponseDTO | undefined)[]; metrics: string[]; + selectedPipelines: IPipelineConfig[]; } enum DORAMetricsChartType { @@ -224,7 +228,7 @@ function isDoraMetricsChartFinish({ ); } -export const DoraMetricsChart = ({ data, dateRanges, metrics }: DoraMetricsChartProps) => { +export const DoraMetricsChart = ({ data, dateRanges, metrics, selectedPipelines }: DoraMetricsChartProps) => { const leadTimeForChange = useRef(null); const deploymentFrequency = useRef(null); const changeFailureRate = useRef(null); @@ -234,7 +238,7 @@ export const DoraMetricsChart = ({ data, dateRanges, metrics }: DoraMetricsChart if (!currentData?.doraMetricsCompleted) { return EMPTY_DATA_MAPPER_DORA_CHART(''); } else { - return reportMapper(currentData); + return reportMapper(currentData, selectedPipelines); } }); diff --git a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx index 05b9c5c417..3e0910670e 100644 --- a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx +++ b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx @@ -4,6 +4,7 @@ import ReportForDeploymentFrequency from '@src/components/Common/ReportForDeploy import ReportForThreeColumns from '@src/components/Common/ReportForThreeColumns'; import ReportForTwoColumns from '@src/components/Common/ReportForTwoColumns'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { reportMapper } from '@src/hooks/reportMapper/report'; import { Optional } from '@src/utils/types'; import { withGoBack } from './withBack'; @@ -11,6 +12,7 @@ import React from 'react'; interface Property { data: ReportResponseDTO; + selectedPipelines: IPipelineConfig[]; onBack: () => void; } @@ -23,8 +25,8 @@ const showDeploymentSection = (title: string, tableTitles: string[], value: Opti const showThreeColumnSection = (title: string, value: Optional) => value && ; -export const DoraDetail = withGoBack(({ data }: Property) => { - const mappedData = reportMapper(data); +export const DoraDetail = withGoBack(({ data, selectedPipelines }: Property) => { + const mappedData = reportMapper(data, selectedPipelines); return ( <> diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index cfa50a69aa..a3c6345cde 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -514,7 +514,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { ); const showDoraChart = (data: (ReportResponseDTO | undefined)[]) => ( - + ); const showBoardChart = (data?: IReportInfo[] | undefined) => ( @@ -524,7 +524,9 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { const showBoardDetail = (data?: ReportResponseDTO) => ( handleBack()} data={data} errorMessage={getErrorMessage4Board()} /> ); - const showDoraDetail = (data: ReportResponseDTO) => backToSummaryPage()} data={data} />; + const showDoraDetail = (data: ReportResponseDTO) => ( + backToSummaryPage()} data={data} selectedPipelines={deploymentFrequencySettings} /> + ); const handleBack = () => { setDisplayType(DISPLAY_TYPE.LIST); diff --git a/frontend/src/containers/ReportStep/style.tsx b/frontend/src/containers/ReportStep/style.tsx index 3014b93dc6..f879289e30 100644 --- a/frontend/src/containers/ReportStep/style.tsx +++ b/frontend/src/containers/ReportStep/style.tsx @@ -79,3 +79,11 @@ export const StyledTab = styled(Tab)({ border: `0.08rem solid ${theme.main.button.borderLine}`, minHeight: '2.5rem', }); + +export const PipelinesSelectContainer = styled('div')({ + display: 'flex', + alignItems: 'flex-start', + [theme.breakpoints.down('lg')]: { + flexDirection: 'column', + }, +}); diff --git a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts index 31d9fa153a..83146b3c6a 100644 --- a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts +++ b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts @@ -1,9 +1,14 @@ import { LeadTimeForChangesResponse } from '@src/clients/report/dto/response'; +import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; + +export const leadTimeForChangesMapper = ( + { leadTimeForChangesOfPipelines, avgLeadTimeForChanges }: LeadTimeForChangesResponse, + selectedPipelines: IPipelineConfig[] | null, +) => { + const nonDataPipelinesName = selectedPipelines + ?.map((item) => `${item.pipelineName}/${item.step}`) + .filter((it) => leadTimeForChangesOfPipelines.every((item) => `${item.name}/${item.step}` !== it)); -export const leadTimeForChangesMapper = ({ - leadTimeForChangesOfPipelines, - avgLeadTimeForChanges, -}: LeadTimeForChangesResponse) => { const minutesPerHour = 60; const formatDuration = (duration: number) => { return (duration / minutesPerHour).toFixed(2); @@ -27,6 +32,27 @@ export const leadTimeForChangesMapper = ({ }; }); + nonDataPipelinesName?.forEach((it, index) => { + mappedLeadTimeForChangesValue.push({ + id: mappedLeadTimeForChangesValue.length + index, + name: it, + valueList: [ + { + name: 'Pipeline Lead Time', + value: '0.00', + }, + { + name: 'PR Lead Time', + value: '0.00', + }, + { + name: 'Total Lead Time', + value: '0.00', + }, + ], + }); + }); + mappedLeadTimeForChangesValue.push({ id: mappedLeadTimeForChangesValue.length, name: avgLeadTimeForChanges.name, @@ -38,5 +64,6 @@ export const leadTimeForChangesMapper = ({ })), }); + console.log(mappedLeadTimeForChangesValue); return mappedLeadTimeForChangesValue; }; diff --git a/frontend/src/hooks/reportMapper/report.ts b/frontend/src/hooks/reportMapper/report.ts index b36e5f9a42..f564cd3d60 100644 --- a/frontend/src/hooks/reportMapper/report.ts +++ b/frontend/src/hooks/reportMapper/report.ts @@ -6,20 +6,24 @@ import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidity import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import { classificationMapper } from '@src/hooks/reportMapper/classification'; import { cycleTimeMapper } from '@src/hooks/reportMapper/cycleTime'; +import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { velocityMapper } from '@src/hooks/reportMapper/velocity'; import reworkMapper from '@src/hooks/reportMapper/reworkMapper'; -export const reportMapper = ({ - velocity, - cycleTime, - classificationList, - deploymentFrequency, - devMeanTimeToRecovery, - leadTimeForChanges, - devChangeFailureRate, - exportValidityTime, - rework, -}: ReportResponseDTO): ReportResponse => { +export const reportMapper = ( + { + velocity, + cycleTime, + classificationList, + deploymentFrequency, + devMeanTimeToRecovery, + leadTimeForChanges, + devChangeFailureRate, + exportValidityTime, + rework, + }: ReportResponseDTO, + selectedPipelines: IPipelineConfig[] | null, +): ReportResponse => { const velocityList = velocity && velocityMapper(velocity); const cycleTimeList = cycleTime && cycleTimeMapper(cycleTime); @@ -32,7 +36,7 @@ export const reportMapper = ({ const devMeanTimeToRecoveryList = devMeanTimeToRecovery && devMeanTimeToRecoveryMapper(devMeanTimeToRecovery); - const leadTimeForChangesList = leadTimeForChanges && leadTimeForChangesMapper(leadTimeForChanges); + const leadTimeForChangesList = leadTimeForChanges && leadTimeForChangesMapper(leadTimeForChanges, selectedPipelines); const devChangeFailureRateList = devChangeFailureRate && devChangeFailureRateMapper(devChangeFailureRate); From cd8f511c901e49ab162b09edc4b98f60fbd30fd8 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Thu, 11 Jul 2024 15:00:09 +0800 Subject: [PATCH 02/20] ADM-975 [frontend]: finish the function that user can select pipelines in the dora chart --- .../PipelineSelector/index.tsx | 73 ++++++++ .../PipelineSelector/style.tsx | 13 ++ .../ReportStep/DoraMetricsChart/index.tsx | 160 ++++++++++++------ frontend/src/containers/ReportStep/index.tsx | 14 +- frontend/src/containers/ReportStep/style.tsx | 8 - .../hooks/reportMapper/leadTimeForChanges.ts | 1 - 6 files changed, 208 insertions(+), 61 deletions(-) create mode 100644 frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx create mode 100644 frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx new file mode 100644 index 0000000000..5abf142bed --- /dev/null +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx @@ -0,0 +1,73 @@ +import { PipelinesSelectContainer } from '@src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style'; +import { getEmojiUrls, removeExtraEmojiName } from '@src/constants/emojis/emoji'; +import { Autocomplete, Box, ListItemText, TextField } from '@mui/material'; +import { EmojiWrap, StyledAvatar } from '@src/constants/emojis/style'; +import { DEFAULT_HELPER_TEXT, Z_INDEX } from '@src/constants/commons'; +import React, { useState } from 'react'; + +interface Props { + options: string[]; + value: string; + onUpDatePipeline: (value: string) => void; + isError?: boolean; + errorText?: string; + title: string; +} + +export default function PipelineSelector({ options, value, onUpDatePipeline, isError, errorText, title }: Props) { + const label = ''; + const [inputValue, setInputValue] = useState(value); + const emojiView = (pipelineStepName: string) => { + const emojiUrls: string[] = getEmojiUrls(pipelineStepName); + return emojiUrls.map((url) => ); + }; + + return ( + + {title}: + removeExtraEmojiName(option).trim()} + renderOption={(props, option: string) => ( + + + {emojiView(option)} + + + + )} + value={value} + onChange={(event, newValue: string) => { + onUpDatePipeline(newValue); + }} + inputValue={inputValue} + onInputChange={(event, newInputValue) => { + setInputValue(newInputValue); + }} + renderInput={(params) => ( + + )} + slotProps={{ + popper: { + sx: { + zIndex: Z_INDEX.DROPDOWN, + }, + }, + }} + /> + + ); +} diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx new file mode 100644 index 0000000000..95ba510e92 --- /dev/null +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx @@ -0,0 +1,13 @@ +import { styled } from '@mui/material/styles'; +import { theme } from '@src/theme'; + +export const PipelinesSelectContainer = styled('div')({ + margin: '1rem 0 0', + display: 'flex', + alignItems: 'flex-start', + width: '50%', + lineHeight: '2rem', + [theme.breakpoints.down('md')]: { + flexDirection: 'column', + }, +}); diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index a4a68dfa54..072293d0e7 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; import { ChartType, @@ -19,10 +19,9 @@ import { LEFT_RIGHT_ALIGN_LABEL, NO_LABEL, } from '@src/containers/ReportStep/BoardMetricsChart'; -import { SingleSelection } from '@src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection'; +import PipelineSelector from '@src/containers/ReportStep/DoraMetricsChart/PipelineSelector'; import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper'; -import { PipelinesSelectContainer } from '@src/containers/ReportStep/style'; import { calculateTrendInfo, percentageFormatter } from '@src/utils/util'; import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { ChartContainer } from '@src/containers/MetricsStep/style'; @@ -30,12 +29,15 @@ import { reportMapper } from '@src/hooks/reportMapper/report'; import { showChart } from '@src/containers/ReportStep'; import { EMPTY_STRING } from '@src/constants/commons'; import { theme } from '@src/theme'; +import { isString } from 'lodash'; interface DoraMetricsChartProps { dateRanges: string[]; data: (ReportResponseDTO | undefined)[]; metrics: string[]; - selectedPipelines: IPipelineConfig[]; + allPipelines: IPipelineConfig[]; + selectedPipeline: string; + onUpdatePipeline: (value: string) => void; } enum DORAMetricsChartType { @@ -47,13 +49,22 @@ enum DORAMetricsChartType { const AVERAGE = 'Average'; const Total = 'Total'; +export const DefaultSelectedPipeline = 'Average/Total'; -function extractedStackedBarData(allDateRanges: string[], mappedData: ReportResponse[] | undefined) { +function extractedStackedBarData( + allDateRanges: string[], + mappedData: ReportResponse[] | undefined, + selectedPipeline: string, +) { const extractedName = mappedData?.[0].leadTimeForChangesList?.[0].valueList .map((item) => LEAD_TIME_CHARTS_MAPPING[item.name]) .slice(0, 2); const extractedValues = mappedData?.map((data) => { - const averageItem = data.leadTimeForChangesList?.find((leadTimeForChange) => leadTimeForChange.name === AVERAGE); + const averageItem = data.leadTimeForChangesList?.find((leadTimeForChange) => + selectedPipeline === DefaultSelectedPipeline + ? leadTimeForChange.name === AVERAGE + : leadTimeForChange.name === selectedPipeline, + ); if (!averageItem) return []; return averageItem.valueList.map((item) => Number(item.value)); @@ -87,15 +98,23 @@ function extractedStackedBarData(allDateRanges: string[], mappedData: ReportResp }; } -function extractedDeploymentFrequencyData(allDateRanges: string[], mappedData: ReportResponse[] | undefined) { +function extractedDeploymentFrequencyData( + allDateRanges: string[], + mappedData: ReportResponse[] | undefined, + selectedPipeline: string, +) { const data = mappedData?.map((item) => item.deploymentFrequencyList); const averageDeploymentFrequency = data?.map((items) => { - const averageItem = items?.find((item) => item.name === AVERAGE); + const averageItem = items?.find((item) => + selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + ); if (!averageItem) return 0; return Number(averageItem.valueList[0].value) || 0; }); const deployTimes = data?.map((items) => { - const averageItem = items?.find((item) => item.name === AVERAGE); + const averageItem = items?.find((item) => + selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + ); if (!averageItem) return 0; return Number(averageItem.valueList[1].value); }); @@ -141,12 +160,27 @@ function extractedDeploymentFrequencyData(allDateRanges: string[], mappedData: R }; } -function extractedChangeFailureRateData(allDateRanges: string[], mappedData: ReportResponse[] | undefined) { +function extractedChangeFailureRateData( + allDateRanges: string[], + mappedData: ReportResponse[] | undefined, + selectedPipeline: string, +) { const data = mappedData?.map((item) => item.devChangeFailureRateList); const value = data?.map((items) => { - const averageItem = items?.find((item) => item.name === AVERAGE); + const averageItem = items?.find((item) => + selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + ); if (!averageItem) return 0; - return Number(averageItem.valueList[0].value) || 0; + const originValue: string | number = averageItem.valueList[0].value; + let value: number = 0; + if (selectedPipeline === DefaultSelectedPipeline) { + value = Number(originValue); + } else { + if (isString(originValue)) { + value = Number(originValue.split('%')[0]); + } + } + return Number(value) || 0; }); const trendInfo = calculateTrendInfo(value, allDateRanges, ChartType.DevChangeFailureRate); return { @@ -170,10 +204,16 @@ function extractedChangeFailureRateData(allDateRanges: string[], mappedData: Rep }; } -function extractedMeanTimeToRecoveryDataData(allDateRanges: string[], mappedData: ReportResponse[] | undefined) { +function extractedMeanTimeToRecoveryDataData( + allDateRanges: string[], + mappedData: ReportResponse[] | undefined, + selectedPipeline: string, +) { const data = mappedData?.map((item) => item.devMeanTimeToRecoveryList); const value = data?.map((items) => { - const totalItem = items?.find((item) => item.name === Total); + const totalItem = items?.find((item) => + selectedPipeline === DefaultSelectedPipeline ? item.name === Total : item.name === selectedPipeline, + ); if (!totalItem) return 0; return Number(totalItem.valueList[0].value) || 0; }); @@ -228,7 +268,14 @@ function isDoraMetricsChartFinish({ ); } -export const DoraMetricsChart = ({ data, dateRanges, metrics, selectedPipelines }: DoraMetricsChartProps) => { +export const DoraMetricsChart = ({ + data, + dateRanges, + metrics, + allPipelines, + selectedPipeline, + onUpdatePipeline, +}: DoraMetricsChartProps) => { const leadTimeForChange = useRef(null); const deploymentFrequency = useRef(null); const changeFailureRate = useRef(null); @@ -238,10 +285,12 @@ export const DoraMetricsChart = ({ data, dateRanges, metrics, selectedPipelines if (!currentData?.doraMetricsCompleted) { return EMPTY_DATA_MAPPER_DORA_CHART(''); } else { - return reportMapper(currentData, selectedPipelines); + return reportMapper(currentData, allPipelines); } }); + console.log(mappedData); + const dateRangeLength: number = dateRanges.length; const isLeadTimeForChangesFinished: boolean = isDoraMetricsChartFinish({ @@ -265,13 +314,13 @@ export const DoraMetricsChart = ({ data, dateRanges, metrics, selectedPipelines type: DORAMetricsChartType.DevMeanTimeToRecovery, }); - const leadTimeForChangeData = extractedStackedBarData(dateRanges, mappedData); + const leadTimeForChangeData = extractedStackedBarData(dateRanges, mappedData, selectedPipeline); const leadTimeForChangeDataOption = leadTimeForChangeData && stackedBarOptionMapper(leadTimeForChangeData, false); - const deploymentFrequencyData = extractedDeploymentFrequencyData(dateRanges, mappedData); + const deploymentFrequencyData = extractedDeploymentFrequencyData(dateRanges, mappedData, selectedPipeline); const deploymentFrequencyDataOption = deploymentFrequencyData && stackedAreaOptionMapper(deploymentFrequencyData); - const changeFailureRateData = extractedChangeFailureRateData(dateRanges, mappedData); + const changeFailureRateData = extractedChangeFailureRateData(dateRanges, mappedData, selectedPipeline); const changeFailureRateDataOption = changeFailureRateData && oneLineOptionMapper(changeFailureRateData); - const meanTimeToRecoveryData = extractedMeanTimeToRecoveryDataData(dateRanges, mappedData); + const meanTimeToRecoveryData = extractedMeanTimeToRecoveryDataData(dateRanges, mappedData, selectedPipeline); const meanTimeToRecoveryDataOption = meanTimeToRecoveryData && oneLineOptionMapper(meanTimeToRecoveryData); useEffect(() => { @@ -290,36 +339,47 @@ export const DoraMetricsChart = ({ data, dateRanges, metrics, selectedPipelines showChart(meanTimeToRecovery.current, isDevMeanTimeToRecoveryValueListFinished, meanTimeToRecoveryDataOption); }, [meanTimeToRecovery, meanTimeToRecoveryDataOption, isDevMeanTimeToRecoveryValueListFinished]); + const pipelineNameOptions = [DefaultSelectedPipeline]; + pipelineNameOptions.push(...allPipelines.map((it) => `${it.pipelineName}/${it.step}`)); + return ( - - {metrics.includes(RequiredData.LeadTimeForChanges) && ( - - )} - {metrics.includes(RequiredData.DeploymentFrequency) && ( - - )} - {metrics.includes(RequiredData.DevChangeFailureRate) && ( - - )} - {metrics.includes(RequiredData.DevMeanTimeToRecovery) && ( - - )} - + <> + onUpdatePipeline(value)} + title={'Pipeline/Step'} + /> + + {metrics.includes(RequiredData.LeadTimeForChanges) && ( + + )} + {metrics.includes(RequiredData.DeploymentFrequency) && ( + + )} + {metrics.includes(RequiredData.DevChangeFailureRate) && ( + + )} + {metrics.includes(RequiredData.DevMeanTimeToRecovery) && ( + + )} + + ); }; diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index a3c6345cde..06de4f6f0d 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -48,9 +48,9 @@ import { REPORT_PAGE_TYPE, RequiredData, } from '@src/constants/resources'; +import { DefaultSelectedPipeline, DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; import { CHART_INDEX, DISPLAY_TYPE, MetricTypes } from '@src/constants/commons'; -import { DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; @@ -114,6 +114,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { const [selectedDateRange, setSelectedDateRange] = useState(descendingDateRanges[0]); const [currentDataInfo, setCurrentDataInfo] = useState(initReportInfo()); + const [selectedPipeline, setSelectedPipeline] = useState(DefaultSelectedPipeline); const { startToRequestData, @@ -514,7 +515,16 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { ); const showDoraChart = (data: (ReportResponseDTO | undefined)[]) => ( - + { + setSelectedPipeline(value); + }} + /> ); const showBoardChart = (data?: IReportInfo[] | undefined) => ( diff --git a/frontend/src/containers/ReportStep/style.tsx b/frontend/src/containers/ReportStep/style.tsx index f879289e30..3014b93dc6 100644 --- a/frontend/src/containers/ReportStep/style.tsx +++ b/frontend/src/containers/ReportStep/style.tsx @@ -79,11 +79,3 @@ export const StyledTab = styled(Tab)({ border: `0.08rem solid ${theme.main.button.borderLine}`, minHeight: '2.5rem', }); - -export const PipelinesSelectContainer = styled('div')({ - display: 'flex', - alignItems: 'flex-start', - [theme.breakpoints.down('lg')]: { - flexDirection: 'column', - }, -}); diff --git a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts index 83146b3c6a..0d08c9fd8b 100644 --- a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts +++ b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts @@ -64,6 +64,5 @@ export const leadTimeForChangesMapper = ( })), }); - console.log(mappedLeadTimeForChangesValue); return mappedLeadTimeForChangesValue; }; From a464b5faa44a0c4d811fa203e217b45f1e9bbae8 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Thu, 11 Jul 2024 15:10:45 +0800 Subject: [PATCH 03/20] ADM-975 [frontend]: remove debug --- frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index 072293d0e7..16767aeb06 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -289,8 +289,6 @@ export const DoraMetricsChart = ({ } }); - console.log(mappedData); - const dateRangeLength: number = dateRanges.length; const isLeadTimeForChangesFinished: boolean = isDoraMetricsChartFinish({ From ff186148a2b3f127774460779142943b1e1e45af Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Thu, 11 Jul 2024 16:55:00 +0800 Subject: [PATCH 04/20] ADM-975 [backend]: change the lead time for change response --- .../report/GenerateReporterService.java | 3 +- .../LeadTimeForChangesCalculator.java | 21 ++++- .../report/GenerateReporterServiceTest.java | 6 +- .../CalculateLeadTimeForChangesTest.java | 80 ++++++++++++++----- 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java index 57e47274c4..14cb387980 100644 --- a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java +++ b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java @@ -265,7 +265,8 @@ private synchronized ReportResponse generateSourceControlReporter(GenerateReport request.getSourceControlMetrics().forEach(metric -> { switch (metric) { case "lead time for changes" -> reportResponse.setLeadTimeForChanges( - leadTimeForChangesCalculator.calculate(fetchedData.getBuildKiteData().getPipelineLeadTimes())); + leadTimeForChangesCalculator.calculate(fetchedData.getBuildKiteData().getPipelineLeadTimes(), + request.getBuildKiteSetting().getDeploymentEnvList())); default -> { // TODO } diff --git a/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java b/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java index 112d4f02f2..78a0a8df81 100644 --- a/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java +++ b/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java @@ -2,6 +2,7 @@ import heartbeat.client.dto.codebase.github.LeadTime; import heartbeat.client.dto.codebase.github.PipelineLeadTime; +import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; import heartbeat.controller.report.dto.response.AvgLeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChangesOfPipelines; @@ -13,6 +14,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Objects; import java.util.stream.LongStream; @Log4j2 @@ -20,7 +22,8 @@ @Component public class LeadTimeForChangesCalculator { - public LeadTimeForChanges calculate(List pipelineLeadTime) { + public LeadTimeForChanges calculate(List pipelineLeadTime, + List deploymentEnvironmentList) { int pipelineCount = pipelineLeadTime.size(); List leadTimeForChangesOfPipelines = new ArrayList<>(); AvgLeadTimeForChanges avgLeadTimeForChanges = new AvgLeadTimeForChanges(); @@ -81,6 +84,22 @@ public LeadTimeForChanges calculate(List pipelineLeadTime) { avgLeadTimeForChanges.setPipelineLeadTime(avgPipelineLeadTime); avgLeadTimeForChanges.setTotalDelayTime(avgPrLeadTime + avgPipelineLeadTime); + List leftOverPipelines = deploymentEnvironmentList.stream() + .filter(deploymentEnvironment -> leadTimeForChangesOfPipelines.stream() + .noneMatch(leadTimeForChangesOfPipeline -> Objects.equals(deploymentEnvironment.getName(), + leadTimeForChangesOfPipeline.getName()) + && Objects.equals(deploymentEnvironment.getStep(), leadTimeForChangesOfPipeline.getStep()))) + .map(it -> LeadTimeForChangesOfPipelines.builder() + .name(it.getName()) + .step(it.getStep()) + .pipelineLeadTime(0.0) + .prLeadTime(0.0) + .totalDelayTime(0.0) + .build()) + .toList(); + + leadTimeForChangesOfPipelines.addAll(leftOverPipelines); + return new LeadTimeForChanges(leadTimeForChangesOfPipelines, avgLeadTimeForChanges); } diff --git a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java index 63ce42164b..18f98bc807 100644 --- a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java @@ -658,7 +658,7 @@ void shouldGenerateCsvWithSourceControlReportWhenSourceControlMetricIsNotEmpty() when(pipelineService.fetchGitHubData(request)) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); LeadTimeForChanges fakeLeadTimeForChange = LeadTimeForChanges.builder().build(); - when(leadTimeForChangesCalculator.calculate(any())).thenReturn(fakeLeadTimeForChange); + when(leadTimeForChangesCalculator.calculate(any(), any())).thenReturn(fakeLeadTimeForChange); generateReporterService.generateDoraReport(request); @@ -698,7 +698,7 @@ void shouldGenerateCsvWithCachedDataWhenBuildKiteDataAlreadyExisted() { when(pipelineService.fetchBuildKiteInfo(any())) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); LeadTimeForChanges fakeLeadTimeForChange = LeadTimeForChanges.builder().build(); - when(leadTimeForChangesCalculator.calculate(any())).thenReturn(fakeLeadTimeForChange); + when(leadTimeForChangesCalculator.calculate(any(), any())).thenReturn(fakeLeadTimeForChange); generateReporterService.generateDoraReport(request); @@ -733,7 +733,7 @@ void shouldUpdateMetricCompletedWhenGenerateCsvWithSourceControlReportFailed() { when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); when(pipelineService.fetchGitHubData(request)).thenReturn( FetchedData.BuildKiteData.builder().pipelineLeadTimes(List.of()).buildInfosList(List.of()).build()); - doThrow(new NotFoundException("")).when(leadTimeForChangesCalculator).calculate(any()); + doThrow(new NotFoundException("")).when(leadTimeForChangesCalculator).calculate(any(), any()); generateReporterService.generateDoraReport(request); diff --git a/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java b/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java index e42abd270e..da174f2ae1 100644 --- a/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java +++ b/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java @@ -4,6 +4,7 @@ import heartbeat.client.dto.codebase.github.LeadTime; import heartbeat.client.dto.codebase.github.PipelineLeadTime; +import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; import heartbeat.controller.report.dto.response.AvgLeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChangesOfPipelines; @@ -47,7 +48,7 @@ void setup() { @Test void shouldReturnEmptyWhenPipelineLeadTimeIsEmpty() { - LeadTimeForChanges result = calculator.calculate(List.of()); + LeadTimeForChanges result = calculator.calculate(List.of(), List.of()); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder().name("Average").build()) @@ -56,18 +57,37 @@ void shouldReturnEmptyWhenPipelineLeadTimeIsEmpty() { } @Test - void shouldReturnLeadTimeForChangesPipelineLeadTimeIsNotEmpty() { + void shouldReturnLeadTimeForChangesPipelineIsNotEmpty() { List pipelineLeadTimes = List.of(pipelineLeadTime); + List deploymentEnvironmentList = List.of( + DeploymentEnvironment.builder().id("1").name("Name").step("Step").build(), + DeploymentEnvironment.builder().id("2").name("Pipeline Name").step("Pipeline Step").build(), + DeploymentEnvironment.builder().id("3").name("Name").step("Pipeline Step").build()); - LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes); + LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes, deploymentEnvironmentList); LeadTimeForChanges expect = LeadTimeForChanges.builder() - .leadTimeForChangesOfPipelines(List.of(LeadTimeForChangesOfPipelines.builder() - .name("Name") - .step("Step") - .prLeadTime(1.0) - .pipelineLeadTime(1.0) - .totalDelayTime(2.0) - .build())) + .leadTimeForChangesOfPipelines(List.of( + LeadTimeForChangesOfPipelines.builder() + .name("Name") + .step("Step") + .prLeadTime(1.0) + .pipelineLeadTime(1.0) + .totalDelayTime(2.0) + .build(), + LeadTimeForChangesOfPipelines.builder() + .name("Pipeline Name") + .step("Pipeline Step") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfPipelines.builder() + .name("Name") + .step("Pipeline Step") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build())) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") .prLeadTime(1.0) @@ -79,12 +99,33 @@ void shouldReturnLeadTimeForChangesPipelineLeadTimeIsNotEmpty() { assertEquals(expect, result); } + @Test + void shouldReturnEmptyWhenLeadTimeIsNull() { + PipelineLeadTime mockPipelineLeadTime = PipelineLeadTime.builder() + .pipelineStep("Step") + .pipelineName("Name") + .build(); + LeadTimeForChanges expect = LeadTimeForChanges.builder() + .leadTimeForChangesOfPipelines(List.of()) + .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() + .name("Average") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build()) + .build(); + + LeadTimeForChanges result = calculator.calculate(List.of(mockPipelineLeadTime), List.of()); + + assertEquals(expect, result); + } + @Test void shouldReturnEmptyWhenLeadTimeIsEmpty() { pipelineLeadTime.setLeadTimes(List.of()); List pipelineLeadTimes = List.of(pipelineLeadTime); - LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes); + LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes, List.of()); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() @@ -103,8 +144,9 @@ void shouldReturnFilteredResultWhenPrMergedTimeOrPrLeadTimeIsNull() { PipelineLeadTime noMergedTime = PipelineLeadTime.builder() .pipelineStep("Step") .pipelineName("Name") - .leadTimes(List.of(LeadTime.builder().prMergedTime(0L).build(), + .leadTimes(List.of(LeadTime.builder().prMergedTime(0L).build(), LeadTime.builder().build(), LeadTime.builder().prMergedTime(1L).prLeadTime(0L).build(), + LeadTime.builder().prMergedTime(1L).build(), LeadTime.builder() .commitId("111") .prCreatedTime(165854910000L) @@ -118,20 +160,20 @@ void shouldReturnFilteredResultWhenPrMergedTimeOrPrLeadTimeIsNull() { .build())) .build(); - LeadTimeForChanges result = calculator.calculate(List.of(noMergedTime)); + LeadTimeForChanges result = calculator.calculate(List.of(noMergedTime), List.of()); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of(LeadTimeForChangesOfPipelines.builder() .name("Name") .step("Step") - .prLeadTime(0.33) - .pipelineLeadTime(0.33) - .totalDelayTime(0.66) + .prLeadTime(0.2) + .pipelineLeadTime(0.2) + .totalDelayTime(0.4) .build())) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") - .prLeadTime(0.33) - .pipelineLeadTime(0.33) - .totalDelayTime(0.66) + .prLeadTime(0.2) + .pipelineLeadTime(0.2) + .totalDelayTime(0.4) .build()) .build(); From a49d68e08154e6b571254029063c082122a9b74c Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Fri, 12 Jul 2024 10:26:19 +0800 Subject: [PATCH 05/20] ADM-975 [frontend][backend]: finish the dora metrics chart selection --- .../ReportStep/DoraMetricsChart/index.tsx | 11 +++--- .../ReportStep/ReportContent/index.tsx | 13 +++++-- .../ReportStep/ReportDetail/dora.tsx | 6 ++-- .../hooks/reportMapper/leadTimeForChanges.ts | 34 +++---------------- frontend/src/hooks/reportMapper/report.ts | 28 +++++++-------- 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index 16767aeb06..36163b5979 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -23,7 +23,6 @@ import PipelineSelector from '@src/containers/ReportStep/DoraMetricsChart/Pipeli import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper'; import { calculateTrendInfo, percentageFormatter } from '@src/utils/util'; -import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { ChartContainer } from '@src/containers/MetricsStep/style'; import { reportMapper } from '@src/hooks/reportMapper/report'; import { showChart } from '@src/containers/ReportStep'; @@ -35,7 +34,6 @@ interface DoraMetricsChartProps { dateRanges: string[]; data: (ReportResponseDTO | undefined)[]; metrics: string[]; - allPipelines: IPipelineConfig[]; selectedPipeline: string; onUpdatePipeline: (value: string) => void; } @@ -272,7 +270,6 @@ export const DoraMetricsChart = ({ data, dateRanges, metrics, - allPipelines, selectedPipeline, onUpdatePipeline, }: DoraMetricsChartProps) => { @@ -285,7 +282,7 @@ export const DoraMetricsChart = ({ if (!currentData?.doraMetricsCompleted) { return EMPTY_DATA_MAPPER_DORA_CHART(''); } else { - return reportMapper(currentData, allPipelines); + return reportMapper(currentData); } }); @@ -338,7 +335,11 @@ export const DoraMetricsChart = ({ }, [meanTimeToRecovery, meanTimeToRecoveryDataOption, isDevMeanTimeToRecoveryValueListFinished]); const pipelineNameOptions = [DefaultSelectedPipeline]; - pipelineNameOptions.push(...allPipelines.map((it) => `${it.pipelineName}/${it.step}`)); + console.log(mappedData); + const allMetrics = mappedData?.[0].leadTimeForChangesList + ?.filter((it) => it.name !== AVERAGE && it.name !== Total) + .map((it) => it.name); + allMetrics && pipelineNameOptions.push(...allMetrics); return ( <> diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index 78d9d184d3..9d0ecf699e 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -32,8 +32,8 @@ import { StyledTab, StyledTabs, } from '@src/containers/ReportStep/style'; +import { DefaultSelectedPipeline, DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; import { CHART_INDEX, DISPLAY_TYPE, MetricTypes } from '@src/constants/commons'; -import { DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import { DateRange, DateRangeList } from '@src/context/config/configSlice'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; @@ -112,6 +112,7 @@ const ReportContent = (props: ReportContentProps) => { const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); const [notifications4SummaryPage, setNotifications4SummaryPage] = useState[]>([]); const [errorNotificationIds, setErrorNotificationIds] = useState([]); + const [selectedPipeline, setSelectedPipeline] = useState(DefaultSelectedPipeline); const startDate = selectedDateRange?.startDate as string; const endDate = selectedDateRange?.endDate as string; @@ -326,7 +327,15 @@ const ReportContent = (props: ReportContentProps) => { ); const showDoraChart = (data: (ReportResponseDTO | undefined)[]) => ( - + { + setSelectedPipeline(value); + }} + /> ); const showBoardChart = (data?: IReportInfo[] | undefined) => ( diff --git a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx index 3e0910670e..05b9c5c417 100644 --- a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx +++ b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx @@ -4,7 +4,6 @@ import ReportForDeploymentFrequency from '@src/components/Common/ReportForDeploy import ReportForThreeColumns from '@src/components/Common/ReportForThreeColumns'; import ReportForTwoColumns from '@src/components/Common/ReportForTwoColumns'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; -import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { reportMapper } from '@src/hooks/reportMapper/report'; import { Optional } from '@src/utils/types'; import { withGoBack } from './withBack'; @@ -12,7 +11,6 @@ import React from 'react'; interface Property { data: ReportResponseDTO; - selectedPipelines: IPipelineConfig[]; onBack: () => void; } @@ -25,8 +23,8 @@ const showDeploymentSection = (title: string, tableTitles: string[], value: Opti const showThreeColumnSection = (title: string, value: Optional) => value && ; -export const DoraDetail = withGoBack(({ data, selectedPipelines }: Property) => { - const mappedData = reportMapper(data, selectedPipelines); +export const DoraDetail = withGoBack(({ data }: Property) => { + const mappedData = reportMapper(data); return ( <> diff --git a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts index 0d08c9fd8b..31d9fa153a 100644 --- a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts +++ b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts @@ -1,14 +1,9 @@ import { LeadTimeForChangesResponse } from '@src/clients/report/dto/response'; -import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; - -export const leadTimeForChangesMapper = ( - { leadTimeForChangesOfPipelines, avgLeadTimeForChanges }: LeadTimeForChangesResponse, - selectedPipelines: IPipelineConfig[] | null, -) => { - const nonDataPipelinesName = selectedPipelines - ?.map((item) => `${item.pipelineName}/${item.step}`) - .filter((it) => leadTimeForChangesOfPipelines.every((item) => `${item.name}/${item.step}` !== it)); +export const leadTimeForChangesMapper = ({ + leadTimeForChangesOfPipelines, + avgLeadTimeForChanges, +}: LeadTimeForChangesResponse) => { const minutesPerHour = 60; const formatDuration = (duration: number) => { return (duration / minutesPerHour).toFixed(2); @@ -32,27 +27,6 @@ export const leadTimeForChangesMapper = ( }; }); - nonDataPipelinesName?.forEach((it, index) => { - mappedLeadTimeForChangesValue.push({ - id: mappedLeadTimeForChangesValue.length + index, - name: it, - valueList: [ - { - name: 'Pipeline Lead Time', - value: '0.00', - }, - { - name: 'PR Lead Time', - value: '0.00', - }, - { - name: 'Total Lead Time', - value: '0.00', - }, - ], - }); - }); - mappedLeadTimeForChangesValue.push({ id: mappedLeadTimeForChangesValue.length, name: avgLeadTimeForChanges.name, diff --git a/frontend/src/hooks/reportMapper/report.ts b/frontend/src/hooks/reportMapper/report.ts index f564cd3d60..b36e5f9a42 100644 --- a/frontend/src/hooks/reportMapper/report.ts +++ b/frontend/src/hooks/reportMapper/report.ts @@ -6,24 +6,20 @@ import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidity import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import { classificationMapper } from '@src/hooks/reportMapper/classification'; import { cycleTimeMapper } from '@src/hooks/reportMapper/cycleTime'; -import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { velocityMapper } from '@src/hooks/reportMapper/velocity'; import reworkMapper from '@src/hooks/reportMapper/reworkMapper'; -export const reportMapper = ( - { - velocity, - cycleTime, - classificationList, - deploymentFrequency, - devMeanTimeToRecovery, - leadTimeForChanges, - devChangeFailureRate, - exportValidityTime, - rework, - }: ReportResponseDTO, - selectedPipelines: IPipelineConfig[] | null, -): ReportResponse => { +export const reportMapper = ({ + velocity, + cycleTime, + classificationList, + deploymentFrequency, + devMeanTimeToRecovery, + leadTimeForChanges, + devChangeFailureRate, + exportValidityTime, + rework, +}: ReportResponseDTO): ReportResponse => { const velocityList = velocity && velocityMapper(velocity); const cycleTimeList = cycleTime && cycleTimeMapper(cycleTime); @@ -36,7 +32,7 @@ export const reportMapper = ( const devMeanTimeToRecoveryList = devMeanTimeToRecovery && devMeanTimeToRecoveryMapper(devMeanTimeToRecovery); - const leadTimeForChangesList = leadTimeForChanges && leadTimeForChangesMapper(leadTimeForChanges, selectedPipelines); + const leadTimeForChangesList = leadTimeForChanges && leadTimeForChangesMapper(leadTimeForChanges); const devChangeFailureRateList = devChangeFailureRate && devChangeFailureRateMapper(devChangeFailureRate); From 49ad666444df469813b23430d293ce076e94f605 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Fri, 12 Jul 2024 11:13:03 +0800 Subject: [PATCH 06/20] ADM-975 [frontend][backend]: finish the dora metrics chart selection --- .../controller/report/ReportController.java | 2 +- .../dto/response/ShareApiDetailsResponse.java | 2 ++ .../heartbeat/repository/FilePrefixType.java | 2 +- .../java/heartbeat/repository/FileType.java | 2 +- .../service/report/ReportService.java | 28 +++++++++++++------ .../service/report/SavedRequestInfo.java | 19 +++++++++++++ .../service/report/ReportServiceTest.java | 18 ++++++------ frontend/src/clients/report/dto/response.ts | 1 + .../ReportStep/BoardMetricsChart/index.tsx | 3 +- .../ReportStep/DoraMetricsChart/index.tsx | 8 ++---- .../ReportStep/ReportContent/index.tsx | 4 +++ frontend/src/containers/ReportStep/index.tsx | 1 + frontend/src/containers/ShareReport/index.tsx | 3 +- frontend/src/hooks/useShareReportEffect.ts | 3 ++ 14 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java diff --git a/backend/src/main/java/heartbeat/controller/report/ReportController.java b/backend/src/main/java/heartbeat/controller/report/ReportController.java index abe519efb5..d3569d0cba 100644 --- a/backend/src/main/java/heartbeat/controller/report/ReportController.java +++ b/backend/src/main/java/heartbeat/controller/report/ReportController.java @@ -79,7 +79,7 @@ public ResponseEntity generateReport(@PathVariable String uuid @RequestBody GenerateReportRequest request) { log.info("Start to generate report, uuid: {}", uuid); reportService.generateReport(request, uuid); - reportService.saveMetrics(request, uuid); + reportService.saveRequestInfo(request, uuid); String callbackUrl = reportService.generateReportCallbackUrl(uuid, TimeUtil.convertToUserSimpleISOFormat(Long.parseLong(request.getStartTime()), request.getTimezoneByZoneId()), diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java b/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java index 8e4419cadc..0214b11722 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java @@ -17,4 +17,6 @@ public class ShareApiDetailsResponse { private List metrics; + private List pipelines; + } diff --git a/backend/src/main/java/heartbeat/repository/FilePrefixType.java b/backend/src/main/java/heartbeat/repository/FilePrefixType.java index 9b2f73bd32..ee86227de0 100644 --- a/backend/src/main/java/heartbeat/repository/FilePrefixType.java +++ b/backend/src/main/java/heartbeat/repository/FilePrefixType.java @@ -5,7 +5,7 @@ @Getter public enum FilePrefixType { - BOARD_REPORT_PREFIX("board-"), METRIC_REPORT_PREFIX("metric-"), PIPELINE_REPORT_PREFIX("pipeline-"), + BOARD_REPORT_PREFIX("board-"), METRIC_REPORT_PREFIX("metric-"), PIPELINE_REPORT_PREFIX("pipeline-"),REQUEST_INFO_REPORT_PREFIX("requestInfo-"), SOURCE_CONTROL_PREFIX("sourceControl-"), ALL_METRICS_PREFIX("allMetrics-"), DATA_COMPLETED_PREFIX("dataCompleted-"); private final String prefix; diff --git a/backend/src/main/java/heartbeat/repository/FileType.java b/backend/src/main/java/heartbeat/repository/FileType.java index 25121d3ac5..0cf1175697 100644 --- a/backend/src/main/java/heartbeat/repository/FileType.java +++ b/backend/src/main/java/heartbeat/repository/FileType.java @@ -8,7 +8,7 @@ public enum FileType { ERROR("error", "error/"), REPORT("report", "report/"), CSV("csv", "csv/"), - METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), METRICS("metrics", "metrics/"); + METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), REQUEST_INFO("request-info", "request-info/"); private final String type; diff --git a/backend/src/main/java/heartbeat/service/report/ReportService.java b/backend/src/main/java/heartbeat/service/report/ReportService.java index 64e0231f80..b3b1004aea 100644 --- a/backend/src/main/java/heartbeat/service/report/ReportService.java +++ b/backend/src/main/java/heartbeat/service/report/ReportService.java @@ -107,17 +107,25 @@ public ShareApiDetailsResponse getShareReportInfo(String uuid) { throw new NotFoundException( String.format("Don't get the data, please check the uuid: %s, maybe it's expired or error", uuid)); } - List metrics = fileRepository.getFiles(FileType.METRICS, uuid) + List savedRequestInfoList = fileRepository.getFiles(FileType.REQUEST_INFO, uuid) .stream() .map(it -> it.split(FILENAME_SEPARATOR)) .map(it -> it[1] + FILENAME_SEPARATOR + it[2] + FILENAME_SEPARATOR + it[3]) - .map(it -> fileRepository.readFileByType(FileType.METRICS, uuid, it, List.class, - FilePrefixType.ALL_METRICS_PREFIX)) - .map(it -> new ArrayList(it)) + .map(it -> fileRepository.readFileByType(FileType.REQUEST_INFO, uuid, it, SavedRequestInfo.class, + FilePrefixType.REQUEST_INFO_REPORT_PREFIX)) + .toList(); + List metrics = savedRequestInfoList + .stream() + .map(SavedRequestInfo::getMetrics) + .flatMap(Collection::stream) + .distinct() + .toList(); + List pipelines = savedRequestInfoList.stream().map(SavedRequestInfo::getPipelines) .flatMap(Collection::stream) + .map(it -> String.format("%s/%s", it.getName(), it.getStep())) .distinct() .toList(); - return ShareApiDetailsResponse.builder().metrics(metrics).reportURLs(reportUrls).build(); + return ShareApiDetailsResponse.builder().metrics(metrics).pipelines(pipelines).reportURLs(reportUrls).build(); } public String generateReportCallbackUrl(String uuid, String startTime, String endTime) { @@ -128,9 +136,13 @@ public UuidResponse generateReportId() { return UuidResponse.builder().reportId(UUID.randomUUID().toString()).build(); } - public void saveMetrics(GenerateReportRequest request, String uuid) { - fileRepository.createFileByType(FileType.METRICS, uuid, request.getTimeRangeAndTimeStamp(), - request.getMetrics(), FilePrefixType.ALL_METRICS_PREFIX); + public void saveRequestInfo(GenerateReportRequest request, String uuid) { + SavedRequestInfo savedRequestInfo = SavedRequestInfo.builder() + .metrics(request.getMetrics()) + .pipelines(request.getBuildKiteSetting().getDeploymentEnvList()) + .build(); + fileRepository.createFileByType(FileType.REQUEST_INFO, uuid, request.getTimeRangeAndTimeStamp(), + savedRequestInfo, FilePrefixType.REQUEST_INFO_REPORT_PREFIX); } } diff --git a/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java new file mode 100644 index 0000000000..b3769e4a82 --- /dev/null +++ b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java @@ -0,0 +1,19 @@ +package heartbeat.service.report; + +import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SavedRequestInfo { + private List metrics; + + private List pipelines; +} diff --git a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java index 2e197376ab..2ad9bbe361 100644 --- a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java @@ -380,12 +380,12 @@ class GetReportUrl { @Test void shouldGetReportUrlsSuccessfully() { when(fileRepository.getFiles(FileType.REPORT, TEST_UUID)).thenReturn(List.of("board-1-2-3", "board-2-3-4")); - when(fileRepository.getFiles(FileType.METRICS, TEST_UUID)) + when(fileRepository.getFiles(FileType.REQUEST_INFO, TEST_UUID)) .thenReturn(List.of("board-0-0-0", "board-9-9-9")); - when(fileRepository.readFileByType(eq(FileType.METRICS), eq(TEST_UUID), eq("0-0-0"), any(), any())) + when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any())) .thenReturn(List.of("test-metrics1", "test-metrics2")); - when(fileRepository.readFileByType(eq(FileType.METRICS), eq(TEST_UUID), eq("9-9-9"), any(), any())) + when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("9-9-9"), any(), any())) .thenReturn(List.of("test-metrics1", "test-metrics3")); ShareApiDetailsResponse shareReportInfo = reportService.getShareReportInfo(TEST_UUID); @@ -402,10 +402,10 @@ void shouldGetReportUrlsSuccessfully() { assertEquals("/reports/test-uuid/detail?startTime=1&endTime=2", reportUrls.get(0)); assertEquals("/reports/test-uuid/detail?startTime=2&endTime=3", reportUrls.get(1)); - verify(fileRepository).getFiles(FileType.METRICS, TEST_UUID); - verify(fileRepository).getFiles(FileType.METRICS, TEST_UUID); - verify(fileRepository).readFileByType(eq(FileType.METRICS), eq(TEST_UUID), eq("0-0-0"), any(), any()); - verify(fileRepository).readFileByType(eq(FileType.METRICS), eq(TEST_UUID), eq("9-9-9"), any(), any()); + verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); + verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); + verify(fileRepository).readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any()); + verify(fileRepository).readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("9-9-9"), any(), any()); } @@ -466,9 +466,9 @@ void shouldSaveMetricsSuccessfully() { .timezone("Asia/Shanghai") .build(); - reportService.saveMetrics(request, TEST_UUID); + reportService.saveRequestInfo(request, TEST_UUID); - verify(fileRepository).createFileByType(eq(FileType.METRICS), eq(TEST_UUID), + verify(fileRepository).createFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(ALL_METRICS_PREFIX)); List savedMetrics = argumentCaptor.getValue(); diff --git a/frontend/src/clients/report/dto/response.ts b/frontend/src/clients/report/dto/response.ts index 27b64b3d07..097f301883 100644 --- a/frontend/src/clients/report/dto/response.ts +++ b/frontend/src/clients/report/dto/response.ts @@ -179,5 +179,6 @@ export interface ReportResponse { export interface ReportURLsResponse { metrics: string[]; + pipelines: string[]; reportURLs: string[]; } diff --git a/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx b/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx index 0f5a981d18..9125562f07 100644 --- a/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx @@ -258,8 +258,7 @@ export const BoardMetricsChart = ({ data, dateRanges, metrics }: BoardMetricsCha const rework = useRef(null); const mappedData: ReportResponse[] | undefined = - data && - data.map((item) => (item.reportData?.boardMetricsCompleted ? reportMapper(item.reportData, null) : emptyData)); + data && data.map((item) => (item.reportData?.boardMetricsCompleted ? reportMapper(item.reportData) : emptyData)); const cycleTimeAllocationData = extractCycleTimeAllocationData(dateRanges, mappedData); const cycleTimeAllocationDataOption = cycleTimeAllocationData && stackedBarOptionMapper(cycleTimeAllocationData); diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index 36163b5979..7bc05ead6a 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -34,6 +34,7 @@ interface DoraMetricsChartProps { dateRanges: string[]; data: (ReportResponseDTO | undefined)[]; metrics: string[]; + allPipelines: string[]; selectedPipeline: string; onUpdatePipeline: (value: string) => void; } @@ -272,6 +273,7 @@ export const DoraMetricsChart = ({ metrics, selectedPipeline, onUpdatePipeline, + allPipelines, }: DoraMetricsChartProps) => { const leadTimeForChange = useRef(null); const deploymentFrequency = useRef(null); @@ -335,11 +337,7 @@ export const DoraMetricsChart = ({ }, [meanTimeToRecovery, meanTimeToRecoveryDataOption, isDevMeanTimeToRecoveryValueListFinished]); const pipelineNameOptions = [DefaultSelectedPipeline]; - console.log(mappedData); - const allMetrics = mappedData?.[0].leadTimeForChangesList - ?.filter((it) => it.name !== AVERAGE && it.name !== Total) - .map((it) => it.name); - allMetrics && pipelineNameOptions.push(...allMetrics); + pipelineNameOptions.push(...allPipelines); return ( <> diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index 9d0ecf699e..2a5451c8cb 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -53,6 +53,7 @@ import { uniqueId } from 'lodash'; export interface ReportContentProps { metrics: string[]; + allPipelines: string[]; dateRanges: DateRangeList; startToRequestDoraData: () => void; startToRequestBoardData: () => void; @@ -77,6 +78,7 @@ export interface DateRangeRequestResult { const ReportContent = (props: ReportContentProps) => { const { metrics, + allPipelines, dateRanges, startToRequestDoraData, startToRequestBoardData, @@ -331,6 +333,7 @@ const ReportContent = (props: ReportContentProps) => { data={data} dateRanges={allDateRanges} metrics={metrics} + allPipelines={allPipelines} selectedPipeline={selectedPipeline} onUpdatePipeline={(value: string) => { setSelectedPipeline(value); @@ -404,6 +407,7 @@ const ReportContent = (props: ReportContentProps) => { }; const showPage = (pageType: string, reportData: ReportResponseDTO | undefined) => { + console.log(reportInfos) switch (pageType) { case REPORT_PAGE_TYPE.SUMMARY: return showSummary(); diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 8323fa9ad9..b933e96e33 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -241,6 +241,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { return ( `${it.pipelineName}/${it.step}`)} dateRanges={dateRanges} startToRequestDoraData={() => startToRequestData(doraReportRequestBody)} startToRequestBoardData={() => startToRequestData(boardReportRequestBody)} diff --git a/frontend/src/containers/ShareReport/index.tsx b/frontend/src/containers/ShareReport/index.tsx index 96a1a7e3e9..1ff2d869c8 100644 --- a/frontend/src/containers/ShareReport/index.tsx +++ b/frontend/src/containers/ShareReport/index.tsx @@ -6,7 +6,7 @@ import ErrorSection from './ErrorSection'; import { useEffect } from 'react'; const ShareReport = () => { - const { getData, reportInfos, dateRanges, metrics, isExpired } = useShareReportEffect(); + const { getData, reportInfos, dateRanges, metrics, isExpired, allPipelines } = useShareReportEffect(); useEffect(() => { getData(); @@ -26,6 +26,7 @@ const ShareReport = () => { { const [dateRanges, setDateRanges] = useState([]); const [reportInfos, setReportInfos] = useState([]); const [metrics, setMetrics] = useState([]); + const [allPipelines, setAllPipelines] = useState([]); const [isExpired, setIsExpired] = useState(false); const getData = async () => { @@ -36,6 +37,7 @@ export const useShareReportEffect = () => { updateReportPageLoadingStatusInfoAfterGetReports(dateRanges, reportRes); setMetrics(reportURLsRes.data.metrics); + setAllPipelines(reportURLsRes.data.pipelines); setDateRanges(dateRanges); setReportInfos(reportInfos); } catch (e) { @@ -115,5 +117,6 @@ export const useShareReportEffect = () => { metrics, isExpired, getData, + allPipelines, }; }; From 865db04e99e1db84a9e3b437051f74699343f149 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Fri, 12 Jul 2024 11:14:33 +0800 Subject: [PATCH 07/20] ADM-975 [frontend]: fix the debug --- frontend/src/containers/ReportStep/ReportContent/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index 2a5451c8cb..702923e63d 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -407,7 +407,6 @@ const ReportContent = (props: ReportContentProps) => { }; const showPage = (pageType: string, reportData: ReportResponseDTO | undefined) => { - console.log(reportInfos) switch (pageType) { case REPORT_PAGE_TYPE.SUMMARY: return showSummary(); From 24183686b077ca816a419f9d9c8f6a1c2739b4fa Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Fri, 12 Jul 2024 11:19:37 +0800 Subject: [PATCH 08/20] Revert "ADM-975 [frontend]: fix the debug" This reverts commit 865db04e99e1db84a9e3b437051f74699343f149. --- frontend/src/containers/ReportStep/ReportContent/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index 702923e63d..2a5451c8cb 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -407,6 +407,7 @@ const ReportContent = (props: ReportContentProps) => { }; const showPage = (pageType: string, reportData: ReportResponseDTO | undefined) => { + console.log(reportInfos) switch (pageType) { case REPORT_PAGE_TYPE.SUMMARY: return showSummary(); From 58fa98690d1ef44a37cad65d3404e64bf588c043 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Fri, 12 Jul 2024 17:45:38 +0800 Subject: [PATCH 09/20] ADM-975 [backend]: fix the backend test coverage --- .../heartbeat/repository/FilePrefixType.java | 5 +- .../java/heartbeat/repository/FileType.java | 3 +- .../report/GenerateReporterService.java | 108 +++++++++--------- .../service/report/ReportService.java | 10 +- .../service/report/SavedRequestInfo.java | 2 + .../report/ReporterControllerTest.java | 4 + .../report/GenerateReporterServiceTest.java | 6 +- .../service/report/ReportServiceTest.java | 45 ++++++-- 8 files changed, 110 insertions(+), 73 deletions(-) diff --git a/backend/src/main/java/heartbeat/repository/FilePrefixType.java b/backend/src/main/java/heartbeat/repository/FilePrefixType.java index ee86227de0..2748f9eea9 100644 --- a/backend/src/main/java/heartbeat/repository/FilePrefixType.java +++ b/backend/src/main/java/heartbeat/repository/FilePrefixType.java @@ -5,8 +5,9 @@ @Getter public enum FilePrefixType { - BOARD_REPORT_PREFIX("board-"), METRIC_REPORT_PREFIX("metric-"), PIPELINE_REPORT_PREFIX("pipeline-"),REQUEST_INFO_REPORT_PREFIX("requestInfo-"), - SOURCE_CONTROL_PREFIX("sourceControl-"), ALL_METRICS_PREFIX("allMetrics-"), DATA_COMPLETED_PREFIX("dataCompleted-"); + BOARD_REPORT_PREFIX("board-"), METRIC_REPORT_PREFIX("metric-"), PIPELINE_REPORT_PREFIX("pipeline-"), + SOURCE_CONTROL_PREFIX("sourceControl-"), REQUEST_INFO_REPORT_PREFIX("requestInfo-"), + DATA_COMPLETED_PREFIX("dataCompleted-"); private final String prefix; diff --git a/backend/src/main/java/heartbeat/repository/FileType.java b/backend/src/main/java/heartbeat/repository/FileType.java index 0cf1175697..e956e089cf 100644 --- a/backend/src/main/java/heartbeat/repository/FileType.java +++ b/backend/src/main/java/heartbeat/repository/FileType.java @@ -8,7 +8,8 @@ public enum FileType { ERROR("error", "error/"), REPORT("report", "report/"), CSV("csv", "csv/"), - METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), REQUEST_INFO("request-info", "request-info/"); + METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), + REQUEST_INFO("request-info", "request-info/"); private final String type; diff --git a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java index 26637ebd31..79e77873e0 100644 --- a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java +++ b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java @@ -85,17 +85,18 @@ public void generateBoardReport(String uuid, GenerateReportRequest request) { String timeRangeAndTimeStamp = request.getTimeRangeAndTimeStamp(); fileRepository.removeFileByType(ERROR, uuid, timeRangeAndTimeStamp, FilePrefixType.BOARD_REPORT_PREFIX); log.info( - "Start to generate board report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", - request.getMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), uuid, - timeRangeAndTimeStamp); + "Start to generate board report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", + request.getMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), uuid, + timeRangeAndTimeStamp); try { saveReporterInHandler(generateBoardReporter(uuid, request), uuid, timeRangeAndTimeStamp, - FilePrefixType.BOARD_REPORT_PREFIX); + FilePrefixType.BOARD_REPORT_PREFIX); log.info( - "Successfully generate board report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", - request.getMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), uuid, - timeRangeAndTimeStamp); - } catch (BaseException e) { + "Successfully generate board report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", + request.getMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), uuid, + timeRangeAndTimeStamp); + } + catch (BaseException e) { fileRepository.createFileByType(ERROR, uuid, timeRangeAndTimeStamp, e, FilePrefixType.BOARD_REPORT_PREFIX); if (List.of(401, 403, 404).contains(e.getStatus())) asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, timeRangeAndTimeStamp, BOARD, false); @@ -118,7 +119,7 @@ public void generateDoraReport(String uuid, GenerateReportRequest request) { } MetricsDataCompleted previousMetricsCompleted = fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, - uuid, timeRangeAndTimeStamp, MetricsDataCompleted.class, FilePrefixType.DATA_COMPLETED_PREFIX); + uuid, timeRangeAndTimeStamp, MetricsDataCompleted.class, FilePrefixType.DATA_COMPLETED_PREFIX); if (previousMetricsCompleted != null && Boolean.FALSE.equals(previousMetricsCompleted.doraMetricsCompleted())) { CompletableFuture.runAsync(() -> generateCSVForPipeline(uuid, request, fetchedData.getBuildKiteData())); @@ -129,20 +130,21 @@ private void generatePipelineReport(String uuid, GenerateReportRequest request, String timeRangeAndTimeStamp = request.getTimeRangeAndTimeStamp(); log.info( - "Start to generate pipeline report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", - request.getPipelineMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), - uuid, timeRangeAndTimeStamp); + "Start to generate pipeline report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", + request.getPipelineMetrics(), request.getCalendarType(), request.getStartTime(), request.getEndTime(), + uuid, timeRangeAndTimeStamp); try { fetchBuildKiteData(request, fetchedData); saveReporterInHandler(generatePipelineReporter(request, fetchedData), uuid, timeRangeAndTimeStamp, - FilePrefixType.PIPELINE_REPORT_PREFIX); + FilePrefixType.PIPELINE_REPORT_PREFIX); log.info( - "Successfully generate pipeline report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", - request.getPipelineMetrics(), request.getCalendarType(), request.getStartTime(), - request.getEndTime(), uuid, timeRangeAndTimeStamp); - } catch (BaseException e) { + "Successfully generate pipeline report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {}, _fileName: {}", + request.getPipelineMetrics(), request.getCalendarType(), request.getStartTime(), + request.getEndTime(), uuid, timeRangeAndTimeStamp); + } + catch (BaseException e) { fileRepository.createFileByType(ERROR, uuid, timeRangeAndTimeStamp, e, - FilePrefixType.PIPELINE_REPORT_PREFIX); + FilePrefixType.PIPELINE_REPORT_PREFIX); if (List.of(401, 403, 404).contains(e.getStatus())) asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, timeRangeAndTimeStamp, DORA, false); } @@ -152,38 +154,39 @@ private void generateSourceControlReport(String uuid, GenerateReportRequest requ String timeRangeAndTimeStamp = request.getTimeRangeAndTimeStamp(); log.info( - "Start to generate source control report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {} _fileName: {}", - request.getSourceControlMetrics(), request.getCalendarType(), request.getStartTime(), - request.getEndTime(), uuid, timeRangeAndTimeStamp); + "Start to generate source control report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {} _fileName: {}", + request.getSourceControlMetrics(), request.getCalendarType(), request.getStartTime(), + request.getEndTime(), uuid, timeRangeAndTimeStamp); try { fetchGitHubData(request, fetchedData); saveReporterInHandler(generateSourceControlReporter(request, fetchedData), uuid, timeRangeAndTimeStamp, - FilePrefixType.SOURCE_CONTROL_PREFIX); + FilePrefixType.SOURCE_CONTROL_PREFIX); log.info( - "Successfully generate source control report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {} _fileName: {}", - request.getSourceControlMetrics(), request.getCalendarType(), request.getStartTime(), - request.getEndTime(), uuid, timeRangeAndTimeStamp); - } catch (BaseException e) { + "Successfully generate source control report, _metrics: {}, _country holiday: {}, _startTime: {}, _endTime: {}, _uuid: {} _fileName: {}", + request.getSourceControlMetrics(), request.getCalendarType(), request.getStartTime(), + request.getEndTime(), uuid, timeRangeAndTimeStamp); + } + catch (BaseException e) { fileRepository.createFileByType(ERROR, uuid, timeRangeAndTimeStamp, e, - FilePrefixType.SOURCE_CONTROL_PREFIX); + FilePrefixType.SOURCE_CONTROL_PREFIX); if (List.of(401, 403, 404).contains(e.getStatus())) asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, timeRangeAndTimeStamp, DORA, false); } } private synchronized ReportResponse generatePipelineReporter(GenerateReportRequest request, - FetchedData fetchedData) { + FetchedData fetchedData) { ReportResponse reportResponse = new ReportResponse(EXPORT_CSV_VALIDITY_TIME); request.getPipelineMetrics().forEach(metric -> { switch (metric) { case "deployment frequency" -> reportResponse.setDeploymentFrequency( - deploymentFrequency.calculate(fetchedData.getBuildKiteData().getDeployTimesList(), - Long.parseLong(request.getStartTime()), Long.parseLong(request.getEndTime()), - request.getCalendarType(), request.getTimezoneByZoneId())); + deploymentFrequency.calculate(fetchedData.getBuildKiteData().getDeployTimesList(), + Long.parseLong(request.getStartTime()), Long.parseLong(request.getEndTime()), + request.getCalendarType(), request.getTimezoneByZoneId())); case "dev change failure rate" -> reportResponse.setDevChangeFailureRate( - devChangeFailureRate.calculate(fetchedData.getBuildKiteData().getDeployTimesList())); + devChangeFailureRate.calculate(fetchedData.getBuildKiteData().getDeployTimesList())); default -> reportResponse.setDevMeanTimeToRecovery(meanToRecoveryCalculator .calculate(fetchedData.getBuildKiteData().getDeployTimesList(), request)); } @@ -214,7 +217,7 @@ private synchronized ReportResponse generateBoardReporter(String uuid, GenerateR private void generateCsvForBoard(String uuid, GenerateReportRequest request, FetchedData fetchedData) { kanbanCsvService.generateCsvInfo(uuid, request, fetchedData.getCardCollectionInfo()); asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, request.getTimeRangeAndTimeStamp(), BOARD, - true); + true); } private void assembleVelocity(FetchedData fetchedData, ReportResponse reportResponse) { @@ -223,19 +226,19 @@ private void assembleVelocity(FetchedData fetchedData, ReportResponse reportResp } private void assembleCycleTime(FetchedData fetchedData, ReportResponse reportResponse, - JiraBoardSetting jiraBoardSetting) { + JiraBoardSetting jiraBoardSetting) { reportResponse.setCycleTime(cycleTimeCalculator.calculateCycleTime( - fetchedData.getCardCollectionInfo().getRealDoneCardCollection(), jiraBoardSetting.getBoardColumns())); + fetchedData.getCardCollectionInfo().getRealDoneCardCollection(), jiraBoardSetting.getBoardColumns())); } private void assembleClassification(FetchedData fetchedData, ReportResponse reportResponse, - JiraBoardSetting jiraBoardSetting) { + JiraBoardSetting jiraBoardSetting) { reportResponse.setClassificationList(classificationCalculator.calculate(jiraBoardSetting.getTargetFields(), - fetchedData.getCardCollectionInfo().getRealDoneCardCollection())); + fetchedData.getCardCollectionInfo().getRealDoneCardCollection())); } private void assembleReworkInfo(GenerateReportRequest request, FetchedData fetchedData, - ReportResponse reportResponse) { + ReportResponse reportResponse) { if (isNull(request.getJiraBoardSetting().getReworkTimesSetting())) { return; } @@ -245,15 +248,14 @@ private void assembleReworkInfo(GenerateReportRequest request, FetchedData fetch } private synchronized ReportResponse generateSourceControlReporter(GenerateReportRequest request, - FetchedData fetchedData) { + FetchedData fetchedData) { ReportResponse reportResponse = new ReportResponse(EXPORT_CSV_VALIDITY_TIME); request.getSourceControlMetrics() .forEach(metric -> reportResponse.setLeadTimeForChanges( - leadTimeForChangesCalculator.calculate(fetchedData.getBuildKiteData().getPipelineLeadTimes(), - request.getBuildKiteSetting().getDeploymentEnvList() - ))); + leadTimeForChangesCalculator.calculate(fetchedData.getBuildKiteData().getPipelineLeadTimes(), + request.getBuildKiteSetting().getDeploymentEnvList()))); return reportResponse; } @@ -281,11 +283,11 @@ private FetchedData fetchJiraBoardData(GenerateReportRequest request, FetchedDat private void generateCSVForPipeline(String uuid, GenerateReportRequest request, BuildKiteData buildKiteData) { List pipelineData = pipelineService.generateCSVForPipeline(request.getStartTime(), - request.getEndTime(), buildKiteData, request.getBuildKiteSetting().getDeploymentEnvList()); + request.getEndTime(), buildKiteData, request.getBuildKiteSetting().getDeploymentEnvList()); csvFileGenerator.convertPipelineDataToCSV(uuid, pipelineData, request.getTimeRangeAndTimeStamp()); asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, request.getTimeRangeAndTimeStamp(), DORA, - true); + true); } public void generateCSVForMetric(String uuid, ReportResponse reportContent, String csvTimeRangeTimeStamp) { @@ -293,7 +295,7 @@ public void generateCSVForMetric(String uuid, ReportResponse reportContent, Stri } private void saveReporterInHandler(ReportResponse reportContent, String uuid, String fileName, - FilePrefixType filePrefixType) { + FilePrefixType filePrefixType) { fileRepository.createFileByType(REPORT, uuid, fileName, reportContent, filePrefixType); } @@ -319,12 +321,12 @@ public MetricsDataCompleted checkReportReadyStatus(String uuid, String timeRange throw new GenerateReportException("Failed to get report due to report time expires"); } return fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, uuid, timeRangeAndTimeStamp, - MetricsDataCompleted.class, FilePrefixType.DATA_COMPLETED_PREFIX); + MetricsDataCompleted.class, FilePrefixType.DATA_COMPLETED_PREFIX); } public ReportResponse getComposedReportResponse(String uuid, String startTime, String endTime) { String timeRangeAndTimeStamp = fileRepository.getFileTimeRangeAndTimeStampByStartTimeAndEndTime( - FileType.METRICS_DATA_COMPLETED, uuid, startTime, endTime); + FileType.METRICS_DATA_COMPLETED, uuid, startTime, endTime); if (timeRangeAndTimeStamp == null) { return ReportResponse.builder() .overallMetricsCompleted(false) @@ -348,11 +350,11 @@ private ReportResponse getComposedReportResponse(String uuid, String timeRangeAn MetricsDataCompleted reportReadyStatus = checkReportReadyStatus(uuid, timeRangeAndTimeStamp); ReportResponse boardReportResponse = fileRepository.readFileByType(REPORT, uuid, timeRangeAndTimeStamp, - ReportResponse.class, FilePrefixType.BOARD_REPORT_PREFIX); + ReportResponse.class, FilePrefixType.BOARD_REPORT_PREFIX); ReportResponse pipelineReportResponse = fileRepository.readFileByType(REPORT, uuid, timeRangeAndTimeStamp, - ReportResponse.class, FilePrefixType.PIPELINE_REPORT_PREFIX); + ReportResponse.class, FilePrefixType.PIPELINE_REPORT_PREFIX); ReportResponse sourceControlReportResponse = fileRepository.readFileByType(REPORT, uuid, timeRangeAndTimeStamp, - ReportResponse.class, FilePrefixType.SOURCE_CONTROL_PREFIX); + ReportResponse.class, FilePrefixType.SOURCE_CONTROL_PREFIX); ReportMetricsError reportMetricsError = getReportErrorAndHandleAsyncException(uuid, timeRangeAndTimeStamp); return ReportResponse.builder() @@ -376,11 +378,11 @@ private ReportResponse getComposedReportResponse(String uuid, String timeRangeAn private ReportMetricsError getReportErrorAndHandleAsyncException(String uuid, String timeRangeAndTimeStamp) { AsyncExceptionDTO boardException = fileRepository.readFileByType(ERROR, uuid, timeRangeAndTimeStamp, - AsyncExceptionDTO.class, FilePrefixType.BOARD_REPORT_PREFIX); + AsyncExceptionDTO.class, FilePrefixType.BOARD_REPORT_PREFIX); AsyncExceptionDTO pipelineException = fileRepository.readFileByType(ERROR, uuid, timeRangeAndTimeStamp, - AsyncExceptionDTO.class, FilePrefixType.PIPELINE_REPORT_PREFIX); + AsyncExceptionDTO.class, FilePrefixType.PIPELINE_REPORT_PREFIX); AsyncExceptionDTO sourceControlException = fileRepository.readFileByType(ERROR, uuid, timeRangeAndTimeStamp, - AsyncExceptionDTO.class, FilePrefixType.SOURCE_CONTROL_PREFIX); + AsyncExceptionDTO.class, FilePrefixType.SOURCE_CONTROL_PREFIX); return ReportMetricsError.builder() .boardMetricsError(handleAsyncExceptionAndGetErrorInfo(boardException)) .pipelineMetricsError(handleAsyncExceptionAndGetErrorInfo(pipelineException)) diff --git a/backend/src/main/java/heartbeat/service/report/ReportService.java b/backend/src/main/java/heartbeat/service/report/ReportService.java index b3b1004aea..9901479717 100644 --- a/backend/src/main/java/heartbeat/service/report/ReportService.java +++ b/backend/src/main/java/heartbeat/service/report/ReportService.java @@ -112,15 +112,15 @@ public ShareApiDetailsResponse getShareReportInfo(String uuid) { .map(it -> it.split(FILENAME_SEPARATOR)) .map(it -> it[1] + FILENAME_SEPARATOR + it[2] + FILENAME_SEPARATOR + it[3]) .map(it -> fileRepository.readFileByType(FileType.REQUEST_INFO, uuid, it, SavedRequestInfo.class, - FilePrefixType.REQUEST_INFO_REPORT_PREFIX)) + FilePrefixType.REQUEST_INFO_REPORT_PREFIX)) .toList(); - List metrics = savedRequestInfoList - .stream() + List metrics = savedRequestInfoList.stream() .map(SavedRequestInfo::getMetrics) .flatMap(Collection::stream) .distinct() .toList(); - List pipelines = savedRequestInfoList.stream().map(SavedRequestInfo::getPipelines) + List pipelines = savedRequestInfoList.stream() + .map(SavedRequestInfo::getPipelines) .flatMap(Collection::stream) .map(it -> String.format("%s/%s", it.getName(), it.getStep())) .distinct() @@ -142,7 +142,7 @@ public void saveRequestInfo(GenerateReportRequest request, String uuid) { .pipelines(request.getBuildKiteSetting().getDeploymentEnvList()) .build(); fileRepository.createFileByType(FileType.REQUEST_INFO, uuid, request.getTimeRangeAndTimeStamp(), - savedRequestInfo, FilePrefixType.REQUEST_INFO_REPORT_PREFIX); + savedRequestInfo, FilePrefixType.REQUEST_INFO_REPORT_PREFIX); } } diff --git a/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java index b3769e4a82..7e42e49217 100644 --- a/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java +++ b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java @@ -13,7 +13,9 @@ @NoArgsConstructor @Builder public class SavedRequestInfo { + private List metrics; private List pipelines; + } diff --git a/backend/src/test/java/heartbeat/controller/report/ReporterControllerTest.java b/backend/src/test/java/heartbeat/controller/report/ReporterControllerTest.java index 945ba3336f..fa9f7017ea 100644 --- a/backend/src/test/java/heartbeat/controller/report/ReporterControllerTest.java +++ b/backend/src/test/java/heartbeat/controller/report/ReporterControllerTest.java @@ -173,6 +173,7 @@ void shouldReturnListCallbackWhenCallGetReportUrls() throws Exception { when(reporterService.getShareReportInfo(uuid)).thenReturn(ShareApiDetailsResponse.builder() .reportURLs(List.of("/reports/test-uuid/detail?startTime=startTime&endTime=endTime")) .metrics(List.of("board")) + .pipelines(List.of("pipeline1", "pipeline2")) .build()); mockMvc.perform(get("/reports/{uuid}", uuid)) @@ -182,6 +183,9 @@ void shouldReturnListCallbackWhenCallGetReportUrls() throws Exception { jsonPath("$.reportURLs[0]").value("/reports/test-uuid/detail?startTime=startTime&endTime=endTime")) .andExpect(jsonPath("$.metrics.length()", Matchers.is(1))) .andExpect(jsonPath("$.metrics[0]").value("board")) + .andExpect(jsonPath("$.pipelines.length()", Matchers.is(2))) + .andExpect(jsonPath("$.pipelines[0]").value("pipeline1")) + .andExpect(jsonPath("$.pipelines[1]").value("pipeline2")) .andReturn() .getResponse(); diff --git a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java index e1146d3dfc..b273a8f506 100644 --- a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java @@ -829,7 +829,7 @@ void shouldGenerateCsvWithSourceControlReportWhenSourceControlMetricIsNotEmpty() assertEquals(fakeLeadTimeForChange, response.getLeadTimeForChanges()); Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { - verify(leadTimeForChangesCalculator, times(1)).calculate(any()); + verify(leadTimeForChangesCalculator, times(1)).calculate(any(), any()); verify(pipelineService, times(1)).generateCSVForPipeline(any(), any(), any(), any()); verify(csvFileGenerator, times(1)).convertPipelineDataToCSV(TEST_UUID, pipelineCSVInfos, timeRangeAndTimeStamp); @@ -889,7 +889,7 @@ void shouldGenerateCsvWithCachedDataWhenBuildKiteDataAlreadyExisted() { timeRangeAndTimeStamp); verify(asyncMetricsDataHandler, times(1)).updateMetricsDataCompletedInHandler(TEST_UUID, timeRangeAndTimeStamp, DORA, true); - verify(leadTimeForChangesCalculator, times(1)).calculate(any()); + verify(leadTimeForChangesCalculator, times(1)).calculate(any(), any()); }); } @@ -918,7 +918,7 @@ void shouldUpdateMetricCompletedWhenGenerateCsvWithSourceControlReportFailed() { generateReporterService.generateDoraReport(TEST_UUID, request); verify(kanbanService, never()).fetchDataFromKanban(request); - verify(leadTimeForChangesCalculator, times(1)).calculate(any()); + verify(leadTimeForChangesCalculator, times(1)).calculate(any(), any()); verify(fileRepository, times(1)).removeFileByType(ERROR, TEST_UUID, timeRangeAndTimeStamp, FilePrefixType.PIPELINE_REPORT_PREFIX); verify(fileRepository, times(1)).removeFileByType(ERROR, TEST_UUID, timeRangeAndTimeStamp, diff --git a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java index 2ad9bbe361..c0d0a05f5f 100644 --- a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java @@ -1,5 +1,7 @@ package heartbeat.service.report; +import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; +import heartbeat.controller.report.dto.request.BuildKiteSetting; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.MetricType; import heartbeat.controller.report.dto.request.ReportType; @@ -39,7 +41,7 @@ import static heartbeat.controller.report.dto.request.MetricType.BOARD; import static heartbeat.controller.report.dto.request.MetricType.DORA; -import static heartbeat.repository.FilePrefixType.ALL_METRICS_PREFIX; +import static heartbeat.repository.FilePrefixType.REQUEST_INFO_REPORT_PREFIX; import static heartbeat.tools.TimeUtils.mockTimeStamp; import static heartbeat.repository.FileRepository.EXPORT_CSV_VALIDITY_TIME; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -74,7 +76,7 @@ public class ReportServiceTest { ReportGenerator reportGenerator; @Captor - ArgumentCaptor> argumentCaptor; + ArgumentCaptor argumentCaptor; public static final String START_TIME = "20240310"; @@ -384,9 +386,17 @@ void shouldGetReportUrlsSuccessfully() { .thenReturn(List.of("board-0-0-0", "board-9-9-9")); when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any())) - .thenReturn(List.of("test-metrics1", "test-metrics2")); + .thenReturn(SavedRequestInfo.builder().metrics(List.of("test-metrics1", "test-metrics2")).pipelines(List.of( + DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), + DeploymentEnvironment.builder().id("1").name("pipeline1").step("step2").build(), + DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build() + )).build()); when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("9-9-9"), any(), any())) - .thenReturn(List.of("test-metrics1", "test-metrics3")); + .thenReturn(SavedRequestInfo.builder().metrics(List.of("test-metrics1", "test-metrics3")).pipelines(List.of( + DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), + DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build(), + DeploymentEnvironment.builder().id("1").name("pipeline2").step("step2").build() + )).build()); ShareApiDetailsResponse shareReportInfo = reportService.getShareReportInfo(TEST_UUID); List metrics = shareReportInfo.getMetrics(); @@ -402,6 +412,13 @@ void shouldGetReportUrlsSuccessfully() { assertEquals("/reports/test-uuid/detail?startTime=1&endTime=2", reportUrls.get(0)); assertEquals("/reports/test-uuid/detail?startTime=2&endTime=3", reportUrls.get(1)); + List pipelines = shareReportInfo.getPipelines(); + assertEquals(4, pipelines.size()); + assertEquals("pipeline1/step1", pipelines.get(0)); + assertEquals("pipeline1/step2", pipelines.get(1)); + assertEquals("pipeline2/step1", pipelines.get(2)); + assertEquals("pipeline2/step2", pipelines.get(3)); + verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); verify(fileRepository).readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any()); @@ -463,19 +480,29 @@ void shouldSaveMetricsSuccessfully() { .startTime(startTimeStamp) .endTime(endTimeStamp) .metrics(List.of("test-metrics1", "test-metrics2")) + .buildKiteSetting(BuildKiteSetting.builder() + .deploymentEnvList(List.of(DeploymentEnvironment.builder().id("1").build())) + .build()) .timezone("Asia/Shanghai") .build(); reportService.saveRequestInfo(request, TEST_UUID); verify(fileRepository).createFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), - eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(ALL_METRICS_PREFIX)); + eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(REQUEST_INFO_REPORT_PREFIX)); + + SavedRequestInfo savedRequestInfo = argumentCaptor.getValue(); + + List pipelines = savedRequestInfo.getPipelines(); - List savedMetrics = argumentCaptor.getValue(); + assertEquals(1, pipelines.size()); + assertEquals("1", pipelines.get(0).getId()); - assertEquals(2, savedMetrics.size()); - assertEquals("test-metrics1", savedMetrics.get(0)); - assertEquals("test-metrics2", savedMetrics.get(1)); + List metrics = savedRequestInfo.getMetrics(); + + assertEquals(2, metrics.size()); + assertEquals("test-metrics1", metrics.get(0)); + assertEquals("test-metrics2", metrics.get(1)); } } From 0e594e0e0d50f77ddb967b61e348ef465d7a6c1b Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 09:20:35 +0800 Subject: [PATCH 10/20] ADM-975 [frontend]: fix test coverage and cr comment --- .../__tests__/constants/emojis/emoji.test.tsx | 6 + .../containers/ReportStep/ReportStep.test.tsx | 40 +++++- frontend/__tests__/fixtures.ts | 126 ++++++++++++++++++ frontend/src/constants/resources.ts | 115 ++++++++-------- .../PipelineSelector/index.tsx | 32 ++--- .../ReportStep/DoraMetricsChart/index.tsx | 35 +++-- .../ReportStep/ReportContent/index.tsx | 5 +- 7 files changed, 258 insertions(+), 101 deletions(-) diff --git a/frontend/__tests__/constants/emojis/emoji.test.tsx b/frontend/__tests__/constants/emojis/emoji.test.tsx index 6f00d9e2e6..09a6a940c9 100644 --- a/frontend/__tests__/constants/emojis/emoji.test.tsx +++ b/frontend/__tests__/constants/emojis/emoji.test.tsx @@ -41,5 +41,11 @@ describe('#emojis', () => { expect(removeExtraEmojiName(input)).toEqual(' one emojis'); }); + + it('should return null when no colons', () => { + const input = 'mock pipeline name/mock step1'; + + expect(removeExtraEmojiName(input)).toEqual('mock pipeline name/mock step1'); + }); }); }); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 4be10df40a..7e131a3a7e 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -10,7 +10,9 @@ import { EXPORT_METRIC_DATA, EXPORT_PIPELINE_DATA, LEAD_TIME_FOR_CHANGES, + LIST_OPEN, MOCK_JIRA_VERIFY_RESPONSE, + MOCK_REPORT_MOCK_PIPELINE_RESPONSE, MOCK_REPORT_RESPONSE, MOCK_REPORT_RESPONSE_WITH_AVERAGE_EXCEPTION, PREVIOUS, @@ -27,8 +29,8 @@ import { updatePipelineToolVerifyResponse, } from '@src/context/config/configSlice'; import { addADeploymentFrequencySetting, updateDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; +import { act, render, renderHook, screen, waitFor, within } from '@testing-library/react'; import { DATA_LOADING_FAILED, DEFAULT_MESSAGE, MESSAGE } from '@src/constants/resources'; -import { act, render, renderHook, screen, waitFor } from '@testing-library/react'; import { closeNotification } from '@src/context/notification/NotificationSlice'; import { addNotification } from '@src/context/notification/NotificationSlice'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; @@ -73,7 +75,9 @@ jest.mock('@src/hooks/useGenerateReportEffect', () => ({ jest.mock('@src/constants/emojis/emoji', () => ({ getEmojiUrls: jest.fn().mockReturnValue(['']), - removeExtraEmojiName: jest.fn(), + removeExtraEmojiName: jest.fn((param) => { + return param; + }), })); jest.mock('@src/utils/util', () => ({ @@ -751,11 +755,43 @@ describe('Report Step', () => { }); it('should correctly render dora chart', async () => { + reportHook.current.reportInfos[0].reportData = { ...MOCK_REPORT_MOCK_PIPELINE_RESPONSE }; + setup(REQUIRED_DATA_LIST, [fullValueDateRange, emptyValueDateRange]); const switchChartButton = screen.getByText(DISPLAY_TYPE.CHART); await userEvent.click(switchChartButton); + const switchDoraChartButton = screen.getByText(CHART_TYPE.DORA); + await userEvent.click(switchDoraChartButton); + + const pipelineSelector = screen.getByLabelText('Pipeline Selector'); + expect(pipelineSelector).toBeInTheDocument(); + + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: LIST_OPEN })); + }); + + let listBox = screen.getByRole('listbox'); + expect(listBox).toBeInTheDocument(); + + await act(async () => { + await userEvent.click(within(listBox).getByText('Average/Total')); + }); + + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: LIST_OPEN })); + }); + + listBox = screen.getByRole('listbox'); + expect(listBox).toBeInTheDocument(); + + await act(async () => { + const pipelineSelector = within(listBox).getByText('mock pipeline name/mock step1'); + expect(pipelineSelector).toBeInTheDocument(); + await userEvent.click(pipelineSelector); + }); + expect(addNotification).toHaveBeenCalledWith({ message: MESSAGE.EXPIRE_INFORMATION, }); diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts index 562018ed91..87b8febd08 100644 --- a/frontend/__tests__/fixtures.ts +++ b/frontend/__tests__/fixtures.ts @@ -700,6 +700,132 @@ export const MOCK_REPORT_RESPONSE: ReportResponseDTO = { reportMetricsError, }; +export const MOCK_REPORT_MOCK_PIPELINE_RESPONSE: ReportResponseDTO = { + velocity: { + velocityForSP: 20, + velocityForCards: 14, + }, + cycleTime: { + averageCycleTimePerCard: 30.26, + averageCycleTimePerSP: 21.18, + totalTimeForCards: 423.59, + swimlaneList: [ + { + optionalItemName: 'Analysis', + averageTimeForSP: 8.36, + averageTimeForCards: 11.95, + totalTime: 167.27, + }, + { + optionalItemName: 'In Dev', + averageTimeForSP: 12.13, + averageTimeForCards: 17.32, + totalTime: 242.51, + }, + ], + }, + deploymentFrequency: { + avgDeploymentFrequency: { + name: 'Average', + deploymentFrequency: 0.4, + }, + deploymentFrequencyOfPipelines: [ + { + name: 'mock pipeline name', + step: 'mock step1', + deploymentFrequency: 0.3, + dailyDeploymentCounts: [ + { + date: '9/9/2022', + count: 1, + }, + ], + deployTimes: 10, + }, + ], + totalDeployTimes: 10, + }, + devMeanTimeToRecovery: { + avgDevMeanTimeToRecovery: { + name: 'Total', + timeToRecovery: 14396108.777777776, + }, + devMeanTimeToRecoveryOfPipelines: [ + { + name: 'mock pipeline name', + step: 'mock step1', + timeToRecovery: 15560177, + }, + ], + }, + rework: { + totalReworkTimes: 111, + reworkState: 'In Dev', + fromAnalysis: null, + fromInDev: null, + fromBlock: 111, + fromReview: 111, + fromWaitingForTesting: 111, + fromTesting: null, + fromDone: 111, + totalReworkCards: 111, + reworkCardsRatio: 0.8888, + throughput: 1110, + }, + leadTimeForChanges: { + leadTimeForChangesOfPipelines: [ + { + name: 'mock pipeline name', + step: 'mock step1', + prLeadTime: 2702.53, + pipelineLeadTime: 2587.42, + totalDelayTime: 5289.95, + }, + ], + avgLeadTimeForChanges: { + name: 'Average', + prLeadTime: 3647.51, + pipelineLeadTime: 2341.72, + totalDelayTime: 5989.22, + }, + }, + devChangeFailureRate: { + avgDevChangeFailureRate: { + name: 'Average', + totalTimes: 6, + totalFailedTimes: 0, + failureRate: 0.0, + }, + devChangeFailureRateOfPipelines: [ + { + name: 'mock pipeline name', + step: 'mock step1', + failedTimesOfPipeline: 0, + totalTimesOfPipeline: 2, + failureRate: 0.0, + }, + ], + }, + classificationList: [ + { + fieldName: 'FS Work Type', + pairList: [ + { + name: 'Feature Work - Planned', + value: 0.5714, + }, + ], + }, + ], + exportValidityTime: 1800000, + boardMetricsCompleted: true, + doraMetricsCompleted: true, + overallMetricsCompleted: true, + allMetricsCompleted: true, + isSuccessfulCreateCsvFile: true, + reportMetricsError, +}; + export const MOCK_RETRIEVE_REPORT_RESPONSE = { callbackUrl: 'reports/123', interval: 10, diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index d56dc071bb..1041a3fe78 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -543,61 +543,68 @@ export enum SortingDateRangeText { export const DISABLED_DATE_RANGE_MESSAGE = 'Report generated failed during this period.'; -export const EMPTY_DATA_MAPPER_DORA_CHART = (value: string) => { +export const emptyDataMapperDoraChart = (allPipelines: string[], value: string) => { + const deploymentFrequencyList = allPipelines.map((it, index) => { + return { + id: index, + name: it, + valueList: [ + { + value: value, + }, + { + value: value, + }, + ], + }; + }); + const devMeanTimeToRecoveryList = allPipelines.map((it, index) => { + return { + id: index, + name: it, + valueList: [ + { + value: value, + }, + ], + }; + }); + const leadTimeForChangesList = allPipelines.map((it, index) => { + return { + id: index, + name: it, + valueList: [ + { + name: LEAD_TIME_FOR_CHANGES.PR_LEAD_TIME, + value: value, + }, + { + name: LEAD_TIME_FOR_CHANGES.PIPELINE_LEAD_TIME, + value: value, + }, + { + name: LEAD_TIME_FOR_CHANGES.TOTAL_LEAD_TIME, + value: value, + }, + ], + }; + }); + const devChangeFailureRateList = allPipelines.map((it, index) => { + return { + id: index, + name: it, + valueList: [ + { + value: value + '%(0/0)', + }, + ], + }; + }); return { - deploymentFrequencyList: [ - { - id: 0, - name: 'Heartbeat/:rocket: Deploy prod', - valueList: [ - { - value: value, - }, - ], - }, - ], - devMeanTimeToRecoveryList: [ - { - id: 0, - name: 'Heartbeat/:rocket: Deploy prod', - valueList: [ - { - value: value, - }, - ], - }, - ], - leadTimeForChangesList: [ - { - id: 0, - name: 'Average', - valueList: [ - { - name: LEAD_TIME_FOR_CHANGES.PR_LEAD_TIME, - value: value, - }, - { - name: LEAD_TIME_FOR_CHANGES.PIPELINE_LEAD_TIME, - value: value, - }, - { - name: LEAD_TIME_FOR_CHANGES.TOTAL_LEAD_TIME, - value: value, - }, - ], - }, - ], - devChangeFailureRateList: [ - { - id: 0, - name: 'Heartbeat/:rocket: Deploy prod', - valueList: [ - { - value: value + '%(0/0)', - }, - ], - }, - ], + deploymentFrequencyList, + devMeanTimeToRecoveryList, + leadTimeForChangesList, + devChangeFailureRateList, exportValidityTimeMin: 0.0005, }; }; diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx index 5abf142bed..d2bcb4788a 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx @@ -2,19 +2,17 @@ import { PipelinesSelectContainer } from '@src/containers/ReportStep/DoraMetrics import { getEmojiUrls, removeExtraEmojiName } from '@src/constants/emojis/emoji'; import { Autocomplete, Box, ListItemText, TextField } from '@mui/material'; import { EmojiWrap, StyledAvatar } from '@src/constants/emojis/style'; -import { DEFAULT_HELPER_TEXT, Z_INDEX } from '@src/constants/commons'; +import { Z_INDEX } from '@src/constants/commons'; import React, { useState } from 'react'; interface Props { options: string[]; value: string; onUpDatePipeline: (value: string) => void; - isError?: boolean; - errorText?: string; title: string; } -export default function PipelineSelector({ options, value, onUpDatePipeline, isError, errorText, title }: Props) { +export default function PipelineSelector({ options, value, onUpDatePipeline, title }: Props) { const label = ''; const [inputValue, setInputValue] = useState(value); const emojiView = (pipelineStepName: string) => { @@ -30,36 +28,24 @@ export default function PipelineSelector({ options, value, onUpDatePipeline, isE sx={{ flex: 1, marginLeft: '1rem', + minWidth: '340px', }} - data-test-id={'Pipeline Selector'} + aria-label={'Pipeline Selector'} options={options} getOptionLabel={(option: string) => removeExtraEmojiName(option).trim()} renderOption={(props, option: string) => ( - + {emojiView(option)} - + )} value={value} - onChange={(event, newValue: string) => { - onUpDatePipeline(newValue); - }} + onChange={(event, newValue: string) => onUpDatePipeline(newValue)} inputValue={inputValue} - onInputChange={(event, newInputValue) => { - setInputValue(newInputValue); - }} - renderInput={(params) => ( - - )} + onInputChange={(event, newInputValue) => setInputValue(newInputValue)} + renderInput={(params) => } slotProps={{ popper: { sx: { diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index 7bc05ead6a..4a8119b653 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { ChartType, - EMPTY_DATA_MAPPER_DORA_CHART, + emptyDataMapperDoraChart, LEAD_TIME_CHARTS_MAPPING, MetricsSubtitle, RequiredData, @@ -47,8 +47,8 @@ enum DORAMetricsChartType { } const AVERAGE = 'Average'; -const Total = 'Total'; -export const DefaultSelectedPipeline = 'Average/Total'; +const TOTAL = 'Total'; +export const DEFAULT_SELECTED_PIPELINE = 'Average/Total'; function extractedStackedBarData( allDateRanges: string[], @@ -60,7 +60,7 @@ function extractedStackedBarData( .slice(0, 2); const extractedValues = mappedData?.map((data) => { const averageItem = data.leadTimeForChangesList?.find((leadTimeForChange) => - selectedPipeline === DefaultSelectedPipeline + selectedPipeline === DEFAULT_SELECTED_PIPELINE ? leadTimeForChange.name === AVERAGE : leadTimeForChange.name === selectedPipeline, ); @@ -105,14 +105,14 @@ function extractedDeploymentFrequencyData( const data = mappedData?.map((item) => item.deploymentFrequencyList); const averageDeploymentFrequency = data?.map((items) => { const averageItem = items?.find((item) => - selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + selectedPipeline === DEFAULT_SELECTED_PIPELINE ? item.name === AVERAGE : item.name === selectedPipeline, ); if (!averageItem) return 0; return Number(averageItem.valueList[0].value) || 0; }); const deployTimes = data?.map((items) => { const averageItem = items?.find((item) => - selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + selectedPipeline === DEFAULT_SELECTED_PIPELINE ? item.name === AVERAGE : item.name === selectedPipeline, ); if (!averageItem) return 0; return Number(averageItem.valueList[1].value); @@ -167,19 +167,17 @@ function extractedChangeFailureRateData( const data = mappedData?.map((item) => item.devChangeFailureRateList); const value = data?.map((items) => { const averageItem = items?.find((item) => - selectedPipeline === DefaultSelectedPipeline ? item.name === AVERAGE : item.name === selectedPipeline, + selectedPipeline === DEFAULT_SELECTED_PIPELINE ? item.name === AVERAGE : item.name === selectedPipeline, ); if (!averageItem) return 0; const originValue: string | number = averageItem.valueList[0].value; - let value: number = 0; - if (selectedPipeline === DefaultSelectedPipeline) { - value = Number(originValue); + let value: number; + if (selectedPipeline !== DEFAULT_SELECTED_PIPELINE && isString(originValue)) { + value = Number(originValue.split('%')[0]); } else { - if (isString(originValue)) { - value = Number(originValue.split('%')[0]); - } + value = Number(originValue); } - return Number(value) || 0; + return value || 0; }); const trendInfo = calculateTrendInfo(value, allDateRanges, ChartType.DevChangeFailureRate); return { @@ -211,7 +209,7 @@ function extractedMeanTimeToRecoveryDataData( const data = mappedData?.map((item) => item.devMeanTimeToRecoveryList); const value = data?.map((items) => { const totalItem = items?.find((item) => - selectedPipeline === DefaultSelectedPipeline ? item.name === Total : item.name === selectedPipeline, + selectedPipeline === DEFAULT_SELECTED_PIPELINE ? item.name === TOTAL : item.name === selectedPipeline, ); if (!totalItem) return 0; return Number(totalItem.valueList[0].value) || 0; @@ -258,7 +256,7 @@ function isDoraMetricsChartFinish({ const valueList = mappedData .flatMap((value) => value[type] as unknown as ChartValueSource[]) .filter((value) => - type === DORAMetricsChartType.DevMeanTimeToRecovery ? value?.name === Total : value?.name === AVERAGE, + type === DORAMetricsChartType.DevMeanTimeToRecovery ? value?.name === TOTAL : value?.name === AVERAGE, ) .map((value) => value?.valueList); @@ -282,7 +280,7 @@ export const DoraMetricsChart = ({ const mappedData = data.map((currentData) => { if (!currentData?.doraMetricsCompleted) { - return EMPTY_DATA_MAPPER_DORA_CHART(''); + return emptyDataMapperDoraChart(allPipelines, ''); } else { return reportMapper(currentData); } @@ -336,8 +334,7 @@ export const DoraMetricsChart = ({ showChart(meanTimeToRecovery.current, isDevMeanTimeToRecoveryValueListFinished, meanTimeToRecoveryDataOption); }, [meanTimeToRecovery, meanTimeToRecoveryDataOption, isDevMeanTimeToRecoveryValueListFinished]); - const pipelineNameOptions = [DefaultSelectedPipeline]; - pipelineNameOptions.push(...allPipelines); + const pipelineNameOptions = [DEFAULT_SELECTED_PIPELINE, ...allPipelines]; return ( <> diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index 2a5451c8cb..c9df3773f8 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -32,7 +32,7 @@ import { StyledTab, StyledTabs, } from '@src/containers/ReportStep/style'; -import { DefaultSelectedPipeline, DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; +import { DEFAULT_SELECTED_PIPELINE, DoraMetricsChart } from '@src/containers/ReportStep/DoraMetricsChart'; import { CHART_INDEX, DISPLAY_TYPE, MetricTypes } from '@src/constants/commons'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import { DateRange, DateRangeList } from '@src/context/config/configSlice'; @@ -114,7 +114,7 @@ const ReportContent = (props: ReportContentProps) => { const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); const [notifications4SummaryPage, setNotifications4SummaryPage] = useState[]>([]); const [errorNotificationIds, setErrorNotificationIds] = useState([]); - const [selectedPipeline, setSelectedPipeline] = useState(DefaultSelectedPipeline); + const [selectedPipeline, setSelectedPipeline] = useState(DEFAULT_SELECTED_PIPELINE); const startDate = selectedDateRange?.startDate as string; const endDate = selectedDateRange?.endDate as string; @@ -407,7 +407,6 @@ const ReportContent = (props: ReportContentProps) => { }; const showPage = (pageType: string, reportData: ReportResponseDTO | undefined) => { - console.log(reportInfos) switch (pageType) { case REPORT_PAGE_TYPE.SUMMARY: return showSummary(); From e9854e56cc97871deae288efaa7b3f5c289e0aa2 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 09:26:20 +0800 Subject: [PATCH 11/20] ADM-975 [frontend]: fix issues --- .../ReportStep/DoraMetricsChart/PipelineSelector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx index d2bcb4788a..968d721bff 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx @@ -28,7 +28,7 @@ export default function PipelineSelector({ options, value, onUpDatePipeline, tit sx={{ flex: 1, marginLeft: '1rem', - minWidth: '340px', + minWidth: '22rem', }} aria-label={'Pipeline Selector'} options={options} From 42a0264ae00b30c2c9ceef0e524b18ab498bbd6a Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 11:05:18 +0800 Subject: [PATCH 12/20] ADM-975 [frontend]: adjust the style of pipeline selector --- .../PipelineSelector/index.tsx | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx index 968d721bff..b6e82fc37b 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx @@ -1,6 +1,6 @@ import { PipelinesSelectContainer } from '@src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style'; +import { Autocomplete, Box, ListItemText, TextField, Tooltip } from '@mui/material'; import { getEmojiUrls, removeExtraEmojiName } from '@src/constants/emojis/emoji'; -import { Autocomplete, Box, ListItemText, TextField } from '@mui/material'; import { EmojiWrap, StyledAvatar } from '@src/constants/emojis/style'; import { Z_INDEX } from '@src/constants/commons'; import React, { useState } from 'react'; @@ -15,6 +15,7 @@ interface Props { export default function PipelineSelector({ options, value, onUpDatePipeline, title }: Props) { const label = ''; const [inputValue, setInputValue] = useState(value); + const emojiView = (pipelineStepName: string) => { const emojiUrls: string[] = getEmojiUrls(pipelineStepName); return emojiUrls.map((url) => ); @@ -27,20 +28,36 @@ export default function PipelineSelector({ options, value, onUpDatePipeline, tit disableClearable sx={{ flex: 1, + paddingRight: '0.62rem', marginLeft: '1rem', minWidth: '22rem', }} aria-label={'Pipeline Selector'} options={options} getOptionLabel={(option: string) => removeExtraEmojiName(option).trim()} - renderOption={(props, option: string) => ( - - - {emojiView(option)} - - - - )} + renderOption={(props, option: string) => { + const optionContent = removeExtraEmojiName(option).trim(); + const emojiWrap = <>{emojiView(option)}; + return ( + + {emojiWrap} + {optionContent} + + } + placement='right' + followCursor + > + + + {emojiWrap} + + + + + ); + }} value={value} onChange={(event, newValue: string) => onUpDatePipeline(newValue)} inputValue={inputValue} From 33c4e28979ca2dd7d46b68f8b9ca73a70e9029b1 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 11:53:18 +0800 Subject: [PATCH 13/20] ADM-975 [backend]: remove the request info files --- .../service/report/scheduler/DeleteExpireCSVScheduler.java | 1 + .../heartbeat/service/report/DeleteExpireCSVSchedulerTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java b/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java index 237f83268a..5a05069870 100644 --- a/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java +++ b/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java @@ -27,6 +27,7 @@ public void triggerBatchDelete() { fileRepository.removeExpiredFiles(FileType.REPORT, currentTimeStamp); fileRepository.removeExpiredFiles(FileType.ERROR, currentTimeStamp); fileRepository.removeExpiredFiles(FileType.METRICS_DATA_COMPLETED, currentTimeStamp); + fileRepository.removeExpiredFiles(FileType.REQUEST_INFO, currentTimeStamp); } } diff --git a/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java b/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java index b7ca635674..7e7aa03ae1 100644 --- a/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java +++ b/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java @@ -35,6 +35,7 @@ void shouldTriggerBatchDeleteCSV() { verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.REPORT), anyLong()); verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.ERROR), anyLong()); verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.METRICS_DATA_COMPLETED), anyLong()); + verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.REQUEST_INFO), anyLong()); } From 81dd00b1e21e41e0f5043c2704a7f0c35e036405 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 16:40:45 +0800 Subject: [PATCH 14/20] ADM-975 [backend][frontend]: fix the backend bug and fix e2e test --- .../heartbeat/repository/FilePrefixType.java | 2 +- .../java/heartbeat/repository/FileType.java | 3 +- .../service/report/ReportService.java | 16 ++-- .../scheduler/DeleteExpireCSVScheduler.java | 2 +- .../report/DeleteExpireCSVSchedulerTest.java | 2 +- .../service/report/ReportServiceTest.java | 85 +++++++++++++++--- .../create-new/metric-20240607-20240607.csv | 3 + .../e2e/fixtures/import-file/chart-result.ts | 72 +++++++++++---- frontend/e2e/pages/metrics/report-step.ts | 90 +++++++++++++++++-- .../import-project-from-file.spec.ts | 6 +- 10 files changed, 231 insertions(+), 50 deletions(-) diff --git a/backend/src/main/java/heartbeat/repository/FilePrefixType.java b/backend/src/main/java/heartbeat/repository/FilePrefixType.java index 2748f9eea9..bafd9d83ff 100644 --- a/backend/src/main/java/heartbeat/repository/FilePrefixType.java +++ b/backend/src/main/java/heartbeat/repository/FilePrefixType.java @@ -6,7 +6,7 @@ public enum FilePrefixType { BOARD_REPORT_PREFIX("board-"), METRIC_REPORT_PREFIX("metric-"), PIPELINE_REPORT_PREFIX("pipeline-"), - SOURCE_CONTROL_PREFIX("sourceControl-"), REQUEST_INFO_REPORT_PREFIX("requestInfo-"), + SOURCE_CONTROL_PREFIX("sourceControl-"), USER_CONFIG_REPORT_PREFIX("userConfig-"), DATA_COMPLETED_PREFIX("dataCompleted-"); private final String prefix; diff --git a/backend/src/main/java/heartbeat/repository/FileType.java b/backend/src/main/java/heartbeat/repository/FileType.java index e956e089cf..1406ddceb7 100644 --- a/backend/src/main/java/heartbeat/repository/FileType.java +++ b/backend/src/main/java/heartbeat/repository/FileType.java @@ -8,8 +8,7 @@ public enum FileType { ERROR("error", "error/"), REPORT("report", "report/"), CSV("csv", "csv/"), - METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), - REQUEST_INFO("request-info", "request-info/"); + METRICS_DATA_COMPLETED("metrics-data-completed", "metrics-data-completed/"), CONFIGS("config", "config/"); private final String type; diff --git a/backend/src/main/java/heartbeat/service/report/ReportService.java b/backend/src/main/java/heartbeat/service/report/ReportService.java index 9901479717..64434218e0 100644 --- a/backend/src/main/java/heartbeat/service/report/ReportService.java +++ b/backend/src/main/java/heartbeat/service/report/ReportService.java @@ -1,5 +1,6 @@ package heartbeat.service.report; +import heartbeat.controller.report.dto.request.BuildKiteSetting; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.MetricType; import heartbeat.controller.report.dto.request.ReportType; @@ -28,6 +29,8 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import static java.util.Optional.ofNullable; + @Service @RequiredArgsConstructor public class ReportService { @@ -107,12 +110,12 @@ public ShareApiDetailsResponse getShareReportInfo(String uuid) { throw new NotFoundException( String.format("Don't get the data, please check the uuid: %s, maybe it's expired or error", uuid)); } - List savedRequestInfoList = fileRepository.getFiles(FileType.REQUEST_INFO, uuid) + List savedRequestInfoList = fileRepository.getFiles(FileType.CONFIGS, uuid) .stream() .map(it -> it.split(FILENAME_SEPARATOR)) .map(it -> it[1] + FILENAME_SEPARATOR + it[2] + FILENAME_SEPARATOR + it[3]) - .map(it -> fileRepository.readFileByType(FileType.REQUEST_INFO, uuid, it, SavedRequestInfo.class, - FilePrefixType.REQUEST_INFO_REPORT_PREFIX)) + .map(it -> fileRepository.readFileByType(FileType.CONFIGS, uuid, it, SavedRequestInfo.class, + FilePrefixType.USER_CONFIG_REPORT_PREFIX)) .toList(); List metrics = savedRequestInfoList.stream() .map(SavedRequestInfo::getMetrics) @@ -139,10 +142,11 @@ public UuidResponse generateReportId() { public void saveRequestInfo(GenerateReportRequest request, String uuid) { SavedRequestInfo savedRequestInfo = SavedRequestInfo.builder() .metrics(request.getMetrics()) - .pipelines(request.getBuildKiteSetting().getDeploymentEnvList()) + .pipelines(ofNullable(request.getBuildKiteSetting()).map(BuildKiteSetting::getDeploymentEnvList) + .orElse(List.of())) .build(); - fileRepository.createFileByType(FileType.REQUEST_INFO, uuid, request.getTimeRangeAndTimeStamp(), - savedRequestInfo, FilePrefixType.REQUEST_INFO_REPORT_PREFIX); + fileRepository.createFileByType(FileType.CONFIGS, uuid, request.getTimeRangeAndTimeStamp(), savedRequestInfo, + FilePrefixType.USER_CONFIG_REPORT_PREFIX); } } diff --git a/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java b/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java index 5a05069870..a50ee2d439 100644 --- a/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java +++ b/backend/src/main/java/heartbeat/service/report/scheduler/DeleteExpireCSVScheduler.java @@ -27,7 +27,7 @@ public void triggerBatchDelete() { fileRepository.removeExpiredFiles(FileType.REPORT, currentTimeStamp); fileRepository.removeExpiredFiles(FileType.ERROR, currentTimeStamp); fileRepository.removeExpiredFiles(FileType.METRICS_DATA_COMPLETED, currentTimeStamp); - fileRepository.removeExpiredFiles(FileType.REQUEST_INFO, currentTimeStamp); + fileRepository.removeExpiredFiles(FileType.CONFIGS, currentTimeStamp); } } diff --git a/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java b/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java index 7e7aa03ae1..317591a196 100644 --- a/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java +++ b/backend/src/test/java/heartbeat/service/report/DeleteExpireCSVSchedulerTest.java @@ -35,7 +35,7 @@ void shouldTriggerBatchDeleteCSV() { verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.REPORT), anyLong()); verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.ERROR), anyLong()); verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.METRICS_DATA_COMPLETED), anyLong()); - verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.REQUEST_INFO), anyLong()); + verify(fileRepository, times(1)).removeExpiredFiles(eq(FileType.CONFIGS), anyLong()); } diff --git a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java index 4413f7eb00..dd7c225f90 100644 --- a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java @@ -41,7 +41,7 @@ import static heartbeat.controller.report.dto.request.MetricType.BOARD; import static heartbeat.controller.report.dto.request.MetricType.DORA; -import static heartbeat.repository.FilePrefixType.REQUEST_INFO_REPORT_PREFIX; +import static heartbeat.repository.FilePrefixType.USER_CONFIG_REPORT_PREFIX; import static heartbeat.tools.TimeUtils.mockTimeStamp; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -381,16 +381,16 @@ class GetReportUrl { @Test void shouldGetReportUrlsSuccessfully() { when(fileRepository.getFiles(FileType.REPORT, TEST_UUID)).thenReturn(List.of("board-1-2-3", "board-2-3-4")); - when(fileRepository.getFiles(FileType.REQUEST_INFO, TEST_UUID)) + when(fileRepository.getFiles(FileType.CONFIGS, TEST_UUID)) .thenReturn(List.of("board-0-0-0", "board-9-9-9")); - when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any())) + when(fileRepository.readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("0-0-0"), any(), any())) .thenReturn(SavedRequestInfo.builder().metrics(List.of("test-metrics1", "test-metrics2")).pipelines(List.of( DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), DeploymentEnvironment.builder().id("1").name("pipeline1").step("step2").build(), DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build() )).build()); - when(fileRepository.readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("9-9-9"), any(), any())) + when(fileRepository.readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("9-9-9"), any(), any())) .thenReturn(SavedRequestInfo.builder().metrics(List.of("test-metrics1", "test-metrics3")).pipelines(List.of( DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build(), @@ -418,10 +418,10 @@ void shouldGetReportUrlsSuccessfully() { assertEquals("pipeline2/step1", pipelines.get(2)); assertEquals("pipeline2/step2", pipelines.get(3)); - verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); - verify(fileRepository).getFiles(FileType.REQUEST_INFO, TEST_UUID); - verify(fileRepository).readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("0-0-0"), any(), any()); - verify(fileRepository).readFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), eq("9-9-9"), any(), any()); + verify(fileRepository).getFiles(FileType.CONFIGS, TEST_UUID); + verify(fileRepository).getFiles(FileType.CONFIGS, TEST_UUID); + verify(fileRepository).readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("0-0-0"), any(), any()); + verify(fileRepository).readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("9-9-9"), any(), any()); } @@ -487,8 +487,8 @@ void shouldSaveMetricsSuccessfully() { reportService.saveRequestInfo(request, TEST_UUID); - verify(fileRepository).createFileByType(eq(FileType.REQUEST_INFO), eq(TEST_UUID), - eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(REQUEST_INFO_REPORT_PREFIX)); + verify(fileRepository).createFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), + eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(USER_CONFIG_REPORT_PREFIX)); SavedRequestInfo savedRequestInfo = argumentCaptor.getValue(); @@ -504,6 +504,71 @@ void shouldSaveMetricsSuccessfully() { assertEquals("test-metrics2", metrics.get(1)); } + @Test + void shouldSaveMetricsSuccessfullyWhenBuildKiteSettingIsNull() { + String timeStamp = String.valueOf(mockTimeStamp(2023, 5, 10, 0, 0, 0)); + String startTimeStamp = String.valueOf(mockTimeStamp(2024, 3, 10, 0, 0, 0)); + String endTimeStamp = String.valueOf(mockTimeStamp(2024, 4, 9, 0, 0, 0)); + + GenerateReportRequest request = GenerateReportRequest.builder() + .csvTimeStamp(timeStamp) + .startTime(startTimeStamp) + .endTime(endTimeStamp) + .metrics(List.of("test-metrics1", "test-metrics2")) + .timezone("Asia/Shanghai") + .build(); + + reportService.saveRequestInfo(request, TEST_UUID); + + verify(fileRepository).createFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), + eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(USER_CONFIG_REPORT_PREFIX)); + + SavedRequestInfo savedRequestInfo = argumentCaptor.getValue(); + + List pipelines = savedRequestInfo.getPipelines(); + + assertEquals(0, pipelines.size()); + + List metrics = savedRequestInfo.getMetrics(); + + assertEquals(2, metrics.size()); + assertEquals("test-metrics1", metrics.get(0)); + assertEquals("test-metrics2", metrics.get(1)); + } + + @Test + void shouldSaveMetricsSuccessfullyWhenDeploymentEnvListIsNull() { + String timeStamp = String.valueOf(mockTimeStamp(2023, 5, 10, 0, 0, 0)); + String startTimeStamp = String.valueOf(mockTimeStamp(2024, 3, 10, 0, 0, 0)); + String endTimeStamp = String.valueOf(mockTimeStamp(2024, 4, 9, 0, 0, 0)); + + GenerateReportRequest request = GenerateReportRequest.builder() + .csvTimeStamp(timeStamp) + .startTime(startTimeStamp) + .endTime(endTimeStamp) + .metrics(List.of("test-metrics1", "test-metrics2")) + .buildKiteSetting(BuildKiteSetting.builder().build()) + .timezone("Asia/Shanghai") + .build(); + + reportService.saveRequestInfo(request, TEST_UUID); + + verify(fileRepository).createFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), + eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(USER_CONFIG_REPORT_PREFIX)); + + SavedRequestInfo savedRequestInfo = argumentCaptor.getValue(); + + List pipelines = savedRequestInfo.getPipelines(); + + assertEquals(0, pipelines.size()); + + List metrics = savedRequestInfo.getMetrics(); + + assertEquals(2, metrics.size()); + assertEquals("test-metrics1", metrics.get(0)); + assertEquals("test-metrics2", metrics.get(1)); + } + } } diff --git a/frontend/e2e/fixtures/create-new/metric-20240607-20240607.csv b/frontend/e2e/fixtures/create-new/metric-20240607-20240607.csv index 74bd5f8936..a3663f4083 100644 --- a/frontend/e2e/fixtures/create-new/metric-20240607-20240607.csv +++ b/frontend/e2e/fixtures/create-new/metric-20240607-20240607.csv @@ -38,5 +38,8 @@ "Rework","Rework cards ratio(Total rework cards/Throughput)","0.0000" "Deployment frequency","Heartbeat / Deploy prod / Deployment frequency(Deployments/Day)","0" "Deployment frequency","Heartbeat / Deploy prod / Deployment frequency(Deployment times)","0" +"Lead time for changes","Heartbeat / Deploy prod / PR Lead Time","0" +"Lead time for changes","Heartbeat / Deploy prod / Pipeline Lead Time","0" +"Lead time for changes","Heartbeat / Deploy prod / Total Lead Time","0" "Dev change failure rate","Heartbeat / Deploy prod / Dev change failure rate","0.0000" "Dev mean time to recovery","Heartbeat / Deploy prod / Dev mean time to recovery","0" diff --git a/frontend/e2e/fixtures/import-file/chart-result.ts b/frontend/e2e/fixtures/import-file/chart-result.ts index 893ce928a7..d56c5a399a 100644 --- a/frontend/e2e/fixtures/import-file/chart-result.ts +++ b/frontend/e2e/fixtures/import-file/chart-result.ts @@ -1,3 +1,13 @@ +type DoraChartType = { + [key: string]: { + [key: string]: { + type: string; + color: string; + value: string; + }; + }; +}; + export const BOARD_CHART_VALUE = { Velocity: { type: 'trend up', @@ -21,25 +31,49 @@ export const BOARD_CHART_VALUE = { }, }; -export const DORA_CHART_VALUE = { - 'Lead Time For Changes': { - type: 'trend down', - color: '#02C4A8', - value: '80.49%', - }, - 'Deployment Frequency': { - type: 'trend down', - color: '#E82107', - value: '75.00%', - }, - 'Dev Change Failure Rate': { - type: 'trend down', - color: '#02C4A8', - value: '59.99%', +export const DORA_CHART_VALUE: DoraChartType = { + 'Average/Total': { + 'Lead Time For Changes': { + type: 'trend down', + color: '#02C4A8', + value: '80.49%', + }, + 'Deployment Frequency': { + type: 'trend down', + color: '#E82107', + value: '75.00%', + }, + 'Dev Change Failure Rate': { + type: 'trend down', + color: '#02C4A8', + value: '59.99%', + }, + 'Dev Mean Time To Recovery': { + type: 'trend down', + color: '#02C4A8', + value: '22.19%', + }, }, - 'Dev Mean Time To Recovery': { - type: 'trend down', - color: '#02C4A8', - value: '22.19%', + 'Heartbeat/ Deploy prod': { + 'Lead Time For Changes': { + type: 'trend down', + color: '#02C4A8', + value: '80.49%', + }, + 'Deployment Frequency': { + type: 'trend down', + color: '#E82107', + value: '75.00%', + }, + 'Dev Change Failure Rate': { + type: 'trend down', + color: '#02C4A8', + value: '59.99%', + }, + 'Dev Mean Time To Recovery': { + type: 'trend down', + color: '#02C4A8', + value: '22.19%', + }, }, }; diff --git a/frontend/e2e/pages/metrics/report-step.ts b/frontend/e2e/pages/metrics/report-step.ts index 8dc1d852e4..0e0de08c98 100644 --- a/frontend/e2e/pages/metrics/report-step.ts +++ b/frontend/e2e/pages/metrics/report-step.ts @@ -97,6 +97,7 @@ export class ReportStep { readonly reworkTrendIcon: Locator; readonly cycleTimeAllocationTrendIcon: Locator; readonly cycleTimeTrendIcon: Locator; + readonly doraPipelineSelector: Locator; readonly devMeanTimeToRecoveryTrendContainer: Locator; readonly devChangeFailureRateTrendContainer: Locator; readonly deploymentFrequencyTrendContainer: Locator; @@ -185,6 +186,7 @@ export class ReportStep { this.cycleTimeAllocationTrendIcon = this.cycleTimeAllocationTrendContainer.getByLabel('trend down'); this.reworkTrendIcon = this.reworkTrendContainer.getByLabel('trend down'); + this.doraPipelineSelector = this.page.getByLabel('Pipeline Selector'); this.leadTimeForChangesTrendContainer = this.page.getByLabel('lead time for changes trend container'); this.deploymentFrequencyTrendContainer = this.page.getByLabel('deployment frequency trend container'); this.devChangeFailureRateTrendContainer = this.page.getByLabel('dev change failure rate trend container'); @@ -789,6 +791,7 @@ export class ReportStep { } async checkChartDoraTabStatus({ + pipeline, showLeadTimeForChangeChart, showDeploymentFrequencyChart, showDevChangeFailureRateTrendContainer, @@ -796,6 +799,7 @@ export class ReportStep { showDevMeanTimeToRecoveryChart, showDevMeanTimeToRecoveryTrendContainer, }: { + pipeline: string; showLeadTimeForChangeChart: boolean; showDeploymentFrequencyChart: boolean; showDevChangeFailureRateTrendContainer: boolean; @@ -803,6 +807,7 @@ export class ReportStep { showDevMeanTimeToRecoveryTrendContainer: boolean; showDevMeanTimeToRecoveryChart: boolean; }) { + const pipelineDoraChartValue = DORA_CHART_VALUE[pipeline]; expect(await this.displayBoardChartTab.getAttribute('aria-selected')).toEqual('false'); expect(await this.displayDoraChartTab.getAttribute('aria-selected')).toEqual('true'); await this.displayListTab.click(); @@ -820,11 +825,11 @@ export class ReportStep { await expect(this.leadTimeForChangesTrendContainer).toBeVisible(); await expect(this.leadTimeForChangesTrendContainer).toHaveAttribute( 'color', - DORA_CHART_VALUE['Lead Time For Changes'].color, + pipelineDoraChartValue['Lead Time For Changes'].color, ); await expect(this.leadTimeForChangesTrendIcon).toBeVisible(); await expect(this.leadTimeForChangesTrendContainer).toContainText( - DORA_CHART_VALUE['Lead Time For Changes'].value, + pipelineDoraChartValue['Lead Time For Changes'].value, ); } else { await expect(this.leadTimeForChangeChart).not.toBeVisible(); @@ -837,11 +842,11 @@ export class ReportStep { await expect(this.deploymentFrequencyTrendContainer).toBeVisible(); await expect(this.deploymentFrequencyTrendContainer).toHaveAttribute( 'color', - DORA_CHART_VALUE['Deployment Frequency'].color, + pipelineDoraChartValue['Deployment Frequency'].color, ); await expect(this.deploymentFrequencyTrendIcon).toBeVisible(); await expect(this.deploymentFrequencyTrendContainer).toContainText( - DORA_CHART_VALUE['Deployment Frequency'].value, + pipelineDoraChartValue['Deployment Frequency'].value, ); } else { await expect(this.deploymentFrequencyChart).not.toBeVisible(); @@ -855,11 +860,11 @@ export class ReportStep { await expect(this.devChangeFailureRateTrendContainer).toBeVisible(); await expect(this.devChangeFailureRateTrendContainer).toHaveAttribute( 'color', - DORA_CHART_VALUE['Dev Change Failure Rate'].color, + pipelineDoraChartValue['Dev Change Failure Rate'].color, ); await expect(this.devChangeFailureRateTrendIcon).toBeVisible(); await expect(this.devChangeFailureRateTrendContainer).toContainText( - DORA_CHART_VALUE['Dev Change Failure Rate'].value, + pipelineDoraChartValue['Dev Change Failure Rate'].value, ); } else { await expect(this.devChangeFailureRateTrendContainer).not.toBeVisible(); @@ -877,11 +882,11 @@ export class ReportStep { await expect(this.devMeanTimeToRecoveryTrendContainer).toBeVisible(); await expect(this.devMeanTimeToRecoveryTrendContainer).toHaveAttribute( 'color', - DORA_CHART_VALUE['Dev Mean Time To Recovery'].color, + pipelineDoraChartValue['Dev Mean Time To Recovery'].color, ); await expect(this.devMeanTimeToRecoveryTrendIcon).toBeVisible(); await expect(this.devMeanTimeToRecoveryTrendContainer).toContainText( - DORA_CHART_VALUE['Dev Mean Time To Recovery'].value, + pipelineDoraChartValue['Dev Mean Time To Recovery'].value, ); } else { await expect(this.devMeanTimeToRecoveryTrendContainer).not.toBeVisible(); @@ -893,4 +898,73 @@ export class ReportStep { await expect(this.devMeanTimeToRecoveryTrendIcon).not.toBeVisible(); } } + + async checkPipelineSelectorAndDoraChart({ + pipelines, + showLeadTimeForChangeChart, + showDeploymentFrequencyChart, + showDevChangeFailureRateTrendContainer, + showDevChangeFailureRateChart, + showDevMeanTimeToRecoveryChart, + showDevMeanTimeToRecoveryTrendContainer, + }: { + pipelines: string[]; + showLeadTimeForChangeChart: boolean; + showDeploymentFrequencyChart: boolean; + showDevChangeFailureRateTrendContainer: boolean; + showDevChangeFailureRateChart: boolean; + showDevMeanTimeToRecoveryTrendContainer: boolean; + showDevMeanTimeToRecoveryChart: boolean; + }) { + await expect(this.doraPipelineSelector).toBeVisible(); + await this.checkPipelineSelectorOptions({ + pipelines, + showLeadTimeForChangeChart, + showDeploymentFrequencyChart, + showDevChangeFailureRateTrendContainer, + showDevChangeFailureRateChart, + showDevMeanTimeToRecoveryChart, + showDevMeanTimeToRecoveryTrendContainer, + }); + } + + async checkPipelineSelectorOptions({ + pipelines, + showLeadTimeForChangeChart, + showDeploymentFrequencyChart, + showDevChangeFailureRateTrendContainer, + showDevChangeFailureRateChart, + showDevMeanTimeToRecoveryChart, + showDevMeanTimeToRecoveryTrendContainer, + }: { + pipelines: string[]; + showLeadTimeForChangeChart: boolean; + showDeploymentFrequencyChart: boolean; + showDevChangeFailureRateTrendContainer: boolean; + showDevChangeFailureRateChart: boolean; + showDevMeanTimeToRecoveryTrendContainer: boolean; + showDevMeanTimeToRecoveryChart: boolean; + }) { + await this.doraPipelineSelector.click(); + const singleOption = this.page.getByLabel(`single-option`); + await expect(singleOption).toHaveCount(2); + + for (let i = 0; i < 1; i++) { + await expect(singleOption.nth(i)).toHaveText(pipelines[i]); + } + + for (let i = 0; i < 1; i++) { + await this.doraPipelineSelector.click(); + await singleOption.nth(i).click(); + this.checkChartDoraTabStatus({ + pipeline: pipelines[i], + showDevMeanTimeToRecoveryTrendContainer: showDevMeanTimeToRecoveryTrendContainer, + showLeadTimeForChangeChart: showLeadTimeForChangeChart, + showDeploymentFrequencyChart: showDeploymentFrequencyChart, + showDevChangeFailureRateTrendContainer: showDevChangeFailureRateTrendContainer, + showDevChangeFailureRateChart: showDevChangeFailureRateChart, + showDevMeanTimeToRecoveryChart: showDevMeanTimeToRecoveryChart, + }); + } + } } diff --git a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts index 848b357161..6ee95ba9cb 100644 --- a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts +++ b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts @@ -120,7 +120,8 @@ test('Import project from file with partial ranges API failed', async ({ showCycleTimeAllocationChart: true, }); await reportStep.goToCharDoraTab(); - await reportStep.checkChartDoraTabStatus({ + await reportStep.checkPipelineSelectorAndDoraChart({ + pipelines: ['Average/Total', 'Heartbeat/ Deploy prod'], showDevMeanTimeToRecoveryTrendContainer: false, showLeadTimeForChangeChart: true, showDeploymentFrequencyChart: true, @@ -165,7 +166,8 @@ test('Import project from file with no all metrics', async ({ homePage, configSt showCycleTimeAllocationChart: true, }); await reportStep.goToCharDoraTab(); - await reportStep.checkChartDoraTabStatus({ + await reportStep.checkPipelineSelectorAndDoraChart({ + pipelines: ['Average/Total', 'Heartbeat/ Deploy prod'], showDevMeanTimeToRecoveryTrendContainer: false, showDevChangeFailureRateChart: false, showDevMeanTimeToRecoveryChart: true, From 801ee818a9471ec185da0f4e778747867e2e0b85 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 16:50:39 +0800 Subject: [PATCH 15/20] ADM-975 [docs]: modify the spike --- docs/src/content/docs/en/spikes/tech-spikes-sharing-report.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/en/spikes/tech-spikes-sharing-report.mdx b/docs/src/content/docs/en/spikes/tech-spikes-sharing-report.mdx index febfe822d4..bab2c0e457 100644 --- a/docs/src/content/docs/en/spikes/tech-spikes-sharing-report.mdx +++ b/docs/src/content/docs/en/spikes/tech-spikes-sharing-report.mdx @@ -43,6 +43,7 @@ frontend -> backend: POST /reports when jumping into report page backend --> frontend: uuid loop Sprint list frontend -> backend: POST /reports/:uuid with the previous/same payload + backend -> backend: save config to files backend --> frontend: Callback URL end @@ -59,7 +60,7 @@ frontend --> frontend: Render frontend page alt There is 1 report at least frontend -> backend: GET /reports/:uuid backend-> backend: Loop all report under uuid folder - backend --> frontend: Response the URL list of report, e.g. [`/api/v1/reports/:uuid/detail?startTime=20240528&endTime=20240531`] + backend --> frontend: Response the share infomation, including the URL list , the metrics list and the pipelines of report, e.g. [{'reportURLs': [`/api/v1/reports/:uuid/detail?startTime=20240528&endTime=20240531`], 'metrics': ['Velocity'], 'pipelines': ['pipeline name/pipeline step']}] loop URL list frontend-> backend: GET /api/v1/reports/:uuid/detail?startTime=20240528&endTime=20240531 backend --> frontend: Report result From 6d2f73acba053e012e3d05c79c558b802f984e00 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Mon, 15 Jul 2024 17:40:03 +0800 Subject: [PATCH 16/20] ADM-975 [docs]: change the readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da11836ea2..16ac83353e 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,9 @@ In report `chart` page, heartbeat provide a better visualization on delivery and - Board chart ![Image 3-22-1](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-1.png) - Dora chart -![Image 3-22-2](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-2-new.png) +![Image 3-22-2](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-2.png) + +In the Dora chart, you can see the different dora chart by selecting the different pipeline. Within chart, Hearteat also provide trend indicator which represent last two periods comparison result. The trend indicator includes three key points: - Color: From delivery perspective, Green means healthy, Red means unhealthy @@ -361,6 +363,7 @@ Below trend indicator means that Velocity is pretty health between 2024.5.13-202 Below trend indicator means that development time ratio is not so unhealthy between 2024.5.13-2024.5.27 and 2024.4.29-20204.5.12. And the development time ratio decreased 23.17%. ![Image 3-22-2](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-4.png) + ### 3.4.1 Velocity In Velocity Report, it will list the corresponding data by Story Point and the number of story tickets. (image 3-19) From c70223003c71a833368726bbc0c7c9e23ae2fca9 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Tue, 16 Jul 2024 09:51:18 +0800 Subject: [PATCH 17/20] ADM-975 [frontend]: fix issues --- README.md | 4 ++-- .../containers/ReportStep/ReportStep.test.tsx | 20 +++++++++---------- .../e2e/fixtures/import-file/chart-result.ts | 4 +++- frontend/e2e/pages/metrics/report-step.ts | 2 +- .../import-project-from-file.spec.ts | 5 +++-- .../PipelineSelector/index.tsx | 16 ++++++++------- .../ReportStep/DoraMetricsChart/index.tsx | 2 +- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 16ac83353e..f8d64cf3a4 100644 --- a/README.md +++ b/README.md @@ -349,9 +349,9 @@ In report `chart` page, heartbeat provide a better visualization on delivery and - Board chart ![Image 3-22-1](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-1.png) - Dora chart -![Image 3-22-2](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-2.png) +![Image 3-22-2](https://cdn.jsdelivr.net/gh/au-heartbeat/data-hosting@main/readme/3-22-2-1.png) -In the Dora chart, you can see the different dora chart by selecting the different pipeline. +Heartbeat chart enable customer to drill down dora metrics to specific pipeline by selecting from pipeline/step dropdown. Within chart, Hearteat also provide trend indicator which represent last two periods comparison result. The trend indicator includes three key points: - Color: From delivery perspective, Green means healthy, Red means unhealthy diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 7e131a3a7e..d7269b1847 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -768,29 +768,27 @@ describe('Report Step', () => { const pipelineSelector = screen.getByLabelText('Pipeline Selector'); expect(pipelineSelector).toBeInTheDocument(); - await act(async () => { - await userEvent.click(screen.getByRole('button', { name: LIST_OPEN })); - }); - - let listBox = screen.getByRole('listbox'); - expect(listBox).toBeInTheDocument(); + const pipelineSelectorText = screen.getByLabelText('Pipeline Selector Text'); + expect(pipelineSelectorText).toBeInTheDocument(); - await act(async () => { - await userEvent.click(within(listBox).getByText('Average/Total')); - }); + const pipelineSelectorInput = pipelineSelectorText.getElementsByTagName('input')[0]; + expect(pipelineSelectorInput).toHaveValue('All'); await act(async () => { await userEvent.click(screen.getByRole('button', { name: LIST_OPEN })); }); - listBox = screen.getByRole('listbox'); + const listBox = screen.getByRole('listbox'); expect(listBox).toBeInTheDocument(); + expect(screen.getByText('All')).toBeInTheDocument(); + expect(screen.getByText('mock pipeline name/mock step1')).toBeInTheDocument(); + await act(async () => { const pipelineSelector = within(listBox).getByText('mock pipeline name/mock step1'); - expect(pipelineSelector).toBeInTheDocument(); await userEvent.click(pipelineSelector); }); + expect(pipelineSelectorInput).toHaveValue('mock pipeline name/mock step1'); expect(addNotification).toHaveBeenCalledWith({ message: MESSAGE.EXPIRE_INFORMATION, diff --git a/frontend/e2e/fixtures/import-file/chart-result.ts b/frontend/e2e/fixtures/import-file/chart-result.ts index d56c5a399a..c45062d57c 100644 --- a/frontend/e2e/fixtures/import-file/chart-result.ts +++ b/frontend/e2e/fixtures/import-file/chart-result.ts @@ -32,7 +32,7 @@ export const BOARD_CHART_VALUE = { }; export const DORA_CHART_VALUE: DoraChartType = { - 'Average/Total': { + All: { 'Lead Time For Changes': { type: 'trend down', color: '#02C4A8', @@ -77,3 +77,5 @@ export const DORA_CHART_VALUE: DoraChartType = { }, }, }; + +export const DORA_CHART_PIPELINES = ['All', 'Heartbeat/ Deploy prod']; diff --git a/frontend/e2e/pages/metrics/report-step.ts b/frontend/e2e/pages/metrics/report-step.ts index 0e0de08c98..f129020ffe 100644 --- a/frontend/e2e/pages/metrics/report-step.ts +++ b/frontend/e2e/pages/metrics/report-step.ts @@ -186,7 +186,7 @@ export class ReportStep { this.cycleTimeAllocationTrendIcon = this.cycleTimeAllocationTrendContainer.getByLabel('trend down'); this.reworkTrendIcon = this.reworkTrendContainer.getByLabel('trend down'); - this.doraPipelineSelector = this.page.getByLabel('Pipeline Selector'); + this.doraPipelineSelector = this.page.getByLabel('Pipeline Selector').first(); this.leadTimeForChangesTrendContainer = this.page.getByLabel('lead time for changes trend container'); this.deploymentFrequencyTrendContainer = this.page.getByLabel('deployment frequency trend container'); this.devChangeFailureRateTrendContainer = this.page.getByLabel('dev change failure rate trend container'); diff --git a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts index 6ee95ba9cb..606b861183 100644 --- a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts +++ b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts @@ -15,6 +15,7 @@ import { cycleTimeByStatusFixture } from '../../fixtures/cycle-time-by-status/cy import { importMultipleDoneProjectFromFile } from '../../fixtures/import-file/multiple-done-config-file'; import { partialTimeRangesSuccess } from '../../fixtures/import-file/partial-time-ranges-success'; import { partialMetricsShowChart } from '../../fixtures/import-file/partial-metrics-show-chart'; +import { DORA_CHART_PIPELINES } from '../../fixtures/import-file/chart-result'; import { ProjectCreationType } from 'e2e/pages/metrics/report-step'; import { test } from '../../fixtures/test-with-extend-fixtures'; import { clearTempDir } from 'e2e/utils/clear-temp-dir'; @@ -121,7 +122,7 @@ test('Import project from file with partial ranges API failed', async ({ }); await reportStep.goToCharDoraTab(); await reportStep.checkPipelineSelectorAndDoraChart({ - pipelines: ['Average/Total', 'Heartbeat/ Deploy prod'], + pipelines: DORA_CHART_PIPELINES, showDevMeanTimeToRecoveryTrendContainer: false, showLeadTimeForChangeChart: true, showDeploymentFrequencyChart: true, @@ -167,7 +168,7 @@ test('Import project from file with no all metrics', async ({ homePage, configSt }); await reportStep.goToCharDoraTab(); await reportStep.checkPipelineSelectorAndDoraChart({ - pipelines: ['Average/Total', 'Heartbeat/ Deploy prod'], + pipelines: DORA_CHART_PIPELINES, showDevMeanTimeToRecoveryTrendContainer: false, showDevChangeFailureRateChart: false, showDevMeanTimeToRecoveryChart: true, diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx index b6e82fc37b..173c04207c 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx @@ -2,18 +2,18 @@ import { PipelinesSelectContainer } from '@src/containers/ReportStep/DoraMetrics import { Autocomplete, Box, ListItemText, TextField, Tooltip } from '@mui/material'; import { getEmojiUrls, removeExtraEmojiName } from '@src/constants/emojis/emoji'; import { EmojiWrap, StyledAvatar } from '@src/constants/emojis/style'; -import { Z_INDEX } from '@src/constants/commons'; +import { EMPTY_STRING, Z_INDEX } from '@src/constants/commons'; import React, { useState } from 'react'; interface Props { - options: string[]; - value: string; - onUpDatePipeline: (value: string) => void; - title: string; + readonly options: string[]; + readonly value: string; + readonly onUpDatePipeline: (value: string) => void; + readonly title: string; } export default function PipelineSelector({ options, value, onUpDatePipeline, title }: Props) { - const label = ''; + const label = EMPTY_STRING; const [inputValue, setInputValue] = useState(value); const emojiView = (pipelineStepName: string) => { @@ -62,7 +62,9 @@ export default function PipelineSelector({ options, value, onUpDatePipeline, tit onChange={(event, newValue: string) => onUpDatePipeline(newValue)} inputValue={inputValue} onInputChange={(event, newInputValue) => setInputValue(newInputValue)} - renderInput={(params) => } + renderInput={(params) => ( + + )} slotProps={{ popper: { sx: { diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx index 4a8119b653..db29209485 100644 --- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx @@ -48,7 +48,7 @@ enum DORAMetricsChartType { const AVERAGE = 'Average'; const TOTAL = 'Total'; -export const DEFAULT_SELECTED_PIPELINE = 'Average/Total'; +export const DEFAULT_SELECTED_PIPELINE = 'All'; function extractedStackedBarData( allDateRanges: string[], From e4b6e3f884d7aa530e27bf939d4076caaf4895fb Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Tue, 16 Jul 2024 15:51:10 +0800 Subject: [PATCH 18/20] ADM-975 [frontend]: change the style of charts --- .../containers/ReportStep/ReportStep.test.tsx | 23 ++++++++++++++++++- frontend/src/containers/MetricsStep/style.tsx | 7 +++--- .../ReportStep/ChartAndTitleWrapper/style.tsx | 5 ++++ frontend/src/containers/ReportStep/index.tsx | 9 ++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index d7269b1847..cd9bb85706 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -32,11 +32,11 @@ import { addADeploymentFrequencySetting, updateDeploymentFrequencySettings } fro import { act, render, renderHook, screen, waitFor, within } from '@testing-library/react'; import { DATA_LOADING_FAILED, DEFAULT_MESSAGE, MESSAGE } from '@src/constants/resources'; import { closeNotification } from '@src/context/notification/NotificationSlice'; +import ReportStep, { resizeChart, showChart } from '@src/containers/ReportStep'; import { addNotification } from '@src/context/notification/NotificationSlice'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { backStep, updateReportId } from '@src/context/stepper/StepperSlice'; import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; -import ReportStep, { showChart } from '@src/containers/ReportStep'; import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import React, { ReactNode } from 'react'; @@ -696,6 +696,27 @@ describe('Report Step', () => { }); }); + describe('resizeChart', () => { + beforeEach(() => { + jest.spyOn(echarts, 'init').mockImplementation( + () => + ({ + setOption: jest.fn(), + resize: jest.fn(), + dispatchAction: jest.fn(), + dispose: jest.fn(), + }) as unknown as echarts.ECharts, + ); + }); + it('should resize', () => { + const chart = echarts.init(); + const resizeFunction = resizeChart(chart); + + resizeFunction(); + expect(chart.resize).toHaveBeenCalledTimes(1); + }); + }); + describe('showChart test', () => { const chart = { setOption: jest.fn(), diff --git a/frontend/src/containers/MetricsStep/style.tsx b/frontend/src/containers/MetricsStep/style.tsx index 31cec8df9c..b0d068caa8 100644 --- a/frontend/src/containers/MetricsStep/style.tsx +++ b/frontend/src/containers/MetricsStep/style.tsx @@ -74,9 +74,10 @@ export const ChartWrapper = styled('div')({ }); export const ChartContainer = styled('div')({ - display: 'grid', - gridTemplateColumns: 'repeat(2, 1fr)', - gap: '1.25rem', + display: 'flex', + width: '100%', + flexWrap: 'wrap', + justifyContent: 'space-around', marginTop: '1.25rem', [theme.breakpoints.down('lg')]: { gridTemplateColumns: '1fr', diff --git a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx index 25e6f65c09..33e84fd38f 100644 --- a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx +++ b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx @@ -3,11 +3,16 @@ import { theme } from '@src/theme'; export const StyledChartAndTitleWrapper = styled('div')({ position: 'relative', + flex: '0 0 calc(50% - 1.25rem)', height: '25rem', + marginBottom: '1.25rem', borderRadius: '0.75rem', border: theme.main.cardBorder, background: theme.main.color, boxShadow: theme.main.cardShadow, + [theme.breakpoints.down('lg')]: { + flex: '0 0 100%', + }, }); export const ChartTitle = styled('div')({ diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 670f534b77..55f3a6ec84 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -25,6 +25,7 @@ import ReportContent from './ReportContent'; import { useAppSelector } from '@src/hooks'; import { useEffect, useState } from 'react'; import * as echarts from 'echarts'; +import { ECharts } from 'echarts'; export interface ReportStepProps { handleSave: () => void; @@ -36,10 +37,18 @@ export interface DateRangeRequestResult { reportData: ReportResponseDTO | undefined; } +export const resizeChart = (chart: ECharts) => { + return () => { + chart.resize(); + }; +}; + export function showChart(div: HTMLDivElement | null, isFinished: boolean, options: echarts.EChartsCoreOption) { if (div) { const chart = echarts.init(div); chart.setOption(options); + const resize = resizeChart(chart); + window.addEventListener('resize', resize); return () => { chart.dispose(); }; From 1bf98943fe6867b2cdecae130adabf1cfca5341e Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Tue, 16 Jul 2024 17:01:11 +0800 Subject: [PATCH 19/20] ADM-975 [frontend]: fix comments --- .../containers/ReportStep/ReportStep.test.tsx | 39 ++++++++----------- frontend/src/containers/ReportStep/index.tsx | 12 ++---- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index cd9bb85706..30df88dd64 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -32,11 +32,11 @@ import { addADeploymentFrequencySetting, updateDeploymentFrequencySettings } fro import { act, render, renderHook, screen, waitFor, within } from '@testing-library/react'; import { DATA_LOADING_FAILED, DEFAULT_MESSAGE, MESSAGE } from '@src/constants/resources'; import { closeNotification } from '@src/context/notification/NotificationSlice'; -import ReportStep, { resizeChart, showChart } from '@src/containers/ReportStep'; import { addNotification } from '@src/context/notification/NotificationSlice'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { backStep, updateReportId } from '@src/context/stepper/StepperSlice'; import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; +import ReportStep, { showChart } from '@src/containers/ReportStep'; import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import React, { ReactNode } from 'react'; @@ -696,27 +696,6 @@ describe('Report Step', () => { }); }); - describe('resizeChart', () => { - beforeEach(() => { - jest.spyOn(echarts, 'init').mockImplementation( - () => - ({ - setOption: jest.fn(), - resize: jest.fn(), - dispatchAction: jest.fn(), - dispose: jest.fn(), - }) as unknown as echarts.ECharts, - ); - }); - it('should resize', () => { - const chart = echarts.init(); - const resizeFunction = resizeChart(chart); - - resizeFunction(); - expect(chart.resize).toHaveBeenCalledTimes(1); - }); - }); - describe('showChart test', () => { const chart = { setOption: jest.fn(), @@ -740,13 +719,29 @@ describe('Report Step', () => { it('should return function when div is not null', async () => { const div = document.createElement('div'); + const disposeFunction = showChart(div, false, {}); + window.dispatchEvent(new Event('resize')); + disposeFunction && disposeFunction(); + + expect(disposeFunction).not.toBeUndefined(); + expect(echarts.init).toHaveBeenCalledTimes(1); + expect(chart.setOption).toHaveBeenCalledTimes(1); + expect(chart.dispose).toHaveBeenCalledTimes(1); + expect(chart.resize).toHaveBeenCalledTimes(1); + }); + + it('should not resize when dispatch resize event after dispose', async () => { + const div = document.createElement('div'); + const disposeFunction = showChart(div, false, {}); disposeFunction && disposeFunction(); + window.dispatchEvent(new Event('resize')); expect(disposeFunction).not.toBeUndefined(); expect(echarts.init).toHaveBeenCalledTimes(1); expect(chart.setOption).toHaveBeenCalledTimes(1); expect(chart.dispose).toHaveBeenCalledTimes(1); + expect(chart.resize).toHaveBeenCalledTimes(0); }); it('should return hide loading when finished', async () => { diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 55f3a6ec84..a001d1f60c 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -25,7 +25,6 @@ import ReportContent from './ReportContent'; import { useAppSelector } from '@src/hooks'; import { useEffect, useState } from 'react'; import * as echarts from 'echarts'; -import { ECharts } from 'echarts'; export interface ReportStepProps { handleSave: () => void; @@ -37,20 +36,17 @@ export interface DateRangeRequestResult { reportData: ReportResponseDTO | undefined; } -export const resizeChart = (chart: ECharts) => { - return () => { - chart.resize(); - }; -}; - export function showChart(div: HTMLDivElement | null, isFinished: boolean, options: echarts.EChartsCoreOption) { if (div) { const chart = echarts.init(div); chart.setOption(options); - const resize = resizeChart(chart); + const resize = () => { + chart.resize(); + }; window.addEventListener('resize', resize); return () => { chart.dispose(); + window.removeEventListener('resize', resize); }; } } From 5607839b37347c362748e1c991a201dbcf32d6b2 Mon Sep 17 00:00:00 2001 From: zhou-yinyuan Date: Tue, 16 Jul 2024 17:16:21 +0800 Subject: [PATCH 20/20] ADM-975 [frontend]: fix comments --- .../__tests__/containers/ReportStep/ReportStep.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 30df88dd64..4646e4af07 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -721,7 +721,7 @@ describe('Report Step', () => { const disposeFunction = showChart(div, false, {}); window.dispatchEvent(new Event('resize')); - disposeFunction && disposeFunction(); + disposeFunction!(); expect(disposeFunction).not.toBeUndefined(); expect(echarts.init).toHaveBeenCalledTimes(1); @@ -734,7 +734,7 @@ describe('Report Step', () => { const div = document.createElement('div'); const disposeFunction = showChart(div, false, {}); - disposeFunction && disposeFunction(); + disposeFunction!(); window.dispatchEvent(new Event('resize')); expect(disposeFunction).not.toBeUndefined(); @@ -748,7 +748,7 @@ describe('Report Step', () => { const div = document.createElement('div'); const disposeFunction = showChart(div, true, {}); - disposeFunction && disposeFunction(); + disposeFunction!(); expect(disposeFunction).not.toBeUndefined(); expect(echarts.init).toHaveBeenCalledTimes(1);