Commit 14d1248a authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Scott Hampton

Add MTTM to MR analytics

Introduce a single stat container which will
house all of the planned stats for the MR
analytics feature.

Add the MTTM stat in the first iteration.
parent a6080c29
......@@ -6,7 +6,8 @@ import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleto
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import throughputChartQueryBuilder from '../graphql/throughput_chart_query_builder';
import { THROUGHPUT_CHART_STRINGS } from '../constants';
import { formatThroughputChartData } from '../utils';
import { formatThroughputChartData, computeMttmData } from '../utils';
import ThroughputStats from './throughput_stats.vue';
export default {
name: 'ThroughputChart',
......@@ -14,6 +15,7 @@ export default {
GlAreaChart,
GlAlert,
ChartSkeletonLoader,
ThroughputStats,
},
inject: ['fullPath'],
props: {
......@@ -88,7 +90,7 @@ export default {
formattedThroughputChartData() {
return formatThroughputChartData(this.throughputChartData);
},
chartDataLoading() {
isLoading() {
return !this.hasError && this.$apollo.queries.throughputChartData.loading;
},
chartDataAvailable() {
......@@ -102,6 +104,9 @@ export default {
: THROUGHPUT_CHART_STRINGS.NO_DATA,
};
},
singleStatsValues() {
return [computeMttmData(this.throughputChartData)];
},
},
strings: {
chartTitle: THROUGHPUT_CHART_STRINGS.CHART_TITLE,
......@@ -111,11 +116,12 @@ export default {
</script>
<template>
<div>
<throughput-stats :stats="singleStatsValues" :is-loading="isLoading" />
<h4 data-testid="chartTitle">{{ $options.strings.chartTitle }}</h4>
<div class="gl-text-gray-500" data-testid="chartDescription">
{{ $options.strings.chartDescription }}
</div>
<chart-skeleton-loader v-if="chartDataLoading" />
<chart-skeleton-loader v-if="isLoading" />
<gl-area-chart
v-else-if="chartDataAvailable"
:data="formattedThroughputChartData"
......
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { STAT_LOADER_HEIGHT } from '../constants';
export default {
name: 'ThroughputStats',
components: {
GlSingleStat,
GlSkeletonLoader,
},
props: {
stats: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
loaderHeight: STAT_LOADER_HEIGHT,
};
</script>
<template>
<div class="gl-my-7 gl-display-flex">
<div v-for="stat in stats" :key="stat.title">
<gl-skeleton-loader v-if="isLoading" :height="$options.loaderHeight" />
<gl-single-stat v-else :value="stat.value" :title="stat.title" :unit="stat.unit" />
</div>
</div>
</template>
import { __ } from '~/locale';
export const DEFAULT_NUMBER_OF_DAYS = 365;
export const STAT_LOADER_HEIGHT = 46;
export const PER_PAGE = 20;
export const ASSIGNEES_VISIBLE = 2;
export const AVATAR_SIZE = 24;
......@@ -14,6 +15,7 @@ export const THROUGHPUT_CHART_STRINGS = {
ERROR_FETCHING_DATA: __(
'There was an error while fetching the chart data. Please refresh the page to try again.',
),
MTTM: __('Mean time to merge'),
};
export const THROUGHPUT_TABLE_STRINGS = {
......@@ -51,3 +53,7 @@ export const PIPELINE_STATUS_ICON_CLASSES = {
status_pending: 'gl-text-orange-500',
default: 'gl-text-grey-500',
};
export const UNITS = {
DAYS: __('days'),
};
......@@ -32,7 +32,7 @@ export default (startDate = null, endDate = null) => {
milestoneTitle: $milestoneTitle,
sourceBranches: $sourceBranches,
targetBranches: $targetBranches
) { count }
) { count, totalTimeToMerge }
`;
});
......
......@@ -4,9 +4,10 @@ import {
dateFromParams,
getDateInPast,
getDayDifference,
secondsToDays,
} from '~/lib/utils/datetime_utility';
import { dateFormats } from '../shared/constants';
import { THROUGHPUT_CHART_STRINGS, DEFAULT_NUMBER_OF_DAYS } from './constants';
import { THROUGHPUT_CHART_STRINGS, DEFAULT_NUMBER_OF_DAYS, UNITS } from './constants';
/**
* A utility function which accepts a date range and returns
......@@ -73,6 +74,42 @@ export const formatThroughputChartData = (chartData) => {
];
};
/**
* A utility function which accepts the raw throughput data
* and computes the mean time to merge.
*
* @param {Object} rawData the raw throughput data
*
* @return {Object} the computed MTTM data
*/
export const computeMttmData = (rawData) => {
if (!rawData) return {};
const mttmData = Object.values(rawData)
// eslint-disable-next-line @gitlab/require-i18n-strings
.filter((value) => value !== 'Project')
.reduce(
(total, monthData) => {
return {
count: total.count + monthData.count,
totalTimeToMerge: total.totalTimeToMerge + monthData.totalTimeToMerge,
};
},
{
count: 0,
totalTimeToMerge: 0,
},
);
// GlSingleStat expects a String for the 'value' prop
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1152
return {
title: THROUGHPUT_CHART_STRINGS.MTTM,
value: `${secondsToDays(mttmData.totalTimeToMerge / mttmData.count)}`,
unit: UNITS.DAYS,
};
};
/**
* A utility function which accepts start and end date params
* and validates that the date range does not exceed the bounds
......
---
title: Add MTTM stat to MR Analytics
merge_request: 52316
author:
type: added
......@@ -3,6 +3,7 @@ import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ThroughputChart from 'ee/analytics/merge_request_analytics/components/throughput_chart.vue';
import ThroughputStats from 'ee/analytics/merge_request_analytics/components/throughput_stats.vue';
import { THROUGHPUT_CHART_STRINGS } from 'ee/analytics/merge_request_analytics/constants';
import store from 'ee/analytics/merge_request_analytics/store';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
......@@ -67,6 +68,10 @@ describe('ThroughputChart', () => {
wrapper = createComponent();
});
it('displays the throughput stats component', () => {
expect(wrapper.find(ThroughputStats).exists()).toBe(true);
});
it('displays the chart title', () => {
const chartTitle = wrapper.find('[data-testid="chartTitle"').text();
......
import { shallowMount } from '@vue/test-utils';
import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import ThroughputStats from 'ee/analytics/merge_request_analytics/components/throughput_stats';
import { stats } from '../mock_data';
describe('ThroughputStats', () => {
let wrapper;
const createWrapper = (props) => {
return shallowMount(ThroughputStats, {
propsData: {
stats,
...props,
},
});
};
describe('default behaviour', () => {
beforeEach(() => {
wrapper = createWrapper();
});
it('displays a GlSingleStat component for each stat entry', () => {
expect(wrapper.findAll(GlSingleStat)).toHaveLength(stats.length);
});
it('passes the GlSingleStat the correct props', () => {
const component = wrapper.findAll(GlSingleStat).at(0);
const { title, unit, value } = stats[0];
expect(component.props('title')).toBe(title);
expect(component.props('unit')).toBe(unit);
expect(component.props('value')).toBe(value);
});
it('does not display any GlSkeletonLoader components', () => {
expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(0);
});
});
describe('loading', () => {
beforeEach(() => {
wrapper = createWrapper({ isLoading: true });
});
it('displays a GlSkeletonLoader component for each stat entry', () => {
expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(stats.length);
});
it('does not display any GlSingleStat components', () => {
expect(wrapper.findAll(GlSingleStat)).toHaveLength(0);
});
});
});
......@@ -8,9 +8,9 @@ export const fullPath = 'gitlab-org/gitlab';
// We should update our tests to use fixtures instead of hardcoded mock data.
// https://gitlab.com/gitlab-org/gitlab/-/issues/270544
export const throughputChartData = {
May_2020: { count: 2, __typename: 'MergeRequestConnection' },
Jun_2020: { count: 4, __typename: 'MergeRequestConnection' },
Jul_2020: { count: 3, __typename: 'MergeRequestConnection' },
May_2020: { count: 2, totalTimeToMerge: 1234567, __typename: 'MergeRequestConnection' },
Jun_2020: { count: 4, totalTimeToMerge: 2345678, __typename: 'MergeRequestConnection' },
Jul_2020: { count: 3, totalTimeToMerge: 3456789, __typename: 'MergeRequestConnection' },
__typename: 'Project',
};
......@@ -32,6 +32,12 @@ export const formattedThroughputChartData = [
},
];
export const formattedMttmData = {
title: 'Mean time to merge',
value: '9',
unit: 'days',
};
export const expectedMonthData = [
{
year: 2020,
......@@ -67,6 +73,7 @@ export const throughputChartQuery = `query ($fullPath: ID!, $labels: [String!],
targetBranches: $targetBranches
) {
count
totalTimeToMerge
}
Jun_2020: mergeRequests(
first: 0
......@@ -80,6 +87,7 @@ export const throughputChartQuery = `query ($fullPath: ID!, $labels: [String!],
targetBranches: $targetBranches
) {
count
totalTimeToMerge
}
Jul_2020: mergeRequests(
first: 0
......@@ -93,6 +101,7 @@ export const throughputChartQuery = `query ($fullPath: ID!, $labels: [String!],
targetBranches: $targetBranches
) {
count
totalTimeToMerge
}
}
}
......@@ -148,3 +157,16 @@ export const throughputTableData = [
},
},
];
export const stats = [
{
title: 'Mean time to merge',
unit: 'days',
value: '10',
},
{
title: 'MRs per engineer',
unit: 'MRs per engineer (per month)',
value: '23',
},
];
import * as utils from 'ee/analytics/merge_request_analytics/utils';
import { useFakeDate } from 'helpers/fake_date';
import { expectedMonthData, throughputChartData, formattedThroughputChartData } from './mock_data';
import {
expectedMonthData,
throughputChartData,
formattedThroughputChartData,
formattedMttmData,
} from './mock_data';
describe('computeMonthRangeData', () => {
const start = new Date('2020-05-17T00:00:00.000Z');
......@@ -33,6 +38,14 @@ describe('formatThroughputChartData', () => {
});
});
describe('computeMttmData', () => {
it('returns the data as expected', () => {
const mttmData = utils.computeMttmData(throughputChartData);
expect(mttmData).toStrictEqual(formattedMttmData);
});
});
describe('parseAndValidateDates', () => {
useFakeDate('2021-01-21');
......
......@@ -17556,6 +17556,9 @@ msgstr ""
msgid "May"
msgstr ""
msgid "Mean time to merge"
msgstr ""
msgid "Measured in bytes of code. Excludes generated and vendored code."
msgstr ""
......@@ -33587,6 +33590,9 @@ msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
msgid "days"
msgstr ""
msgid "default branch"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment