Commit 9b9b25ea authored by Nathan Friend's avatar Nathan Friend Committed by Nicolò Maria Mezzopera

Add deployment frequency charts to CI/CD Analytics

This commit updates the CI/CD Analytics page to include 3 new charts
that display data about the deployment frequency to the "production"
environment over three different time ranges.
parent 326cc0ca
...@@ -7,7 +7,8 @@ import { getDateInPast } from '~/lib/utils/datetime_utility'; ...@@ -7,7 +7,8 @@ import { getDateInPast } from '~/lib/utils/datetime_utility';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
import StatisticsList from './statistics_list.vue'; import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue'; import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT, CHART_DATE_FORMAT,
...@@ -52,13 +53,19 @@ export default { ...@@ -52,13 +53,19 @@ export default {
GlColumnChart, GlColumnChart,
GlSkeletonLoader, GlSkeletonLoader,
StatisticsList, StatisticsList,
PipelinesAreaChart, CiCdAnalyticsAreaChart,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
}, },
inject: { inject: {
projectPath: { projectPath: {
type: String, type: String,
default: '', default: '',
}, },
shouldRenderDeploymentFrequencyCharts: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -260,6 +267,15 @@ export default { ...@@ -260,6 +267,15 @@ export default {
lastYear: __('Pipelines for last year'), lastYear: __('Pipelines for last year'),
}; };
}, },
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
}; };
</script> </script>
<template> <template>
...@@ -292,12 +308,17 @@ export default { ...@@ -292,12 +308,17 @@ export default {
</div> </div>
<hr /> <hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart <ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts" v-for="(chart, index) in areaCharts"
:key="index" :key="index"
:chart-data="chart.data" :chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
> >
{{ chart.title }} {{ chart.title }}
</pipelines-area-chart> </ci-cd-analytics-area-chart>
<template v-if="shouldRenderDeploymentFrequencyCharts">
<hr />
<deployment-frequency-charts />
</template>
</div> </div>
</template> </template>
<script> <script>
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { __, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility'; import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue'; import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue'; import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT, INNER_CHART_HEIGHT,
...@@ -19,7 +20,15 @@ export default { ...@@ -19,7 +20,15 @@ export default {
components: { components: {
StatisticsList, StatisticsList,
GlColumnChart, GlColumnChart,
PipelinesAreaChart, CiCdAnalyticsAreaChart,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
},
inject: {
shouldRenderDeploymentFrequencyCharts: {
type: Boolean,
default: false,
},
}, },
props: { props: {
counts: { counts: {
...@@ -112,6 +121,15 @@ export default { ...@@ -112,6 +121,15 @@ export default {
lastYear: __('Pipelines for last year'), lastYear: __('Pipelines for last year'),
}; };
}, },
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
}; };
</script> </script>
<template> <template>
...@@ -140,12 +158,17 @@ export default { ...@@ -140,12 +158,17 @@ export default {
</div> </div>
<hr /> <hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4> <h4 class="my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart <ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts" v-for="(chart, index) in areaCharts"
:key="index" :key="index"
:chart-data="chart.data" :chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
> >
{{ chart.title }} {{ chart.title }}
</pipelines-area-chart> </ci-cd-analytics-area-chart>
<template v-if="shouldRenderDeploymentFrequencyCharts">
<hr />
<deployment-frequency-charts />
</template>
</div> </div>
</template> </template>
<script> <script>
import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue'; import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { CHART_CONTAINER_HEIGHT } from '../constants'; import { CHART_CONTAINER_HEIGHT } from '../constants';
export default { export default {
name: 'CiCdAnalyticsAreaChart',
components: { components: {
GlAreaChart, GlAreaChart,
ResizableChartContainer, ResizableChartContainer,
...@@ -14,14 +14,9 @@ export default { ...@@ -14,14 +14,9 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
}, areaChartOptions: {
areaChartOptions: { type: Object,
xAxis: { required: true,
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
}, },
}, },
chartContainerHeight: CHART_CONTAINER_HEIGHT, chartContainerHeight: CHART_CONTAINER_HEIGHT,
...@@ -39,7 +34,7 @@ export default { ...@@ -39,7 +34,7 @@ export default {
:height="$options.chartContainerHeight" :height="$options.chartContainerHeight"
:data="chartData" :data="chartData"
:include-legend-avg-max="false" :include-legend-avg-max="false"
:option="$options.areaChartOptions" :option="areaChartOptions"
/> />
</resizable-chart-container> </resizable-chart-container>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import ProjectPipelinesChartsLegacy from './components/app_legacy.vue'; import ProjectPipelinesChartsLegacy from './components/app_legacy.vue';
import ProjectPipelinesCharts from './components/app.vue'; import ProjectPipelinesCharts from './components/app.vue';
...@@ -35,6 +36,10 @@ const mountPipelineChartsApp = (el) => { ...@@ -35,6 +36,10 @@ const mountPipelineChartsApp = (el) => {
projectPath, projectPath,
} = el.dataset; } = el.dataset;
const shouldRenderDeploymentFrequencyCharts = parseBoolean(
el.dataset.shouldRenderDeploymentFrequencyCharts,
);
const parseAreaChartData = (labels, totals, success) => { const parseAreaChartData = (labels, totals, success) => {
let parsedData = {}; let parsedData = {};
...@@ -61,6 +66,7 @@ const mountPipelineChartsApp = (el) => { ...@@ -61,6 +66,7 @@ const mountPipelineChartsApp = (el) => {
apolloProvider, apolloProvider,
provide: { provide: {
projectPath, projectPath,
shouldRenderDeploymentFrequencyCharts,
}, },
render: (createElement) => createElement(ProjectPipelinesCharts, {}), render: (createElement) => createElement(ProjectPipelinesCharts, {}),
}); });
...@@ -72,6 +78,10 @@ const mountPipelineChartsApp = (el) => { ...@@ -72,6 +78,10 @@ const mountPipelineChartsApp = (el) => {
components: { components: {
ProjectPipelinesChartsLegacy, ProjectPipelinesChartsLegacy,
}, },
provide: {
projectPath,
shouldRenderDeploymentFrequencyCharts,
},
render: (createElement) => render: (createElement) =>
createElement(ProjectPipelinesChartsLegacy, { createElement(ProjectPipelinesChartsLegacy, {
props: { props: {
......
...@@ -22,4 +22,10 @@ module GraphHelper ...@@ -22,4 +22,10 @@ module GraphHelper
ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100 ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
ratio.to_i ratio.to_i
end end
def should_render_deployment_frequency_charts
false
end
end end
GraphHelper.prepend_if_ee('EE::GraphHelper')
- page_title _('CI / CD Analytics') - page_title _('CI / CD Analytics')
- if Feature.enabled?(:graphql_pipeline_analytics) - if Feature.enabled?(:graphql_pipeline_analytics)
#js-project-pipelines-charts-app{ data: { project_path: @project.full_path } } #js-project-pipelines-charts-app{ data: { project_path: @project.full_path,
should_render_deployment_frequency_charts: should_render_deployment_frequency_charts.to_s } }
- else - else
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts), #js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts),
times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times }, times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times },
last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success }, last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success },
last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success }, last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success },
last_year_chart: { labels: @charts[:year].labels, totals: @charts[:year].total, success: @charts[:year].success } } } last_year_chart: { labels: @charts[:year].labels, totals: @charts[:year].total, success: @charts[:year].success },
project_path: @project.full_path,
should_render_deployment_frequency_charts: should_render_deployment_frequency_charts.to_s } }
...@@ -42,6 +42,8 @@ export default { ...@@ -42,6 +42,8 @@ export default {
vulnerabilityIssueLinksPath: '/api/:version/vulnerabilities/:id/issue_links', vulnerabilityIssueLinksPath: '/api/:version/vulnerabilities/:id/issue_links',
applicationSettingsPath: '/api/:version/application/settings', applicationSettingsPath: '/api/:version/application/settings',
descendantGroupsPath: '/api/:version/groups/:group_id/descendant_groups', descendantGroupsPath: '/api/:version/groups/:group_id/descendant_groups',
projectDeploymentFrequencyAnalyticsPath:
'/api/:version/projects/:id/analytics/deployment_frequency',
userSubscription(namespaceId) { userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId)); const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
...@@ -330,4 +332,13 @@ export default { ...@@ -330,4 +332,13 @@ export default {
const url = Api.buildUrl(this.applicationSettingsPath); const url = Api.buildUrl(this.applicationSettingsPath);
return axios.put(url, data); return axios.put(url, data);
}, },
deploymentFrequencies(projectId, params = {}) {
const url = Api.buildUrl(this.projectDeploymentFrequencyAnalyticsPath).replace(
':id',
encodeURIComponent(projectId),
);
return axios.get(url, { params });
},
}; };
<script>
import dateFormat from 'dateformat';
import Api from 'ee/api';
import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import * as Sentry from '~/sentry/wrapper';
import { nDaysBefore, nMonthsBefore, getDatesInRange } from '~/lib/utils/datetime_utility';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
export default {
name: 'DeploymentFrequencyCharts',
components: {
CiCdAnalyticsAreaChart,
},
inject: {
projectPath: {
type: String,
default: '',
},
},
data() {
// Compute all relative dates based on the _beginning_ of today
const startOfToday = new Date(new Date().setHours(0, 0, 0, 0));
const lastWeek = new Date(nDaysBefore(startOfToday, 7));
const lastMonth = new Date(nMonthsBefore(startOfToday, 1));
const last90Days = new Date(nDaysBefore(startOfToday, 90));
const apiDateFormatString = 'isoDateTime';
const titleDateFormatString = 'mmm d';
const sharedRequestParams = {
environment: 'production',
interval: 'daily',
// We will never have more than 91 records (1 record per day), so we
// don't have to worry about making multiple requests to get all the results
per_page: 100,
};
return {
charts: [
{
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for last week (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(lastWeek, titleDateFormatString),
endDate: dateFormat(startOfToday, titleDateFormatString),
},
),
startDate: lastWeek,
requestParams: {
...sharedRequestParams,
from: dateFormat(lastWeek, apiDateFormatString),
},
isLoading: true,
data: [],
},
{
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for last month (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(lastMonth, titleDateFormatString),
endDate: dateFormat(startOfToday, titleDateFormatString),
},
),
startDate: lastMonth,
requestParams: {
...sharedRequestParams,
from: dateFormat(lastMonth, apiDateFormatString),
},
isLoading: true,
data: [],
},
{
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for the last 90 days (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(last90Days, titleDateFormatString),
endDate: dateFormat(startOfToday, titleDateFormatString),
},
),
startDate: last90Days,
requestParams: {
...sharedRequestParams,
from: dateFormat(last90Days, apiDateFormatString),
},
isLoading: true,
data: [],
},
],
};
},
async mounted() {
const results = await Promise.allSettled(
this.charts.map(async (c) => {
const chart = c;
chart.isLoading = true;
try {
const { data: apiData } = await Api.deploymentFrequencies(
this.projectPath,
chart.requestParams,
);
chart.data = this.apiDataToChartSeries(apiData, chart.startDate);
} finally {
chart.isLoading = false;
}
}),
);
const requestErrors = results.filter((r) => r.status === 'rejected').map((r) => r.reason);
if (requestErrors.length) {
createFlash({
message: s__(
'DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data',
),
});
const allErrorMessages = requestErrors.join('\n');
Sentry.captureException(
new Error(
`Something went wrong while getting deployment frequency data:\n${allErrorMessages}`,
),
);
}
},
methods: {
/**
* Converts the raw data fetched from the
* [Deployment Frequency API](https://docs.gitlab.com/ee/api/project_analytics.html#list-project-deployment-frequencies)
* into series data consumable by
* [GlAreaChart](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/charts-area-chart--default)
*
* @param apiData The raw JSON data from the API request
* @param startDate The first day that should be rendered on the graph
*/
apiDataToChartSeries(apiData, startDate) {
// Get a list of dates (formatted identically to the dates in the API response),
// one date per day in the graph's date range
const dates = getDatesInRange(startDate, new Date(), (date) =>
dateFormat(date, 'yyyy-mm-dd'),
);
// Fill in the API data (the API data doesn't included data points for
// days with 0 deployments) and transform it for use in the graph
const data = dates.map((date) => {
const value = apiData.find((dataPoint) => dataPoint.from === date)?.value || 0;
const formattedDate = dateFormat(new Date(date), 'mmm d');
return [formattedDate, value];
});
return [
{
name: s__('DeploymentFrequencyCharts|Deployments'),
data,
},
];
},
},
areaChartOptions: {
xAxis: {
name: s__('DeploymentFrequencyCharts|Date'),
type: 'category',
},
yAxis: {
name: s__('DeploymentFrequencyCharts|Deployments'),
type: 'value',
minInterval: 1,
},
},
};
</script>
<template>
<div>
<h4 class="gl-my-4">{{ s__('DeploymentFrequencyCharts|Deployments charts') }}</h4>
<ci-cd-analytics-area-chart
v-for="(chart, index) in charts"
:key="index"
:chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
>
{{ chart.title }}
</ci-cd-analytics-area-chart>
</div>
</template>
# frozen_string_literal: true
module EE
module GraphHelper
extend ::Gitlab::Utils::Override
override :should_render_deployment_frequency_charts
def should_render_deployment_frequency_charts
return false unless ::Feature.enabled?(:deployment_frequency_charts, @project)
return false unless @project.feature_available?(:project_activity_analytics)
can?(current_user, :read_project_activity_analytics, @project)
end
end
end
---
name: deployment_frequency_charts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50885
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296893
milestone: '13.8'
type: development
group: group::release
default_enabled: false
...@@ -818,4 +818,21 @@ describe('Api', () => { ...@@ -818,4 +818,21 @@ describe('Api', () => {
}); });
}); });
}); });
describe('Project analytics: deployment frequency', () => {
const projectPath = 'test/project';
const encodedProjectPath = encodeURIComponent(projectPath);
const params = { environment: 'production' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodedProjectPath}/analytics/deployment_frequency`;
describe('deploymentFrequencies', () => {
it('GETs the right url', async () => {
mock.onGet(expectedUrl, { params }).replyOnce(httpStatus.OK, []);
const { data } = await Api.deploymentFrequencies(projectPath, params);
expect(data).toEqual([]);
});
});
});
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue when there are no network errors converts the data from the API into data usable by the chart component 1`] = `
Array [
Array [
"Jun 26",
0,
],
Array [
"Jun 27",
0,
],
Array [
"Jun 28",
0,
],
Array [
"Jun 29",
0,
],
Array [
"Jun 30",
3,
],
Array [
"Jul 1",
1,
],
Array [
"Jul 2",
0,
],
Array [
"Jul 3",
1,
],
Array [
"Jul 4",
0,
],
]
`;
exports[`ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue when there are no network errors converts the data from the API into data usable by the chart component 2`] = `
Array [
Array [
"Jun 3",
0,
],
Array [
"Jun 4",
0,
],
Array [
"Jun 5",
0,
],
Array [
"Jun 6",
0,
],
Array [
"Jun 7",
0,
],
Array [
"Jun 8",
0,
],
Array [
"Jun 9",
0,
],
Array [
"Jun 10",
0,
],
Array [
"Jun 11",
0,
],
Array [
"Jun 12",
0,
],
Array [
"Jun 13",
0,
],
Array [
"Jun 14",
0,
],
Array [
"Jun 15",
0,
],
Array [
"Jun 16",
0,
],
Array [
"Jun 17",
0,
],
Array [
"Jun 18",
0,
],
Array [
"Jun 19",
0,
],
Array [
"Jun 20",
0,
],
Array [
"Jun 21",
0,
],
Array [
"Jun 22",
0,
],
Array [
"Jun 23",
0,
],
Array [
"Jun 24",
0,
],
Array [
"Jun 25",
1,
],
Array [
"Jun 26",
0,
],
Array [
"Jun 27",
0,
],
Array [
"Jun 28",
0,
],
Array [
"Jun 29",
0,
],
Array [
"Jun 30",
3,
],
Array [
"Jul 1",
1,
],
Array [
"Jul 2",
0,
],
Array [
"Jul 3",
1,
],
Array [
"Jul 4",
0,
],
]
`;
exports[`ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue when there are no network errors converts the data from the API into data usable by the chart component 3`] = `
Array [
Array [
"Apr 4",
0,
],
Array [
"Apr 5",
0,
],
Array [
"Apr 6",
0,
],
Array [
"Apr 7",
0,
],
Array [
"Apr 8",
0,
],
Array [
"Apr 9",
0,
],
Array [
"Apr 10",
0,
],
Array [
"Apr 11",
0,
],
Array [
"Apr 12",
0,
],
Array [
"Apr 13",
0,
],
Array [
"Apr 14",
0,
],
Array [
"Apr 15",
0,
],
Array [
"Apr 16",
0,
],
Array [
"Apr 17",
0,
],
Array [
"Apr 18",
0,
],
Array [
"Apr 19",
0,
],
Array [
"Apr 20",
0,
],
Array [
"Apr 21",
0,
],
Array [
"Apr 22",
0,
],
Array [
"Apr 23",
0,
],
Array [
"Apr 24",
0,
],
Array [
"Apr 25",
0,
],
Array [
"Apr 26",
0,
],
Array [
"Apr 27",
0,
],
Array [
"Apr 28",
0,
],
Array [
"Apr 29",
0,
],
Array [
"Apr 30",
0,
],
Array [
"May 1",
0,
],
Array [
"May 2",
0,
],
Array [
"May 3",
0,
],
Array [
"May 4",
0,
],
Array [
"May 5",
0,
],
Array [
"May 6",
0,
],
Array [
"May 7",
0,
],
Array [
"May 8",
0,
],
Array [
"May 9",
0,
],
Array [
"May 10",
0,
],
Array [
"May 11",
0,
],
Array [
"May 12",
0,
],
Array [
"May 13",
0,
],
Array [
"May 14",
0,
],
Array [
"May 15",
0,
],
Array [
"May 16",
0,
],
Array [
"May 17",
0,
],
Array [
"May 18",
0,
],
Array [
"May 19",
0,
],
Array [
"May 20",
0,
],
Array [
"May 21",
0,
],
Array [
"May 22",
0,
],
Array [
"May 23",
0,
],
Array [
"May 24",
0,
],
Array [
"May 25",
0,
],
Array [
"May 26",
0,
],
Array [
"May 27",
0,
],
Array [
"May 28",
0,
],
Array [
"May 29",
0,
],
Array [
"May 30",
0,
],
Array [
"May 31",
0,
],
Array [
"Jun 1",
1,
],
Array [
"Jun 2",
0,
],
Array [
"Jun 3",
0,
],
Array [
"Jun 4",
0,
],
Array [
"Jun 5",
0,
],
Array [
"Jun 6",
0,
],
Array [
"Jun 7",
0,
],
Array [
"Jun 8",
0,
],
Array [
"Jun 9",
0,
],
Array [
"Jun 10",
0,
],
Array [
"Jun 11",
0,
],
Array [
"Jun 12",
0,
],
Array [
"Jun 13",
0,
],
Array [
"Jun 14",
0,
],
Array [
"Jun 15",
0,
],
Array [
"Jun 16",
0,
],
Array [
"Jun 17",
0,
],
Array [
"Jun 18",
0,
],
Array [
"Jun 19",
0,
],
Array [
"Jun 20",
0,
],
Array [
"Jun 21",
0,
],
Array [
"Jun 22",
0,
],
Array [
"Jun 23",
0,
],
Array [
"Jun 24",
0,
],
Array [
"Jun 25",
1,
],
Array [
"Jun 26",
0,
],
Array [
"Jun 27",
0,
],
Array [
"Jun 28",
0,
],
Array [
"Jun 29",
0,
],
Array [
"Jun 30",
3,
],
Array [
"Jul 1",
1,
],
Array [
"Jul 2",
0,
],
Array [
"Jul 3",
1,
],
Array [
"Jul 4",
0,
],
]
`;
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { useFakeDate } from 'helpers/fake_date';
import DeploymentFrequencyCharts from 'ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as Sentry from '~/sentry/wrapper';
import httpStatus from '~/lib/utils/http_status';
jest.mock('~/flash');
jest.mock('~/sentry/wrapper');
const lastWeekData = getJSONFixture(
'api/project_analytics/daily_deployment_frequencies_for_last_week.json',
);
const lastMonthData = getJSONFixture(
'api/project_analytics/daily_deployment_frequencies_for_last_month.json',
);
const last90DaysData = getJSONFixture(
'api/project_analytics/daily_deployment_frequencies_for_last_90_days.json',
);
describe('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue', () => {
// Set the current Date to the same value that is used when generating the fixtures
useFakeDate(2015, 6, 3, 10);
let wrapper;
let mock;
const createComponent = () => {
wrapper = shallowMount(DeploymentFrequencyCharts, {
provide: {
projectPath: 'test/project',
},
});
};
// Initializes the mock endpoint to return a specific set of deployment
// frequency data for a given "from" date.
const setUpMockDeploymentFrequencies = ({ from, data }) => {
mock
.onGet(/projects\/test%2Fproject\/analytics\/deployment_frequency/, {
params: {
environment: 'production',
interval: 'daily',
per_page: 100,
from,
},
})
.replyOnce(httpStatus.OK, data);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
describe('when there are no network errors', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
setUpMockDeploymentFrequencies({ from: '2015-06-26T00:00:00+0000', data: lastWeekData });
setUpMockDeploymentFrequencies({ from: '2015-06-03T00:00:00+0000', data: lastMonthData });
setUpMockDeploymentFrequencies({ from: '2015-04-04T00:00:00+0000', data: last90DaysData });
createComponent();
await axios.waitForAll();
});
it('makes 3 GET requests - one for each chart', () => {
expect(mock.history.get).toHaveLength(3);
});
it('converts the data from the API into data usable by the chart component', () => {
wrapper.findAll(CiCdAnalyticsAreaChart).wrappers.forEach((chartWrapper) => {
expect(chartWrapper.props().chartData[0].data).toMatchSnapshot();
});
});
it('does not show a flash message', () => {
expect(createFlash).not.toHaveBeenCalled();
});
});
describe('when there are network errors', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
createComponent();
await axios.waitForAll();
});
it('shows a flash message', () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while getting deployment frequency data',
});
});
it('reports an error to Sentry', () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
const expectedErrorMessage = [
'Something went wrong while getting deployment frequency data:',
'Error: Request failed with status code 404',
'Error: Request failed with status code 404',
'Error: Request failed with status code 404',
].join('\n');
expect(Sentry.captureException).toHaveBeenCalledWith(new Error(expectedErrorMessage));
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::GraphHelper do
describe '#should_render_deployment_frequency_charts' do
let_it_be(:current_user) { create(:user) }
let(:project) { create(:project, :private) }
let(:is_feature_licensed) { true }
let(:is_flag_enabled) { true }
let(:is_user_authorized) { true }
before do
stub_licensed_features(project_activity_analytics: is_feature_licensed)
stub_feature_flags(deployment_frequency_charts: is_flag_enabled)
self.instance_variable_set(:@current_user, current_user)
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_project_activity_analytics, project).and_return(is_user_authorized)
end
shared_examples 'returns true' do
it { expect(should_render_deployment_frequency_charts).to be(true) }
end
shared_examples 'returns false' do
it { expect(should_render_deployment_frequency_charts).to be(false) }
end
it_behaves_like 'returns true'
context 'when the feature is not available' do
let(:is_feature_licensed) { false }
it_behaves_like 'returns false'
end
context 'when the feature flag is disabled' do
let(:is_flag_enabled) { false }
it_behaves_like 'returns false'
end
context 'when the user does not have permission' do
let(:is_user_authorized) { false }
it_behaves_like 'returns false'
end
end
end
...@@ -9601,6 +9601,27 @@ msgstr "" ...@@ -9601,6 +9601,27 @@ msgstr ""
msgid "Deployment Frequency" msgid "Deployment Frequency"
msgstr "" msgstr ""
msgid "DeploymentFrequencyCharts|Date"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments charts"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for last month (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for last week (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for the last 90 days (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
msgstr ""
msgid "Deployment|API" msgid "Deployment|API"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinesAreaChart matches the snapshot 1`] = ` exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
<div <div
class="gl-mt-3" class="gl-mt-3"
> >
......
...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app_legacy.vue'; import Component from '~/projects/pipelines/charts/components/app_legacy.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue'; import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue'; import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import { import {
counts, counts,
timesChartData, timesChartData,
...@@ -23,6 +23,13 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -23,6 +23,13 @@ describe('ProjectsPipelinesChartsApp', () => {
lastMonthChartData, lastMonthChartData,
lastYearChartData, lastYearChartData,
}, },
provide: {
projectPath: 'test/project',
shouldRenderDeploymentFrequencyCharts: true,
},
stubs: {
DeploymentFrequencyCharts: true,
},
}); });
}); });
...@@ -52,12 +59,12 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -52,12 +59,12 @@ describe('ProjectsPipelinesChartsApp', () => {
describe('pipelines charts', () => { describe('pipelines charts', () => {
it('displays 3 area charts', () => { it('displays 3 area charts', () => {
expect(wrapper.findAll(PipelinesAreaChart).length).toBe(3); expect(wrapper.findAll(CiCdAnalyticsAreaChart).length).toBe(3);
}); });
describe('displays individual correctly', () => { describe('displays individual correctly', () => {
it('renders with the correct data', () => { it('renders with the correct data', () => {
const charts = wrapper.findAll(PipelinesAreaChart); const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) { for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i); const chart = charts.at(i);
......
import { merge } from 'lodash';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper'; import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.vue'; import Component from '~/projects/pipelines/charts/components/app.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue'; import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue'; import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql'; import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql'; import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
import { mockPipelineCount, mockPipelineStatistics } from '../mock_data'; import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
...@@ -13,6 +14,8 @@ const projectPath = 'gitlab-org/gitlab'; ...@@ -13,6 +14,8 @@ const projectPath = 'gitlab-org/gitlab';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
...@@ -25,21 +28,29 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -25,21 +28,29 @@ describe('ProjectsPipelinesChartsApp', () => {
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
} }
function createComponent(options = {}) { function createComponent(mountOptions = {}) {
const { fakeApollo } = options; wrapper = shallowMount(
Component,
return shallowMount(Component, { merge(
provide: { {},
projectPath, {
}, provide: {
localVue, projectPath,
apolloProvider: fakeApollo, shouldRenderDeploymentFrequencyCharts: false,
}); },
localVue,
apolloProvider: createMockApolloProvider(),
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
},
},
mountOptions,
),
);
} }
beforeEach(() => { beforeEach(() => {
const fakeApollo = createMockApolloProvider(); createComponent();
wrapper = createComponent({ fakeApollo });
}); });
afterEach(() => { afterEach(() => {
...@@ -73,12 +84,12 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -73,12 +84,12 @@ describe('ProjectsPipelinesChartsApp', () => {
describe('pipelines charts', () => { describe('pipelines charts', () => {
it('displays 3 area charts', () => { it('displays 3 area charts', () => {
expect(wrapper.findAll(PipelinesAreaChart)).toHaveLength(3); expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
}); });
describe('displays individual correctly', () => { describe('displays individual correctly', () => {
it('renders with the correct data', () => { it('renders with the correct data', () => {
const charts = wrapper.findAll(PipelinesAreaChart); const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) { for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i); const chart = charts.at(i);
...@@ -92,4 +103,26 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -92,4 +103,26 @@ describe('ProjectsPipelinesChartsApp', () => {
}); });
}); });
}); });
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
});
it('renders the deployment frequency charts', () => {
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
});
});
describe('when shouldRenderDeploymentFrequencyCharts is false', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: false } });
});
it('does not render the deployment frequency charts', () => {
expect(findDeploymentFrequencyCharts().exists()).toBe(false);
});
});
}); });
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Component from '~/projects/pipelines/charts/components/pipelines_area_chart.vue'; import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import { transformedAreaChartData } from '../mock_data'; import { transformedAreaChartData } from '../mock_data';
describe('PipelinesAreaChart', () => { describe('CiCdAnalyticsAreaChart', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
wrapper = mount(Component, { wrapper = mount(CiCdAnalyticsAreaChart, {
propsData: { propsData: {
chartData: transformedAreaChartData, chartData: transformedAreaChartData,
areaChartOptions: {
xAxis: {
name: 'X axis title',
type: 'category',
},
yAxis: {
name: 'Y axis title',
},
},
}, },
slots: { slots: {
default: 'Some title', default: 'Some title',
......
...@@ -15,4 +15,16 @@ RSpec.describe GraphHelper do ...@@ -15,4 +15,16 @@ RSpec.describe GraphHelper do
expect(refs).to match('master') expect(refs).to match('master')
end end
end end
describe '#should_render_deployment_frequency_charts' do
let(:project) { create(:project, :private) }
before do
self.instance_variable_set(:@project, project)
end
it 'always returns false' do
expect(should_render_deployment_frequency_charts).to be(false)
end
end
end end
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