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 ...@@ -58,7 +58,8 @@ The DevOps Adoption tab shows you which groups within your organization are usin
- Pipelines - Pipelines
- Deployments - 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: DevOps Adoption allows you to:
......
...@@ -36,7 +36,7 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your ...@@ -36,7 +36,7 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your
- Pipelines - Pipelines
- Deployments - 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. button, in the top right hand section of your Groups pages.
With DevOps Adoption you can: 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'; ...@@ -4,11 +4,11 @@ import * as Sentry from '@sentry/browser';
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue'; import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import API from '~/api'; import API from '~/api';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { import {
DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_ERROR_KEYS, DEVOPS_ADOPTION_ERROR_KEYS,
MAX_REQUEST_COUNT,
DATE_TIME_FORMAT, DATE_TIME_FORMAT,
DEFAULT_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL,
DEVOPS_ADOPTION_GROUP_LEVEL_LABEL, DEVOPS_ADOPTION_GROUP_LEVEL_LABEL,
...@@ -21,15 +21,15 @@ import devopsAdoptionEnabledNamespacesQuery from '../graphql/queries/devops_adop ...@@ -21,15 +21,15 @@ import devopsAdoptionEnabledNamespacesQuery from '../graphql/queries/devops_adop
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql'; import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates'; import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates';
import { shouldPollTableData } from '../utils/helpers'; import { shouldPollTableData } from '../utils/helpers';
import DevopsAdoptionAddDropdown from './devops_adoption_add_dropdown.vue';
import DevopsAdoptionSection from './devops_adoption_section.vue'; import DevopsAdoptionSection from './devops_adoption_section.vue';
import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
export default { export default {
name: 'DevopsAdoptionApp', name: 'DevopsAdoptionApp',
components: { components: {
GlAlert, GlAlert,
DevopsAdoptionAddDropdown,
DevopsAdoptionSection, DevopsAdoptionSection,
DevopsAdoptionSegmentModal,
DevopsScore, DevopsScore,
GlTabs, GlTabs,
GlTab, GlTab,
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION, devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION,
data() { data() {
return { return {
hasSubgroups: undefined,
isLoadingGroups: false, isLoadingGroups: false,
isLoadingEnableGroup: false, isLoadingEnableGroup: false,
requestCount: 0, requestCount: 0,
...@@ -138,11 +139,6 @@ export default { ...@@ -138,11 +139,6 @@ export default {
this.isLoadingEnableGroup || this.$apollo.queries.devopsAdoptionEnabledNamespaces.loading this.isLoadingEnableGroup || this.$apollo.queries.devopsAdoptionEnabledNamespaces.loading
); );
}, },
editGroupsButtonLabel() {
return this.isGroup
? this.$options.i18n.groupLevelLabel
: this.$options.i18n.tableHeader.button;
},
tabIndexValues() { tabIndexValues() {
const tabs = this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab); const tabs = this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab);
...@@ -151,13 +147,21 @@ export default { ...@@ -151,13 +147,21 @@ export default {
availableGroups() { availableGroups() {
return this.groups?.nodes || []; return this.groups?.nodes || [];
}, },
enabledGroups() { enabledNamespaces() {
return this.devopsAdoptionEnabledNamespaces?.nodes || []; 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() { created() {
this.fetchGroups(); this.fetchGroups();
this.selectTab(); this.selectTab();
this.startPollingTableData();
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.pollingTableData); clearInterval(this.pollingTableData);
...@@ -215,8 +219,10 @@ export default { ...@@ -215,8 +219,10 @@ export default {
this.errors[key] = true; this.errors[key] = true;
Sentry.captureException(error); Sentry.captureException(error);
}, },
fetchGroups(nextPage) { fetchGroups(searchTerm = '') {
this.searchTerm = searchTerm;
this.isLoadingGroups = true; this.isLoadingGroups = true;
this.$apollo this.$apollo
.query({ .query({
query: getGroupsQuery, query: getGroupsQuery,
...@@ -224,25 +230,17 @@ export default { ...@@ -224,25 +230,17 @@ export default {
isSingleRequest: true, isSingleRequest: true,
}, },
variables: { variables: {
nextPage, search: searchTerm,
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
const { pageInfo, nodes } = data.groups; this.groups = data.groups;
// Update data if (this.hasSubgroups === undefined) {
this.groups = { this.hasSubgroups = this.groups?.nodes?.length > 0;
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();
} }
this.isLoadingGroups = false;
}) })
.catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error)); .catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error));
}, },
...@@ -317,11 +315,16 @@ export default { ...@@ -317,11 +315,16 @@ export default {
:has-segments-data="hasSegmentsData" :has-segments-data="hasSegmentsData"
:timestamp="timestamp" :timestamp="timestamp"
:has-group-data="hasGroupData" :has-group-data="hasGroupData"
:edit-groups-button-label="editGroupsButtonLabel"
:cols="tab.cols" :cols="tab.cols"
:segments="devopsAdoptionEnabledNamespaces" :segments="devopsAdoptionEnabledNamespaces"
:search-term="searchTerm"
:disabled-group-nodes="disabledGroupNodes"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@segmentsRemoved="deleteSegmentsFromCache" @segmentsRemoved="deleteSegmentsFromCache"
@openAddRemoveModal="openAddRemoveModal" @fetchGroups="fetchGroups"
@segmentsAdded="addSegmentsToCache"
@trackModalOpenState="trackModalOpenState"
/> />
</gl-tab> </gl-tab>
...@@ -329,17 +332,22 @@ export default { ...@@ -329,17 +332,22 @@ export default {
<template #title>{{ s__('DevopsReport|DevOps Score') }}</template> <template #title>{{ s__('DevopsReport|DevOps Score') }}</template>
<devops-score /> <devops-score />
</gl-tab> </gl-tab>
</gl-tabs>
<devops-adoption-segment-modal <template #tabs-end>
v-if="!hasLoadingError" <span
ref="addRemoveModal" class="nav-item gl-align-self-center gl-flex-fill-1 gl-display-none gl-md-display-block"
:groups="availableGroups" align="right"
:enabled-groups="enabledGroups" >
:is-loading="isLoading" <devops-adoption-add-dropdown
@segmentsAdded="addSegmentsToCache" :search-term="searchTerm"
@segmentsRemoved="deleteSegmentsFromCache" :groups="disabledGroupNodes"
@trackModalOpenState="trackModalOpenState" :is-loading-groups="isLoadingGroups"
/> :has-subgroups="hasSubgroups"
@fetchGroups="fetchGroups"
@segmentsAdded="addSegmentsToCache"
/>
</span>
</template>
</gl-tabs>
</div> </div>
</template> </template>
<script> <script>
import { GlLoadingIcon, GlButton, GlSprintf } from '@gitlab/ui'; import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { TABLE_HEADER_TEXT } from '../constants'; import { TABLE_HEADER_TEXT } from '../constants';
import DevopsAdoptionAddDropdown from './devops_adoption_add_dropdown.vue';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue'; import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue'; import DevopsAdoptionTable from './devops_adoption_table.vue';
...@@ -8,9 +9,9 @@ export default { ...@@ -8,9 +9,9 @@ export default {
components: { components: {
DevopsAdoptionTable, DevopsAdoptionTable,
GlLoadingIcon, GlLoadingIcon,
GlButton,
GlSprintf, GlSprintf,
DevopsAdoptionEmptyState, DevopsAdoptionEmptyState,
DevopsAdoptionAddDropdown,
}, },
i18n: { i18n: {
tableHeaderText: TABLE_HEADER_TEXT, tableHeaderText: TABLE_HEADER_TEXT,
...@@ -32,10 +33,6 @@ export default { ...@@ -32,10 +33,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
editGroupsButtonLabel: {
type: String,
required: true,
},
cols: { cols: {
type: Array, type: Array,
required: true, required: true,
...@@ -45,25 +42,46 @@ export default { ...@@ -45,25 +42,46 @@ export default {
required: false, required: false,
default: () => {}, default: () => {},
}, },
disabledGroupNodes: {
type: Array,
required: true,
},
searchTerm: {
type: String,
required: true,
},
isLoadingGroups: {
type: Boolean,
required: true,
},
hasSubgroups: {
type: Boolean,
required: false,
default: false,
},
}, },
}; };
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoading" size="md" class="gl-my-5" /> <gl-loading-icon v-if="isLoading" size="md" class="gl-my-5" />
<div v-else-if="hasSegmentsData" class="gl-mt-3"> <div v-else-if="hasSegmentsData" class="gl-mt-3">
<div <div class="gl-my-3" data-testid="tableHeader">
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-my-3"
data-testid="tableHeader"
>
<span class="gl-text-gray-400"> <span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.tableHeaderText"> <gl-sprintf :message="$options.i18n.tableHeaderText">
<template #timestamp>{{ timestamp }}</template> <template #timestamp>{{ timestamp }}</template>
</gl-sprintf> </gl-sprintf>
</span> </span>
<gl-button v-if="hasGroupData" @click="$emit('openAddRemoveModal')">{{ <devops-adoption-add-dropdown
editGroupsButtonLabel class="gl-mt-4 gl-mb-3 gl-md-display-none"
}}</gl-button> :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> </div>
<devops-adoption-table <devops-adoption-table
:cols="cols" :cols="cols"
......
...@@ -2,9 +2,9 @@ import { s__, __ } from '~/locale'; ...@@ -2,9 +2,9 @@ import { s__, __ } from '~/locale';
export const DEFAULT_POLLING_INTERVAL = 30000; 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'; export const DEVOPS_ADOPTION_SEGMENT_MODAL_ID = 'devopsSegmentModal';
...@@ -28,6 +28,15 @@ export const DEVOPS_ADOPTION_TABLE_REMOVE_BUTTON_DISABLED = s__( ...@@ -28,6 +28,15 @@ export const DEVOPS_ADOPTION_TABLE_REMOVE_BUTTON_DISABLED = s__(
'DevopsAdoption|You cannot remove the group you are currently in.', '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 = { export const DEVOPS_ADOPTION_STRINGS = {
app: { app: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: s__( [DEVOPS_ADOPTION_ERROR_KEYS.groups]: s__(
......
...@@ -9,7 +9,7 @@ Vue.use(VueApollo); ...@@ -9,7 +9,7 @@ Vue.use(VueApollo);
export const createResolvers = (groupId) => ({ export const createResolvers = (groupId) => ({
Query: { Query: {
groups(_, { search, nextPage }) { groups(_, { search }) {
const url = groupId const url = groupId
? Api.buildUrl(Api.subgroupsPath).replace(':id', groupId) ? Api.buildUrl(Api.subgroupsPath).replace(':id', groupId)
: Api.buildUrl(Api.groupsPath); : Api.buildUrl(Api.groupsPath);
...@@ -17,20 +17,13 @@ export const createResolvers = (groupId) => ({ ...@@ -17,20 +17,13 @@ export const createResolvers = (groupId) => ({
per_page: PER_PAGE, per_page: PER_PAGE,
search, search,
}; };
if (nextPage) {
params.page = nextPage;
}
return axios.get(url, { params }).then(({ data, headers }) => { return axios.get(url, { params }).then(({ data }) => {
const pageInfo = {
nextPage: headers['x-next-page'],
};
const groups = { const groups = {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Groups', __typename: 'Groups',
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
nodes: data.map((group) => ({ ...group, __typename: 'Group' })), nodes: data.map((group) => ({ ...group, __typename: 'Group' })),
pageInfo,
}; };
return groups; return groups;
......
query getGroups($search: String, $nextPage: String) { query getGroups($search: String) {
groups(search: $search, nextPage: $nextPage) @client groups(search: $search) @client
} }
...@@ -54,7 +54,7 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -54,7 +54,7 @@ RSpec.describe 'DevOps Report page', :js do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
within tabs_selector do 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' expect(page).to have_text 'Dev Sec Ops DevOps Score'
end end
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 * as Sentry from '@sentry/browser';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; 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 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 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'; 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'; ...@@ -22,7 +23,6 @@ import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import API from '~/api'; import API from '~/api';
import { import {
groupNodes, groupNodes,
nextGroupNode,
groupPageInfo, groupPageInfo,
devopsAdoptionNamespaceData, devopsAdoptionNamespaceData,
devopsAdoptionNamespaceDataEmpty, devopsAdoptionNamespaceDataEmpty,
...@@ -95,6 +95,9 @@ describe('DevopsAdoptionApp', () => { ...@@ -95,6 +95,9 @@ describe('DevopsAdoptionApp', () => {
data() { data() {
return data; return data;
}, },
stubs: {
GlTabs,
},
}); });
} }
...@@ -104,7 +107,7 @@ describe('DevopsAdoptionApp', () => { ...@@ -104,7 +107,7 @@ describe('DevopsAdoptionApp', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('initial request', () => { describe('group data request', () => {
let groupsSpy; let groupsSpy;
afterEach(() => { afterEach(() => {
...@@ -119,16 +122,12 @@ describe('DevopsAdoptionApp', () => { ...@@ -119,16 +122,12 @@ describe('DevopsAdoptionApp', () => {
await waitForPromises(); await waitForPromises();
}); });
it('renders the segment modal', () => {
expect(wrapper.find(DevopsAdoptionSegmentModal).exists()).toBe(true);
});
it('should fetch data once', () => { it('should fetch data once', () => {
expect(groupsSpy).toHaveBeenCalledTimes(1); 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!'); const error = new Error('foo!');
beforeEach(async () => { beforeEach(async () => {
...@@ -152,98 +151,6 @@ describe('DevopsAdoptionApp', () => { ...@@ -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('segments data', () => {
describe('when there is no active group', () => { describe('when there is no active group', () => {
beforeEach(async () => { beforeEach(async () => {
...@@ -458,6 +365,10 @@ describe('DevopsAdoptionApp', () => { ...@@ -458,6 +365,10 @@ describe('DevopsAdoptionApp', () => {
).toBe(true); ).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'); 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 { getByText } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils'; 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 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 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 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 { DEVOPS_ADOPTION_TABLE_CONFIGURATION } from 'ee/analytics/devops_report/devops_adoption/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { devopsAdoptionNamespaceData } from '../mock_data'; import { devopsAdoptionNamespaceData, groupNodes } from '../mock_data';
describe('DevopsAdoptionSection', () => { describe('DevopsAdoptionSection', () => {
let wrapper; let wrapper;
...@@ -19,9 +20,12 @@ describe('DevopsAdoptionSection', () => { ...@@ -19,9 +20,12 @@ describe('DevopsAdoptionSection', () => {
hasSegmentsData: true, hasSegmentsData: true,
timestamp: '2020-10-31 23:59', timestamp: '2020-10-31 23:59',
hasGroupData: true, hasGroupData: true,
editGroupsButtonLabel: 'Add/Remove groups',
cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols, cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols,
segments: devopsAdoptionNamespaceData, segments: devopsAdoptionNamespaceData,
disabledGroupNodes: groupNodes,
searchTerm: '',
isLoadingGroups: false,
hasSubgroups: true,
...props, ...props,
}, },
stubs: { stubs: {
...@@ -35,7 +39,7 @@ describe('DevopsAdoptionSection', () => { ...@@ -35,7 +39,7 @@ describe('DevopsAdoptionSection', () => {
const findTableHeaderSection = () => wrapper.findByTestId('tableHeader'); const findTableHeaderSection = () => wrapper.findByTestId('tableHeader');
const findTable = () => wrapper.findComponent(DevopsAdoptionTable); const findTable = () => wrapper.findComponent(DevopsAdoptionTable);
const findEmptyState = () => wrapper.findComponent(DevopsAdoptionEmptyState); const findEmptyState = () => wrapper.findComponent(DevopsAdoptionEmptyState);
const findAddEditButton = () => wrapper.findComponent(GlButton); const findDropdown = () => wrapper.findComponent(DevopsAdoptionAddDropdown);
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
...@@ -96,40 +100,10 @@ describe('DevopsAdoptionSection', () => { ...@@ -96,40 +100,10 @@ describe('DevopsAdoptionSection', () => {
expect(getByText(wrapper.element, text)).not.toBeNull(); expect(getByText(wrapper.element, text)).not.toBeNull();
}); });
describe('with group data', () => { it('displays the add groups dropdown', () => {
it('displays the edit groups button', () => { createComponent();
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('does not display the edit groups button', () => { expect(findDropdown().exists()).toBe(true);
expect(findAddEditButton().exists()).toBe(false);
});
}); });
}); });
}); });
...@@ -5,7 +5,7 @@ import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/q ...@@ -5,7 +5,7 @@ import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/q
import Api from 'ee/api'; import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; 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 fetchGroupsUrl = Api.buildUrl(Api.groupsPath);
const fetchSubGroupsUrl = Api.buildUrl(Api.subgroupsPath).replace(':id', 1); const fetchSubGroupsUrl = Api.buildUrl(Api.subgroupsPath).replace(':id', 1);
...@@ -29,7 +29,7 @@ describe('DevOps GraphQL resolvers', () => { ...@@ -29,7 +29,7 @@ describe('DevOps GraphQL resolvers', () => {
}); });
it('fetches all relevent groups / subgroups', async () => { 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 }); await mockClient.query({ query: getGroupsQuery });
expect(mockAdapter.history.get[0].params).not.toEqual( expect(mockAdapter.history.get[0].params).not.toEqual(
...@@ -38,40 +38,25 @@ describe('DevOps GraphQL resolvers', () => { ...@@ -38,40 +38,25 @@ describe('DevOps GraphQL resolvers', () => {
}); });
it('when receiving groups data', async () => { 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 }); const result = await mockClient.query({ query: getGroupsQuery });
expect(result.data).toEqual({ expect(result.data).toEqual({
groups: { groups: {
__typename: 'Groups', __typename: 'Groups',
nodes: groupNodes, nodes: groupNodes,
pageInfo: groupPageInfo,
}, },
}); });
}); });
it('when receiving empty groups data', async () => { 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 }); const result = await mockClient.query({ query: getGroupsQuery });
expect(result.data).toEqual({ expect(result.data).toEqual({
groups: { groups: {
__typename: 'Groups', __typename: 'Groups',
nodes: [], 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 = [ ...@@ -3,10 +3,6 @@ export const groupData = [
{ id: '2', full_name: 'Bar' }, { id: '2', full_name: 'Bar' },
]; ];
export const pageData = {
'x-next-page': 2,
};
export const groupNodes = [ export const groupNodes = [
{ {
__typename: 'Group', __typename: 'Group',
...@@ -29,12 +25,6 @@ export const groupIds = [1, 2]; ...@@ -29,12 +25,6 @@ export const groupIds = [1, 2];
export const groupGids = ['gid://gitlab/Group/1', 'gid://gitlab/Group/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 = { export const groupPageInfo = {
nextPage: 2, nextPage: 2,
}; };
......
...@@ -11255,6 +11255,18 @@ msgstr "" ...@@ -11255,6 +11255,18 @@ msgstr ""
msgid "DevopsAdoption|Add a group to get started" msgid "DevopsAdoption|Add a group to get started"
msgstr "" 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" msgid "DevopsAdoption|Add/remove groups"
msgstr "" msgstr ""
...@@ -11330,6 +11342,9 @@ msgstr "" ...@@ -11330,6 +11342,9 @@ msgstr ""
msgid "DevopsAdoption|No filter results." msgid "DevopsAdoption|No filter results."
msgstr "" msgstr ""
msgid "DevopsAdoption|No results…"
msgstr ""
msgid "DevopsAdoption|Not adopted" msgid "DevopsAdoption|Not adopted"
msgstr "" msgstr ""
...@@ -11369,6 +11384,9 @@ msgstr "" ...@@ -11369,6 +11384,9 @@ msgstr ""
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page." msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page."
msgstr "" msgstr ""
msgid "DevopsAdoption|This group has no sub-groups"
msgstr ""
msgid "DevopsAdoption|You cannot remove the group you are currently in." msgid "DevopsAdoption|You cannot remove the group you are currently in."
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