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>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import {
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
GlModalDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility';
import { getProjectIdQueryParams } from '../utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
export default {
name: 'GroupRepositoryAnalytics',
components: {
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
},
directives: {
GlModalDirective,
},
inject: {
groupAnalyticsCoverageReportsPath: {
type: String,
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: {
cancelModalButton() {
return {
text: __('Cancel'),
};
},
csvReportPath() {
const today = new Date();
const endDate = pikadayToString(today);
today.setFullYear(today.getFullYear() - 1);
today.setDate(today.getDate() - this.selectedDateRange.value);
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: {
codeCoverageHeader: __('RepositoriesAnalytics|Test Code Coverage'),
downloadCSVButton: __('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'),
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>
<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>
<gl-button
:href="csvReportPath"
rel="nofollow"
download
data-testid="group-code-coverage-csv-button"
v-gl-modal-directive="'download-csv-modal'"
data-testid="group-code-coverage-modal-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>
</template>
query getGroupProjects($groupFullPath: ID!) {
group(fullPath: $groupFullPath) {
projects(includeSubgroups: true) {
nodes {
name
id
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import GroupRepositoryAnalytics from './components/group_repository_analytics.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.querySelector('#js-group-repository-analytics');
const { groupAnalyticsCoverageReportsPath } = el?.dataset || {};
const { groupAnalyticsCoverageReportsPath, groupFullPath } = el?.dataset || {};
if (el) {
// eslint-disable-next-line no-new
......@@ -12,8 +20,10 @@ export default () => {
components: {
GroupRepositoryAnalytics,
},
apolloProvider,
provide: {
groupAnalyticsCoverageReportsPath,
groupFullPath,
},
render(createElement) {
return createElement('group-repository-analytics', {});
......
export const getProjectIdQueryParams = projects =>
projects.map(project => `project_ids[]=${project.id}`).join('&');
......@@ -4,4 +4,5 @@
%h3
= _("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 ""
msgid "Date picker"
msgstr ""
msgid "Date range"
msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
......@@ -14480,6 +14483,18 @@ msgstr[1] ""
msgid "Last %{days} days"
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"
msgstr ""
......@@ -14555,6 +14570,9 @@ msgstr ""
msgid "Last used on:"
msgstr ""
msgid "Last week"
msgstr ""
msgid "LastCommit|authored"
msgstr ""
......@@ -21303,9 +21321,18 @@ msgstr ""
msgid "Repositories Analytics"
msgstr ""
msgid "RepositoriesAnalytics|Download Historic Test Coverage Data"
msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)"
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"
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