Commit 49c03436 authored by Scott Hampton's avatar Scott Hampton Committed by Jose Ivan Vargas

Get code coverage for selected projects

Create a modal for selecting the projects
for which you want to download the code
coverage CSV file.
parent c4440a96
<script> <script>
import { GlButton } from '@gitlab/ui'; import {
import { __ } from '~/locale'; GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
GlModalDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility'; import { pikadayToString } from '~/lib/utils/datetime_utility';
import { getProjectIdQueryParams } from '../utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
export default { export default {
name: 'GroupRepositoryAnalytics', name: 'GroupRepositoryAnalytics',
components: { components: {
GlButton, GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
},
directives: {
GlModalDirective,
}, },
inject: { inject: {
groupAnalyticsCoverageReportsPath: { groupAnalyticsCoverageReportsPath: {
type: String, type: String,
default: '', default: '',
}, },
groupFullPath: {
type: String,
default: '',
},
},
apollo: {
groupProjects: {
query: getGroupProjects,
variables() {
return {
groupFullPath: this.groupFullPath,
};
},
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
id: project.id.split('Project/')[1],
isSelected: false,
}));
},
},
},
data() {
return {
groupProjects: [],
projectSearchTerm: '',
selectAllProjects: true,
selectedDateRange: this.$options.dateRangeOptions[2],
};
}, },
computed: { computed: {
cancelModalButton() {
return {
text: __('Cancel'),
};
},
csvReportPath() { csvReportPath() {
const today = new Date(); const today = new Date();
const endDate = pikadayToString(today); const endDate = pikadayToString(today);
today.setFullYear(today.getFullYear() - 1); today.setDate(today.getDate() - this.selectedDateRange.value);
const startDate = pikadayToString(today); const startDate = pikadayToString(today);
return `${this.groupAnalyticsCoverageReportsPath}&start_date=${startDate}&end_date=${endDate}`; return `${this.groupAnalyticsCoverageReportsPath}&start_date=${startDate}&end_date=${endDate}&${this.selectedProjectIdsParam}`;
},
downloadCSVModalButton() {
return {
text: this.$options.text.downloadCSVModalButton,
attributes: [
{ variant: 'info' },
{ href: this.csvReportPath },
{ rel: 'nofollow' },
{ download: '' },
{ disabled: this.isDownloadButtonDisabled },
{ 'data-testid': 'group-code-coverage-download-button' },
],
};
},
isDownloadButtonDisabled() {
return !this.selectAllProjects && !this.groupProjects.some(project => project.isSelected);
},
filteredProjects() {
return this.groupProjects.filter(project =>
project.name.toLowerCase().includes(this.projectSearchTerm.toLowerCase()),
);
},
selectedProjectIdsParam() {
if (this.selectAllProjects) {
return getProjectIdQueryParams(this.groupProjects);
}
return getProjectIdQueryParams(this.groupProjects.filter(project => project.isSelected));
},
},
methods: {
clickDropdownProject(id) {
const index = this.groupProjects.map(project => project.id).indexOf(id);
this.groupProjects[index].isSelected = !this.groupProjects[index].isSelected;
this.selectAllProjects = false;
},
clickSelectAllProjects() {
this.selectAllProjects = true;
this.groupProjects = this.groupProjects.map(project => ({
...project,
isSelected: false,
}));
},
clickDateRange(dateRange) {
this.selectedDateRange = dateRange;
}, },
}, },
text: { text: {
codeCoverageHeader: __('RepositoriesAnalytics|Test Code Coverage'), codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'),
downloadCSVButton: __('RepositoriesAnalytics|Download historic test coverage data (.csv)'), downloadCSVButton: s__('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
dateRangeHeader: __('Date range'),
downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'),
downloadCSVModalTitle: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVModalDescription: s__(
'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.',
),
projectDropdown: __('Select projects'),
projectDropdownHeader: __('Projects'),
projectDropdownAllProjects: __('All projects'),
projectSelectAll: __('Select all'),
}, },
dateRangeOptions: [
{ value: 7, text: __('Last week') },
{ value: 14, text: __('Last 2 weeks') },
{ value: 30, text: __('Last 30 days') },
{ value: 60, text: __('Last 60 days') },
{ value: 90, text: __('Last 90 days') },
],
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center"> <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
<h4 class="sub-header">{{ $options.text.codeCoverageHeader }}</h4> <h4 class="sub-header">{{ $options.text.codeCoverageHeader }}</h4>
<gl-button <gl-button
:href="csvReportPath" v-gl-modal-directive="'download-csv-modal'"
rel="nofollow" data-testid="group-code-coverage-modal-button"
download
data-testid="group-code-coverage-csv-button"
>{{ $options.text.downloadCSVButton }}</gl-button >{{ $options.text.downloadCSVButton }}</gl-button
> >
<gl-modal
modal-id="download-csv-modal"
:title="$options.text.downloadCSVModalTitle"
no-fade
:action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton"
>
<div>{{ $options.text.downloadCSVModalDescription }}</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">{{
$options.text.projectDropdownHeader
}}</label>
<gl-dropdown
:text="$options.text.projectDropdown"
class="gl-w-half"
data-testid="group-code-coverage-project-dropdown"
>
<gl-dropdown-section-header>{{
$options.text.projectDropdownHeader
}}</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="projectSearchTerm" class="gl-my-2 gl-mx-3" />
<gl-dropdown-item
:is-check-item="true"
:is-checked="selectAllProjects"
data-testid="group-code-coverage-download-select-all-projects"
@click.native.capture.stop="clickSelectAllProjects()"
>{{ $options.text.projectDropdownAllProjects }}</gl-dropdown-item
>
<gl-dropdown-item
v-for="project in filteredProjects"
:key="project.id"
:is-check-item="true"
:is-checked="project.isSelected"
:data-testid="`group-code-coverage-download-select-project-${project.id}`"
@click.native.capture.stop="clickDropdownProject(project.id)"
>{{ project.name }}</gl-dropdown-item
>
</gl-dropdown>
<gl-button
class="gl-ml-2"
variant="link"
data-testid="group-code-coverage-select-all-projects-button"
@click="clickSelectAllProjects()"
>{{ $options.text.projectSelectAll }}</gl-button
>
</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">{{
$options.text.dateRangeHeader
}}</label>
<gl-dropdown :text="selectedDateRange.text" class="gl-w-half">
<gl-dropdown-section-header>{{
$options.text.dateRangeHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="dateRange in $options.dateRangeOptions"
:key="dateRange.value"
:data-testid="`group-code-coverage-download-select-date-${dateRange.value}`"
@click="clickDateRange(dateRange)"
>{{ dateRange.text }}</gl-dropdown-item
>
</gl-dropdown>
</div>
</gl-modal>
</div> </div>
</template> </template>
query getGroupProjects($groupFullPath: ID!) {
group(fullPath: $groupFullPath) {
projects(includeSubgroups: true) {
nodes {
name
id
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import GroupRepositoryAnalytics from './components/group_repository_analytics.vue'; import GroupRepositoryAnalytics from './components/group_repository_analytics.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const el = document.querySelector('#js-group-repository-analytics'); const el = document.querySelector('#js-group-repository-analytics');
const { groupAnalyticsCoverageReportsPath } = el?.dataset || {}; const { groupAnalyticsCoverageReportsPath, groupFullPath } = el?.dataset || {};
if (el) { if (el) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -12,8 +20,10 @@ export default () => { ...@@ -12,8 +20,10 @@ export default () => {
components: { components: {
GroupRepositoryAnalytics, GroupRepositoryAnalytics,
}, },
apolloProvider,
provide: { provide: {
groupAnalyticsCoverageReportsPath, groupAnalyticsCoverageReportsPath,
groupFullPath,
}, },
render(createElement) { render(createElement) {
return createElement('group-repository-analytics', {}); return createElement('group-repository-analytics', {});
......
export const getProjectIdQueryParams = projects =>
projects.map(project => `project_ids[]=${project.id}`).join('&');
...@@ -4,4 +4,5 @@ ...@@ -4,4 +4,5 @@
%h3 %h3
= _("Repositories Analytics") = _("Repositories Analytics")
#js-group-repository-analytics{ data: { group_analytics_coverage_reports_path: group_analytics_coverage_reports_path(@group, format: :csv, ref_path: "refs/heads/master") } } #js-group-repository-analytics{ data: { group_analytics_coverage_reports_path: group_analytics_coverage_reports_path(@group, format: :csv, ref_path: "refs/heads/master"),
group_full_path: @group.full_path } }
---
title: Add ability to select projects for group coverage report
merge_request: 42129
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlModal } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import { getProjectIdQueryParams } from 'ee/analytics/repository_analytics/utils';
import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue';
const localVue = createLocalVue();
describe('Group repository analytics app', () => {
useFakeDate();
let wrapper;
const findCodeCoverageModalButton = () =>
wrapper.find('[data-testid="group-code-coverage-modal-button"]');
const openCodeCoverageModal = () => {
findCodeCoverageModalButton().vm.$emit('click');
};
const findCodeCoverageDownloadButton = () =>
wrapper.find('[data-testid="group-code-coverage-download-button"]');
const selectAllCodeCoverageProjects = () =>
wrapper
.find('[data-testid="group-code-coverage-download-select-all-projects"]')
.trigger('click');
const selectCodeCoverageProjectById = id =>
wrapper
.find(`[data-testid="group-code-coverage-download-select-project-${id}"]`)
.trigger('click');
const injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
groupFullPath: 'gitlab-org',
};
const groupProjectsData = [{ id: 1, name: '1' }, { id: 2, name: '2' }];
const createComponent = () => {
wrapper = shallowMount(GroupRepositoryAnalytics, {
localVue,
data() {
return {
// Ensure that isSelected is set to false for each project so that every test is reset properly
groupProjects: groupProjectsData.map(project => ({ ...project, isSelected: false })),
};
},
provide: {
...injectedProperties,
},
stubs: { GlDropdown, GlDropdownItem, GlModal },
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders button to open download code coverage modal', () => {
expect(findCodeCoverageModalButton().exists()).toBe(true);
});
describe('when download code coverage modal is displayed', () => {
beforeEach(() => {
openCodeCoverageModal();
});
describe('when selecting a project', () => {
// Due to the fake_date helper, we can always expect today's date to be 2020-07-06
// and the default date 30 days ago to be 2020-06-06
const groupAnalyticsCoverageReportsPathWithDates = `${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06`;
describe('with all projects selected', () => {
beforeEach(() => {
selectAllCodeCoverageProjects();
});
it('renders primary action as a link with all project IDs as parameters', () => {
const projectIdParams = getProjectIdQueryParams(groupProjectsData);
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdParams}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('with one project selected', () => {
beforeEach(() => {
selectCodeCoverageProjectById(groupProjectsData[0].id);
});
it('renders primary action as a link with one project ID as a parameter', () => {
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&project_ids[]=${groupProjectsData[0].id}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('with no projects selected', () => {
beforeEach(() => {
// Select a project to make sure that "Select all" is unchecked
selectCodeCoverageProjectById(groupProjectsData[0].id);
// Click the same project again to unselect it
selectCodeCoverageProjectById(groupProjectsData[0].id);
});
it('renders a disabled primary action button', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
});
});
});
describe('when selecting a date range', () => {
const projectIdParams = '&project_ids[]=1&project_ids[]=2';
it.each`
date | expected
${7} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-29&end_date=2020-07-06${projectIdParams}`}
${14} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-22&end_date=2020-07-06${projectIdParams}`}
${30} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06${projectIdParams}`}
${60} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-05-07&end_date=2020-07-06${projectIdParams}`}
${90} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-04-07&end_date=2020-07-06${projectIdParams}`}
`(
'updates CSV path to have the start date be $date days before today',
({ date, expected }) => {
wrapper
.find(`[data-testid="group-code-coverage-download-select-date-${date}"]`)
.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expected);
});
},
);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue';
const localVue = createLocalVue();
describe('Group repository analytics app', () => {
useFakeDate();
let wrapper;
const injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
};
const createComponent = () => {
wrapper = shallowMount(GroupRepositoryAnalytics, {
localVue,
provide: {
...injectedProperties,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders button to download code coverage CSV report', () => {
const reportButton = wrapper.find('[data-testid="group-code-coverage-csv-button"]');
// Due to the fake_date helper, we can always expect today's date to be 2020-07-06
// and one year ago to be 2019-07-06
const expectedPath = `${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2019-07-06&end_date=2020-07-06`;
expect(reportButton.exists()).toBe(true);
expect(reportButton.attributes('href')).toBe(expectedPath);
});
});
import { getProjectIdQueryParams } from 'ee/analytics/repository_analytics/utils';
describe('group repository analytics util functions', () => {
describe('getProjectIdQueryParams', () => {
it('returns query param string project ids', () => {
const projects = [{ id: 1 }, { id: 2 }];
const expectedString = 'project_ids[]=1&project_ids[]=2';
expect(getProjectIdQueryParams(projects)).toBe(expectedString);
});
});
});
...@@ -8035,6 +8035,9 @@ msgstr "" ...@@ -8035,6 +8035,9 @@ msgstr ""
msgid "Date picker" msgid "Date picker"
msgstr "" msgstr ""
msgid "Date range"
msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days." msgid "Date range cannot exceed %{maxDateRange} days."
msgstr "" msgstr ""
...@@ -14480,6 +14483,18 @@ msgstr[1] "" ...@@ -14480,6 +14483,18 @@ msgstr[1] ""
msgid "Last %{days} days" msgid "Last %{days} days"
msgstr "" msgstr ""
msgid "Last 2 weeks"
msgstr ""
msgid "Last 30 days"
msgstr ""
msgid "Last 60 days"
msgstr ""
msgid "Last 90 days"
msgstr ""
msgid "Last Accessed On" msgid "Last Accessed On"
msgstr "" msgstr ""
...@@ -14555,6 +14570,9 @@ msgstr "" ...@@ -14555,6 +14570,9 @@ msgstr ""
msgid "Last used on:" msgid "Last used on:"
msgstr "" msgstr ""
msgid "Last week"
msgstr ""
msgid "LastCommit|authored" msgid "LastCommit|authored"
msgstr "" msgstr ""
...@@ -21303,9 +21321,18 @@ msgstr "" ...@@ -21303,9 +21321,18 @@ msgstr ""
msgid "Repositories Analytics" msgid "Repositories Analytics"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Download Historic Test Coverage Data"
msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)" msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Download test coverage data (.csv)"
msgstr ""
msgid "RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis."
msgstr ""
msgid "RepositoriesAnalytics|Test Code Coverage" msgid "RepositoriesAnalytics|Test Code Coverage"
msgstr "" 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