Commit 06584f7a authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '329445-devops-adoption-should-not-only-fetch-the-first-200-groups' into 'master'

DevOps Adoption change add groups to dropdown

See merge request gitlab-org/gitlab!63746
parents d89c4976 8bd72f34
......@@ -58,7 +58,8 @@ The DevOps Adoption tab shows you which groups within your organization are usin
- Pipelines
- Deployments
Buttons to manage your groups appear in the DevOps Adoption section of the page.
When managing groups in the UI, you can add your groups with the **Add group to table**
button, in the top right hand section the page.
DevOps Adoption allows you to:
......
......@@ -36,7 +36,7 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your
- Pipelines
- Deployments
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
When managing groups in the UI, you can add your sub-groups with the **Add sub-group to table**
button, in the top right hand section of your Groups pages.
With DevOps Adoption you can:
......
<script>
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { convertToGraphQLId, TYPE_GROUP } from '~/graphql_shared/utils';
import {
DEBOUNCE_DELAY,
DEVOPS_ADOPTION_GROUP_DROPDOWN_TEXT,
DEVOPS_ADOPTION_GROUP_DROPDOWN_HEADER,
DEVOPS_ADOPTION_ADMIN_DROPDOWN_TEXT,
DEVOPS_ADOPTION_ADMIN_DROPDOWN_HEADER,
DEVOPS_ADOPTION_NO_RESULTS,
DEVOPS_ADOPTION_NO_SUB_GROUPS,
} from '../constants';
import bulkEnableDevopsAdoptionNamespacesMutation from '../graphql/mutations/bulk_enable_devops_adoption_namespaces.mutation.graphql';
export default {
name: 'DevopsAdoptionAddDropdown',
i18n: {
noResults: DEVOPS_ADOPTION_NO_RESULTS,
},
debounceDelay: DEBOUNCE_DELAY,
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
isGroup: {
default: false,
},
},
props: {
groups: {
type: Array,
required: true,
},
searchTerm: {
type: String,
required: false,
default: '',
},
isLoadingGroups: {
type: Boolean,
required: false,
default: false,
},
hasSubgroups: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
filteredGroupsLength() {
return this.groups?.length;
},
dropdownTitle() {
return this.isGroup
? DEVOPS_ADOPTION_GROUP_DROPDOWN_TEXT
: DEVOPS_ADOPTION_ADMIN_DROPDOWN_TEXT;
},
dropdownHeader() {
return this.isGroup
? DEVOPS_ADOPTION_GROUP_DROPDOWN_HEADER
: DEVOPS_ADOPTION_ADMIN_DROPDOWN_HEADER;
},
tooltipText() {
return this.isLoadingGroups || this.hasSubgroups ? false : DEVOPS_ADOPTION_NO_SUB_GROUPS;
},
},
beforeDestroy() {
clearTimeout(this.timeout);
this.timeout = null;
},
methods: {
enableGroup(id) {
this.$apollo
.mutate({
mutation: bulkEnableDevopsAdoptionNamespacesMutation,
variables: {
namespaceIds: [convertToGraphQLId(TYPE_GROUP, id)],
displayNamespaceId: this.groupGid,
},
update: (store, { data }) => {
const {
bulkEnableDevopsAdoptionNamespaces: { enabledNamespaces, errors: requestErrors },
} = data;
if (!requestErrors.length) this.$emit('segmentsAdded', enabledNamespaces);
},
})
.catch((error) => {
Sentry.captureException(error);
});
},
},
};
</script>
<template>
<gl-dropdown
v-gl-tooltip="tooltipText"
:text="dropdownTitle"
:header-text="dropdownHeader"
:disabled="!hasSubgroups"
@show="$emit('trackModalOpenState', true)"
@hide="$emit('trackModalOpenState', false)"
>
<template #header>
<gl-search-box-by-type
:debounce="$options.debounceDelay"
@input="$emit('fetchGroups', $event)"
/>
</template>
<gl-loading-icon v-if="isLoadingGroups" />
<template v-else>
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
data-testid="group-row"
@click="enableGroup(group.id)"
>
{{ group.full_name }}
</gl-dropdown-item>
<gl-dropdown-item v-show="!filteredGroupsLength" data-testid="no-results">{{
$options.i18n.noResults
}}</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
......@@ -4,11 +4,11 @@ import * as Sentry from '@sentry/browser';
import dateformat from 'dateformat';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import API from '~/api';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import {
DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_ERROR_KEYS,
MAX_REQUEST_COUNT,
DATE_TIME_FORMAT,
DEFAULT_POLLING_INTERVAL,
DEVOPS_ADOPTION_GROUP_LEVEL_LABEL,
......@@ -21,15 +21,15 @@ import devopsAdoptionEnabledNamespacesQuery from '../graphql/queries/devops_adop
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates';
import { shouldPollTableData } from '../utils/helpers';
import DevopsAdoptionAddDropdown from './devops_adoption_add_dropdown.vue';
import DevopsAdoptionSection from './devops_adoption_section.vue';
import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
export default {
name: 'DevopsAdoptionApp',
components: {
GlAlert,
DevopsAdoptionAddDropdown,
DevopsAdoptionSection,
DevopsAdoptionSegmentModal,
DevopsScore,
GlTabs,
GlTab,
......@@ -60,6 +60,7 @@ export default {
devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION,
data() {
return {
hasSubgroups: undefined,
isLoadingGroups: false,
isLoadingEnableGroup: false,
requestCount: 0,
......@@ -138,11 +139,6 @@ export default {
this.isLoadingEnableGroup || this.$apollo.queries.devopsAdoptionEnabledNamespaces.loading
);
},
editGroupsButtonLabel() {
return this.isGroup
? this.$options.i18n.groupLevelLabel
: this.$options.i18n.tableHeader.button;
},
tabIndexValues() {
const tabs = this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab);
......@@ -151,13 +147,21 @@ export default {
availableGroups() {
return this.groups?.nodes || [];
},
enabledGroups() {
enabledNamespaces() {
return this.devopsAdoptionEnabledNamespaces?.nodes || [];
},
disabledGroupNodes() {
const enabledNamespaceIds = this.enabledNamespaces.map((group) =>
getIdFromGraphQLId(group.namespace.id),
);
return this.availableGroups.filter((group) => !enabledNamespaceIds.includes(group.id));
},
},
created() {
this.fetchGroups();
this.selectTab();
this.startPollingTableData();
},
beforeDestroy() {
clearInterval(this.pollingTableData);
......@@ -215,8 +219,10 @@ export default {
this.errors[key] = true;
Sentry.captureException(error);
},
fetchGroups(nextPage) {
fetchGroups(searchTerm = '') {
this.searchTerm = searchTerm;
this.isLoadingGroups = true;
this.$apollo
.query({
query: getGroupsQuery,
......@@ -224,25 +230,17 @@ export default {
isSingleRequest: true,
},
variables: {
nextPage,
search: searchTerm,
},
})
.then(({ data }) => {
const { pageInfo, nodes } = data.groups;
this.groups = data.groups;
// Update data
this.groups = {
pageInfo,
nodes: [...this.groups.nodes, ...nodes],
};
this.requestCount += 1;
if (this.requestCount < MAX_REQUEST_COUNT && pageInfo?.nextPage) {
this.fetchGroups(pageInfo.nextPage);
} else {
this.isLoadingGroups = false;
this.startPollingTableData();
if (this.hasSubgroups === undefined) {
this.hasSubgroups = this.groups?.nodes?.length > 0;
}
this.isLoadingGroups = false;
})
.catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error));
},
......@@ -317,11 +315,16 @@ export default {
:has-segments-data="hasSegmentsData"
:timestamp="timestamp"
:has-group-data="hasGroupData"
:edit-groups-button-label="editGroupsButtonLabel"
:cols="tab.cols"
:segments="devopsAdoptionEnabledNamespaces"
:search-term="searchTerm"
:disabled-group-nodes="disabledGroupNodes"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@segmentsRemoved="deleteSegmentsFromCache"
@openAddRemoveModal="openAddRemoveModal"
@fetchGroups="fetchGroups"
@segmentsAdded="addSegmentsToCache"
@trackModalOpenState="trackModalOpenState"
/>
</gl-tab>
......@@ -329,17 +332,22 @@ export default {
<template #title>{{ s__('DevopsReport|DevOps Score') }}</template>
<devops-score />
</gl-tab>
</gl-tabs>
<devops-adoption-segment-modal
v-if="!hasLoadingError"
ref="addRemoveModal"
:groups="availableGroups"
:enabled-groups="enabledGroups"
:is-loading="isLoading"
@segmentsAdded="addSegmentsToCache"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
<template #tabs-end>
<span
class="nav-item gl-align-self-center gl-flex-fill-1 gl-display-none gl-md-display-block"
align="right"
>
<devops-adoption-add-dropdown
:search-term="searchTerm"
:groups="disabledGroupNodes"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@fetchGroups="fetchGroups"
@segmentsAdded="addSegmentsToCache"
/>
</span>
</template>
</gl-tabs>
</div>
</template>
<script>
import { GlLoadingIcon, GlButton, GlSprintf } from '@gitlab/ui';
import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { TABLE_HEADER_TEXT } from '../constants';
import DevopsAdoptionAddDropdown from './devops_adoption_add_dropdown.vue';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue';
......@@ -8,9 +9,9 @@ export default {
components: {
DevopsAdoptionTable,
GlLoadingIcon,
GlButton,
GlSprintf,
DevopsAdoptionEmptyState,
DevopsAdoptionAddDropdown,
},
i18n: {
tableHeaderText: TABLE_HEADER_TEXT,
......@@ -32,10 +33,6 @@ export default {
type: Boolean,
required: true,
},
editGroupsButtonLabel: {
type: String,
required: true,
},
cols: {
type: Array,
required: true,
......@@ -45,25 +42,46 @@ export default {
required: false,
default: () => {},
},
disabledGroupNodes: {
type: Array,
required: true,
},
searchTerm: {
type: String,
required: true,
},
isLoadingGroups: {
type: Boolean,
required: true,
},
hasSubgroups: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="md" class="gl-my-5" />
<div v-else-if="hasSegmentsData" class="gl-mt-3">
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-my-3"
data-testid="tableHeader"
>
<div class="gl-my-3" data-testid="tableHeader">
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.tableHeaderText">
<template #timestamp>{{ timestamp }}</template>
</gl-sprintf>
</span>
<gl-button v-if="hasGroupData" @click="$emit('openAddRemoveModal')">{{
editGroupsButtonLabel
}}</gl-button>
<devops-adoption-add-dropdown
class="gl-mt-4 gl-mb-3 gl-md-display-none"
:search-term="searchTerm"
:groups="disabledGroupNodes"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@fetchGroups="$emit('fetchGroups', $event)"
@segmentsAdded="$emit('segmentsAdded', $event)"
@trackModalOpenState="$emit('trackModalOpenState', $event)"
/>
</div>
<devops-adoption-table
:cols="cols"
......
......@@ -2,9 +2,9 @@ import { s__, __ } from '~/locale';
export const DEFAULT_POLLING_INTERVAL = 30000;
export const MAX_REQUEST_COUNT = 10;
export const PER_PAGE = 20;
export const PER_PAGE = 100;
export const DEBOUNCE_DELAY = 500;
export const DEVOPS_ADOPTION_SEGMENT_MODAL_ID = 'devopsSegmentModal';
......@@ -28,6 +28,15 @@ export const DEVOPS_ADOPTION_TABLE_REMOVE_BUTTON_DISABLED = s__(
'DevopsAdoption|You cannot remove the group you are currently in.',
);
export const DEVOPS_ADOPTION_GROUP_DROPDOWN_TEXT = s__('DevopsAdoption|Add sub-group to table');
export const DEVOPS_ADOPTION_GROUP_DROPDOWN_HEADER = s__('DevopsAdoption|Add sub-group');
export const DEVOPS_ADOPTION_ADMIN_DROPDOWN_TEXT = s__('DevopsAdoption|Add group to table');
export const DEVOPS_ADOPTION_ADMIN_DROPDOWN_HEADER = s__('DevopsAdoption|Add group');
export const DEVOPS_ADOPTION_NO_RESULTS = s__('DevopsAdoption|No results…');
export const DEVOPS_ADOPTION_NO_SUB_GROUPS = s__('DevopsAdoption|This group has no sub-groups');
export const DEVOPS_ADOPTION_STRINGS = {
app: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: s__(
......
......@@ -9,7 +9,7 @@ Vue.use(VueApollo);
export const createResolvers = (groupId) => ({
Query: {
groups(_, { search, nextPage }) {
groups(_, { search }) {
const url = groupId
? Api.buildUrl(Api.subgroupsPath).replace(':id', groupId)
: Api.buildUrl(Api.groupsPath);
......@@ -17,20 +17,13 @@ export const createResolvers = (groupId) => ({
per_page: PER_PAGE,
search,
};
if (nextPage) {
params.page = nextPage;
}
return axios.get(url, { params }).then(({ data, headers }) => {
const pageInfo = {
nextPage: headers['x-next-page'],
};
return axios.get(url, { params }).then(({ data }) => {
const groups = {
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Groups',
// eslint-disable-next-line @gitlab/require-i18n-strings
nodes: data.map((group) => ({ ...group, __typename: 'Group' })),
pageInfo,
};
return groups;
......
query getGroups($search: String, $nextPage: String) {
groups(search: $search, nextPage: $nextPage) @client
query getGroups($search: String) {
groups(search: $search) @client
}
......@@ -54,7 +54,7 @@ RSpec.describe 'DevOps Report page', :js do
visit admin_dev_ops_report_path
within tabs_selector do
expect(page.all(:css, tab_item_selector).length).to be(4)
expect(page.all(:css, tab_item_selector).length).to be(5)
expect(page).to have_text 'Dev Sec Ops DevOps Score'
end
end
......
import { GlDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import DevopsAdoptionAddDropdown from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_add_dropdown.vue';
import bulkEnableDevopsAdoptionNamespacesMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/bulk_enable_devops_adoption_namespaces.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { groupNodes, devopsAdoptionNamespaceData, genericDeleteErrorMessage } from '../mock_data';
const localVue = createLocalVue();
Vue.use(VueApollo);
const mutate = jest.fn().mockResolvedValue({
data: {
bulkEnableDevopsAdoptionNamespaces: {
enabledNamespaces: [devopsAdoptionNamespaceData.nodes[0]],
errors: [],
},
},
});
const mutateWithErrors = jest.fn().mockRejectedValue(genericDeleteErrorMessage);
describe('DevopsAdoptionAddDropdown', () => {
let wrapper;
const createComponent = ({ enableNamespaceSpy = mutate, provide = {}, props = {} } = {}) => {
const mockApollo = createMockApollo([
[bulkEnableDevopsAdoptionNamespacesMutation, enableNamespaceSpy],
]);
wrapper = shallowMountExtended(DevopsAdoptionAddDropdown, {
localVue,
apolloProvider: mockApollo,
propsData: {
groups: [],
...props,
},
provide,
directives: {
GlTooltip: createMockDirective(),
},
stubs: {
GlDropdown,
},
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const clickFirstRow = () => wrapper.findByTestId('group-row').vm.$emit('click');
describe('default behaviour', () => {
beforeEach(() => {
createComponent();
});
it('displays a dropdown component', () => {
expect(findDropdown().exists()).toBe(true);
});
it('displays the correct text', () => {
const dropdown = findDropdown();
expect(dropdown.props('text')).toBe('Add group to table');
expect(dropdown.props('headerText')).toBe('Add group');
});
it('is disabled', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
it('displays a tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe('This group has no sub-groups');
});
});
describe('with isGroup === true', () => {
it('displays the correct text', () => {
createComponent({ provide: { isGroup: true } });
const dropdown = findDropdown();
expect(dropdown.props('text')).toBe('Add sub-group to table');
expect(dropdown.props('headerText')).toBe('Add sub-group');
});
});
describe('with sub-groups available', () => {
describe('displays the correct components', () => {
beforeEach(() => {
createComponent({ props: { hasSubgroups: true } });
});
it('is enabled', () => {
expect(findDropdown().props('disabled')).toBe(false);
});
it('does not display a tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(false);
});
it('displays the no results message', () => {
const noResultsRow = wrapper.findByTestId('no-results');
expect(noResultsRow.exists()).toBe(true);
expect(noResultsRow.text()).toBe('No results…');
});
});
describe('with group data', () => {
it('displays the corrent number of rows', () => {
createComponent({ props: { hasSubgroups: true, groups: groupNodes } });
expect(wrapper.findAllByTestId('group-row')).toHaveLength(groupNodes.length);
});
describe('on row click', () => {
describe('sucessful request', () => {
beforeEach(() => {
createComponent({ props: { hasSubgroups: true, groups: groupNodes } });
clickFirstRow();
});
it('makes a request to enable the selected group', () => {
expect(mutate).toHaveBeenCalledWith({
displayNamespaceId: undefined,
namespaceIds: ['gid://gitlab/Group/1'],
});
});
it('emits the segmentsAdded event', () => {
const [params] = wrapper.emitted().segmentsAdded[0];
expect(params).toStrictEqual([devopsAdoptionNamespaceData.nodes[0]]);
});
});
describe('on error', () => {
beforeEach(() => {
jest.spyOn(Sentry, 'captureException');
createComponent({
enableNamespaceSpy: mutateWithErrors,
props: { hasSubgroups: true, groups: groupNodes },
});
clickFirstRow();
});
it('calls sentry', () => {
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(
genericDeleteErrorMessage,
);
});
it('does not emit the segmentsAdded event', () => {
expect(wrapper.emitted().segmentsAdded).not.toBeDefined();
});
});
});
});
describe('while loading', () => {
beforeEach(() => {
wrapper.setProps({ isLoadingGroups: true });
});
it('displays a loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not display any rows', () => {
expect(wrapper.findAllByTestId('group-row')).toHaveLength(0);
});
});
describe('searching', () => {
it('emits the fetchGroups event ', () => {
createComponent({ props: { hasSubgroups: true } });
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'blah');
jest.runAllTimers();
const [params] = wrapper.emitted().fetchGroups[0];
expect(params).toBe('blah');
});
});
});
});
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlTabs } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import DevopsAdoptionAddDropdown from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_add_dropdown.vue';
import DevopsAdoptionApp from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_app.vue';
import DevopsAdoptionSection from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_section.vue';
import DevopsAdoptionSegmentModal from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_segment_modal.vue';
......@@ -22,7 +23,6 @@ import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import API from '~/api';
import {
groupNodes,
nextGroupNode,
groupPageInfo,
devopsAdoptionNamespaceData,
devopsAdoptionNamespaceDataEmpty,
......@@ -95,6 +95,9 @@ describe('DevopsAdoptionApp', () => {
data() {
return data;
},
stubs: {
GlTabs,
},
});
}
......@@ -104,7 +107,7 @@ describe('DevopsAdoptionApp', () => {
wrapper.destroy();
});
describe('initial request', () => {
describe('group data request', () => {
let groupsSpy;
afterEach(() => {
......@@ -119,16 +122,12 @@ describe('DevopsAdoptionApp', () => {
await waitForPromises();
});
it('renders the segment modal', () => {
expect(wrapper.find(DevopsAdoptionSegmentModal).exists()).toBe(true);
});
it('should fetch data once', () => {
expect(groupsSpy).toHaveBeenCalledTimes(1);
});
});
describe('when error is thrown in the initial request', () => {
describe('when error is thrown fetching group data', () => {
const error = new Error('foo!');
beforeEach(async () => {
......@@ -152,98 +151,6 @@ describe('DevopsAdoptionApp', () => {
});
});
describe('fetchMore request', () => {
let groupsSpy;
afterEach(() => {
groupsSpy = null;
});
describe('when group data is present', () => {
beforeEach(async () => {
groupsSpy = jest
.fn()
.mockResolvedValueOnce(initialResponse)
// `fetchMore` response
.mockResolvedValueOnce({
__typename: 'Groups',
nodes: [nextGroupNode],
nextPage: null,
});
const mockApollo = createMockApolloProvider({ groupsSpy });
wrapper = createComponent({ mockApollo });
await waitForPromises();
});
it('renders the segment modal', () => {
expect(wrapper.find(DevopsAdoptionSegmentModal).exists()).toBe(true);
});
it('should fetch data twice', () => {
expect(groupsSpy).toHaveBeenCalledTimes(2);
});
it('should fetch more data', () => {
expect(groupsSpy.mock.calls[0][1]).toMatchObject({
nextPage: undefined,
});
expect(groupsSpy.mock.calls[1][1]).toMatchObject({
nextPage: 2,
});
});
});
describe('when fetching too many pages of data', () => {
beforeEach(async () => {
// Always send the same page
groupsSpy = jest.fn().mockResolvedValue(initialResponse);
const mockApollo = createMockApolloProvider({ groupsSpy });
wrapper = createComponent({ mockApollo, data: { requestCount: 2 } });
await waitForPromises();
});
it('should fetch data twice', () => {
expect(groupsSpy).toHaveBeenCalledTimes(2);
});
});
describe('when error is thrown in the fetchMore request', () => {
const error = 'Error: foo!';
beforeEach(async () => {
jest.spyOn(Sentry, 'captureException');
groupsSpy = jest
.fn()
.mockResolvedValueOnce(initialResponse)
// `fetchMore` response
.mockRejectedValue(error);
const mockApollo = createMockApolloProvider({ groupsSpy });
wrapper = createComponent({ mockApollo });
await waitForPromises();
});
it('should fetch data twice', () => {
expect(groupsSpy).toHaveBeenCalledTimes(2);
});
it('should fetch more data', () => {
expect(groupsSpy.mock.calls[0][1]).toMatchObject({
nextPage: undefined,
});
expect(groupsSpy.mock.calls[1][1]).toMatchObject({
nextPage: 2,
});
});
it('displays the error message and calls Sentry', () => {
const alert = wrapper.find(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.groupsError);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(error);
});
});
});
describe('segments data', () => {
describe('when there is no active group', () => {
beforeEach(async () => {
......@@ -458,6 +365,10 @@ describe('DevopsAdoptionApp', () => {
).toBe(true);
});
it('displays the DevopsAdoptionAddDropdown as the last tab', () => {
expect(wrapper.find(DevopsAdoptionAddDropdown).exists()).toBe(true);
});
eventTrackingBehaviour('devops-adoption-tab', 'i_analytics_dev_ops_adoption');
});
};
......
import { GlLoadingIcon, GlButton, GlSprintf } from '@gitlab/ui';
import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getByText } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import DevopsAdoptionAddDropdown from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_add_dropdown.vue';
import DevopsAdoptionEmptyState from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_empty_state.vue';
import DevopsAdoptionSection from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_section.vue';
import DevopsAdoptionTable from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table.vue';
import { DEVOPS_ADOPTION_TABLE_CONFIGURATION } from 'ee/analytics/devops_report/devops_adoption/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { devopsAdoptionNamespaceData } from '../mock_data';
import { devopsAdoptionNamespaceData, groupNodes } from '../mock_data';
describe('DevopsAdoptionSection', () => {
let wrapper;
......@@ -19,9 +20,12 @@ describe('DevopsAdoptionSection', () => {
hasSegmentsData: true,
timestamp: '2020-10-31 23:59',
hasGroupData: true,
editGroupsButtonLabel: 'Add/Remove groups',
cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols,
segments: devopsAdoptionNamespaceData,
disabledGroupNodes: groupNodes,
searchTerm: '',
isLoadingGroups: false,
hasSubgroups: true,
...props,
},
stubs: {
......@@ -35,7 +39,7 @@ describe('DevopsAdoptionSection', () => {
const findTableHeaderSection = () => wrapper.findByTestId('tableHeader');
const findTable = () => wrapper.findComponent(DevopsAdoptionTable);
const findEmptyState = () => wrapper.findComponent(DevopsAdoptionEmptyState);
const findAddEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(DevopsAdoptionAddDropdown);
describe('while loading', () => {
beforeEach(() => {
......@@ -96,40 +100,10 @@ describe('DevopsAdoptionSection', () => {
expect(getByText(wrapper.element, text)).not.toBeNull();
});
describe('with group data', () => {
it('displays the edit groups button', () => {
createComponent();
expect(findAddEditButton().exists()).toBe(true);
});
describe('edit groups button', () => {
beforeEach(() => {
createComponent();
});
it('is enabled', () => {
expect(findAddEditButton().props('disabled')).toBe(false);
});
it('emits openAddRemoveModal when clicked', () => {
expect(wrapper.emitted('openAddRemoveModal')).toBeUndefined();
findAddEditButton().vm.$emit('click');
expect(wrapper.emitted('openAddRemoveModal')).toEqual([[]]);
});
});
});
describe('with no group data', () => {
beforeEach(() => {
createComponent({ hasGroupData: false });
});
it('displays the add groups dropdown', () => {
createComponent();
it('does not display the edit groups button', () => {
expect(findAddEditButton().exists()).toBe(false);
});
expect(findDropdown().exists()).toBe(true);
});
});
});
......@@ -5,7 +5,7 @@ import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/q
import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { groupData, pageData, groupNodes, groupPageInfo } from '../mock_data';
import { groupData, groupNodes } from '../mock_data';
const fetchGroupsUrl = Api.buildUrl(Api.groupsPath);
const fetchSubGroupsUrl = Api.buildUrl(Api.subgroupsPath).replace(':id', 1);
......@@ -29,7 +29,7 @@ describe('DevOps GraphQL resolvers', () => {
});
it('fetches all relevent groups / subgroups', async () => {
mockAdapter.onGet(url).reply(httpStatus.OK, groupData, pageData);
mockAdapter.onGet(url).reply(httpStatus.OK, groupData);
await mockClient.query({ query: getGroupsQuery });
expect(mockAdapter.history.get[0].params).not.toEqual(
......@@ -38,40 +38,25 @@ describe('DevOps GraphQL resolvers', () => {
});
it('when receiving groups data', async () => {
mockAdapter.onGet(url).reply(httpStatus.OK, groupData, pageData);
mockAdapter.onGet(url).reply(httpStatus.OK, groupData);
const result = await mockClient.query({ query: getGroupsQuery });
expect(result.data).toEqual({
groups: {
__typename: 'Groups',
nodes: groupNodes,
pageInfo: groupPageInfo,
},
});
});
it('when receiving empty groups data', async () => {
mockAdapter.onGet(url).reply(httpStatus.OK, [], pageData);
mockAdapter.onGet(url).reply(httpStatus.OK, []);
const result = await mockClient.query({ query: getGroupsQuery });
expect(result.data).toEqual({
groups: {
__typename: 'Groups',
nodes: [],
pageInfo: groupPageInfo,
},
});
});
it('with no page information', async () => {
mockAdapter.onGet(url).reply(httpStatus.OK, [], {});
const result = await mockClient.query({ query: getGroupsQuery });
expect(result.data).toEqual({
groups: {
__typename: 'Groups',
nodes: [],
pageInfo: {},
},
});
});
......
......@@ -3,10 +3,6 @@ export const groupData = [
{ id: '2', full_name: 'Bar' },
];
export const pageData = {
'x-next-page': 2,
};
export const groupNodes = [
{
__typename: 'Group',
......@@ -29,12 +25,6 @@ export const groupIds = [1, 2];
export const groupGids = ['gid://gitlab/Group/1', 'gid://gitlab/Group/2'];
export const nextGroupNode = {
__typename: 'Group',
full_name: 'Baz',
id: '3',
};
export const groupPageInfo = {
nextPage: 2,
};
......
......@@ -11255,6 +11255,18 @@ msgstr ""
msgid "DevopsAdoption|Add a group to get started"
msgstr ""
msgid "DevopsAdoption|Add group"
msgstr ""
msgid "DevopsAdoption|Add group to table"
msgstr ""
msgid "DevopsAdoption|Add sub-group"
msgstr ""
msgid "DevopsAdoption|Add sub-group to table"
msgstr ""
msgid "DevopsAdoption|Add/remove groups"
msgstr ""
......@@ -11330,6 +11342,9 @@ msgstr ""
msgid "DevopsAdoption|No filter results."
msgstr ""
msgid "DevopsAdoption|No results…"
msgstr ""
msgid "DevopsAdoption|Not adopted"
msgstr ""
......@@ -11369,6 +11384,9 @@ msgstr ""
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page."
msgstr ""
msgid "DevopsAdoption|This group has no sub-groups"
msgstr ""
msgid "DevopsAdoption|You cannot remove the group you are currently in."
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