Commit a592e3fe authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch '250684-fetch-more-projects' into 'master'

Fetch more group projects

See merge request gitlab-org/gitlab!43044
parents a00f092f 7736ce72
<script>
import {
GlAlert,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
GlModalDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import produce from 'immer';
import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
export default {
name: 'GroupRepositoryAnalytics',
components: {
GlAlert,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
GlSearchBoxByType,
},
......@@ -46,15 +54,23 @@ export default {
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
id: project.id.split('Project/')[1],
id: getIdFromGraphQLId(project.id),
isSelected: false,
}));
},
result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo || {};
},
error() {
this.hasError = true;
},
},
},
data() {
return {
groupProjects: [],
hasError: false,
projectsPageInfo: {},
projectSearchTerm: '',
selectAllProjects: true,
selectedDateRange: this.$options.dateRangeOptions[2],
......@@ -125,6 +141,31 @@ export default {
clickDateRange(dateRange) {
this.selectedDateRange = dateRange;
},
dismissError() {
this.hasError = false;
},
loadMoreProjects() {
this.$apollo.queries.groupProjects
.fetchMore({
variables: {
groupFullPath: this.groupFullPath,
after: this.projectsPageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const results = produce(fetchMoreResult, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.group.projects.nodes = [
...previousResult.group.projects.nodes,
...draftData.group.projects.nodes,
];
});
return results;
},
})
.catch(() => {
this.hasError = true;
});
},
},
text: {
codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'),
......@@ -139,6 +180,7 @@ export default {
projectDropdownHeader: __('Projects'),
projectDropdownAllProjects: __('All projects'),
projectSelectAll: __('Select all'),
queryErrorMessage: s__('RepositoriesAnalytics|There was an error fetching the projects.'),
},
dateRangeOptions: [
{ value: 7, text: __('Last week') },
......@@ -165,20 +207,27 @@ export default {
no-fade
:action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton"
>
<gl-alert
v-if="hasError"
variant="danger"
data-testid="group-code-coverage-projects-error"
@dismiss="dismissError"
>{{ $options.text.queryErrorMessage }}</gl-alert
>
<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>
<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-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"
......@@ -196,6 +245,9 @@ export default {
@click.native.capture.stop="clickDropdownProject(project.id)"
>{{ project.name }}</gl-dropdown-item
>
<gl-intersection-observer v-if="projectsPageInfo.hasNextPage" @appear="loadMoreProjects">
<gl-loading-icon v-if="$apollo.queries.groupProjects.loading" size="md" />
</gl-intersection-observer>
</gl-dropdown>
<gl-button
......@@ -208,13 +260,13 @@ export default {
</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">{{
$options.text.dateRangeHeader
}}</label>
<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-section-header>
{{ $options.text.dateRangeHeader }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="dateRange in $options.dateRangeOptions"
:key="dateRange.value"
......
query getGroupProjects($groupFullPath: ID!) {
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getGroupProjects($groupFullPath: ID!, $after: String) {
group(fullPath: $groupFullPath) {
projects {
projects(after: $after, first: 100) {
nodes {
name
id
}
pageInfo {
...PageInfo
}
}
}
}
---
title: Fetch more group projects for the test coverage dropdown
merge_request: 43044
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlModal } from '@gitlab/ui';
import {
GlAlert,
GlDropdown,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
} from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue';
......@@ -24,6 +31,9 @@ describe('Group repository analytics app', () => {
wrapper
.find(`[data-testid="group-code-coverage-download-select-project-${id}"]`)
.trigger('click');
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
const injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
......@@ -31,18 +41,34 @@ describe('Group repository analytics app', () => {
};
const groupProjectsData = [{ id: 1, name: '1' }, { id: 2, name: '2' }];
const createComponent = () => {
const createComponent = ({ data = {}, apolloGroupProjects = {} } = {}) => {
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 })),
hasError: false,
projectsPageInfo: {
hasNextPage: false,
endCursor: null,
},
...data,
};
},
provide: {
...injectedProperties,
},
mocks: {
$apollo: {
queries: {
groupProjects: {
fetchMore: jest.fn().mockResolvedValue(),
...apolloGroupProjects,
},
},
},
},
stubs: { GlDropdown, GlDropdownItem, GlModal },
});
};
......@@ -65,6 +91,16 @@ describe('Group repository analytics app', () => {
openCodeCoverageModal();
});
describe('when there is an error fetching the projects', () => {
beforeEach(() => {
createComponent({ data: { hasError: true } });
});
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
});
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
......@@ -121,6 +157,67 @@ describe('Group repository analytics app', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
});
});
describe('when there is only one page of projects', () => {
it('should not render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when there is more than a page of projects', () => {
beforeEach(() => {
createComponent({ data: { projectsPageInfo: { hasNextPage: true } } });
});
it('should render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
describe('when the intersection observer component appears in view', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockResolvedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('makes a query to fetch more projects', () => {
expect(wrapper.vm.$apollo.queries.groupProjects.fetchMore).toHaveBeenCalledTimes(1);
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
});
});
describe('when a query is loading a new page of projects', () => {
beforeEach(() => {
createComponent({
data: { projectsPageInfo: { hasNextPage: true } },
apolloGroupProjects: {
loading: true,
},
});
});
it('should render the loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
});
});
describe('when selecting a date range', () => {
......
......@@ -21528,6 +21528,9 @@ msgstr ""
msgid "RepositoriesAnalytics|Test Code Coverage"
msgstr ""
msgid "RepositoriesAnalytics|There was an error fetching the projects."
msgstr ""
msgid "Repository"
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