Commit b02a9c52 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Martin Wortschack

DevOps Adoption - Allow multi-select for enabling / disabling groups

parent 13827375
......@@ -48,6 +48,7 @@ feature is available.
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
> - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2.
> - Multiselect [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2.
DevOps Adoption shows you which groups in your organization are using the most essential features of GitLab:
......@@ -66,15 +67,15 @@ DevOps Adoption shows you which groups in your organization are using the most e
- Pipelines
- Runners
To add your groups, in the top right-hand section the page, select **Add group to table**.
To add or remove your groups, in the top right-hand section the page, select **Add or remove groups**.
DevOps Adoption allows you to:
- Verify whether you are getting the return on investment that you expected from GitLab.
- Identify specific groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
- Find the groups that have adopted certain features and can provide guidance to other groups on how to use those features.
- Identify specific groups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey.
- Find the groups that have adopted certain features, and can provide guidance to other groups on how to use those features.
![DevOps Report](img/admin_devops_adoption_v14_1.png)
![DevOps Report](img/admin_devops_adoption_v14_2.png)
### Disable or enable DevOps Adoption
......
......@@ -12,6 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
> - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2.
> - Multiselect [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2.
Prerequisites:
......@@ -19,7 +20,7 @@ Prerequisites:
To access Group DevOps Adoption, go to your group and select **Analytics > DevOps Adoption**.
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
Group DevOps Adoption shows you how individual groups and subgroups within your organization use the following features:
- Dev
- Approvals
......@@ -36,16 +37,16 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your
- Pipelines
- Runners
When managing groups in the UI, you can add your sub-groups with the **Add sub-group to table**
When managing groups in the UI, you can add or remove your subgroups with the **Add or remove subgroups**
button, in the top right hand section of your Groups pages.
With DevOps Adoption you can:
- Verify whether you are getting the return on investment that you expected from GitLab.
- Identify specific sub-groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
- Find the sub-groups that have adopted certain features and can provide guidance to other sub-groups on how to use those features.
- Identify specific subgroups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey.
- Find the subgroups that have adopted certain features, and can provide guidance to other subgroups on how to use those features.
![DevOps Report](img/group_devops_adoption_v14_1.png)
![DevOps Report](img/group_devops_adoption_v14_2.png)
## Enable data processing
......@@ -59,10 +60,10 @@ GitLab requires around a minute to process it.
## What is displayed
DevOps Adoption displays feature adoption data for the given group
and any added sub-groups for the current calendar month.
and any added subgroups for the current calendar month.
Each group appears as a separate row in the table.
For each row, a feature is considered "adopted" if it has been used in a project in the given group
during the time period (including projects in any sub-groups of the given group).
during the time period (including projects in any subgroups of the given group).
## When is a feature considered adopted
......@@ -83,14 +84,14 @@ Following this guideline, GitLab doesn't penalize for:
over time, so we should not consider adoption to have decreased if GitLab adds features.
This means we should not measure adoption by percentages, only total counts.
## Add a sub-group
## Add a subgroup
DevOps Adoption can also display data for sub-groups within the given group,
DevOps Adoption can also display data for subgroups within the given group,
to show you differences in adoption across the group.
To add a sub-group to your Group DevOps Adoption report:
To add subgroups to your Group DevOps Adoption report:
1. Select **Add/remove sub-groups**.
1. Select the sub-group you want to add and select **Save changes**.
1. Select **Add or remove subgroups**.
1. Select the subgroups you want to add and select **Save changes**.
The sub-group data might not appear immediately, because GitLab requires around a minute to collect
The subgroup data might not appear immediately, because GitLab requires around a minute to collect
the data.
......@@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
DEBOUNCE_DELAY,
I18N_GROUP_DROPDOWN_TEXT,
......@@ -19,6 +19,7 @@ import {
I18N_NO_SUB_GROUPS,
} from '../constants';
import bulkEnableDevopsAdoptionNamespacesMutation from '../graphql/mutations/bulk_enable_devops_adoption_namespaces.mutation.graphql';
import disableDevopsAdoptionNamespaceMutation from '../graphql/mutations/disable_devops_adoption_namespace.mutation.graphql';
export default {
name: 'DevopsAdoptionAddDropdown',
......@@ -63,6 +64,11 @@ export default {
required: false,
default: false,
},
enabledNamespaces: {
type: Object,
required: false,
default: () => ({ nodes: [] }),
},
},
computed: {
filteredGroupsLength() {
......@@ -77,12 +83,31 @@ export default {
tooltipText() {
return this.isLoadingGroups || this.hasSubgroups ? false : I18N_NO_SUB_GROUPS;
},
enabledNamespaceIds() {
return this.enabledNamespaces.nodes.map((enabledNamespace) =>
getIdFromGraphQLId(enabledNamespace.namespace.id),
);
},
},
beforeDestroy() {
clearTimeout(this.timeout);
this.timeout = null;
},
methods: {
namespaceIdByGroupId(groupId) {
return this.enabledNamespaces.nodes?.find(
(enabledNamespace) => getIdFromGraphQLId(enabledNamespace.namespace.id) === groupId,
).id;
},
handleGroupSelect(id) {
const groupEnabled = this.isGroupEnabled(id);
if (groupEnabled) {
this.disableGroup(id);
} else {
this.enableGroup(id);
}
},
enableGroup(id) {
this.$apollo
.mutate({
......@@ -103,6 +128,28 @@ export default {
Sentry.captureException(error);
});
},
disableGroup(id) {
const gid = this.namespaceIdByGroupId(id);
this.$apollo
.mutate({
mutation: disableDevopsAdoptionNamespaceMutation,
variables: {
id: gid,
},
update: () => {
this.$emit('enabledNamespacesRemoved', gid);
},
})
.catch((error) => {
Sentry.captureException(error);
});
},
isGroupEnabled(groupId) {
return this.enabledNamespaceIds.some((namespaceId) => {
return namespaceId === groupId;
});
},
},
};
</script>
......@@ -126,8 +173,10 @@ export default {
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:is-check-item="true"
:is-checked="isGroupEnabled(group.id)"
data-testid="group-row"
@click="enableGroup(group.id)"
@click.native.capture.stop="handleGroupSelect(group.id)"
>
{{ group.full_name }}
</gl-dropdown-item>
......
......@@ -4,7 +4,6 @@ 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 {
I18N_GROUPS_QUERY_ERROR,
......@@ -150,13 +149,6 @@ export default {
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();
......@@ -328,7 +320,7 @@ export default {
:cols="tab.cols"
:enabled-namespaces="devopsAdoptionEnabledNamespaces"
:search-term="searchTerm"
:disabled-group-nodes="disabledGroupNodes"
:groups="availableGroups"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@enabledNamespacesRemoved="deleteEnabledNamespacesFromCache"
......@@ -350,11 +342,13 @@ export default {
>
<devops-adoption-add-dropdown
:search-term="searchTerm"
:groups="disabledGroupNodes"
:groups="availableGroups"
:enabled-namespaces="devopsAdoptionEnabledNamespaces"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@fetchGroups="fetchGroups"
@enabledNamespacesAdded="addEnabledNamespacesToCache"
@enabledNamespacesRemoved="deleteEnabledNamespacesFromCache"
/>
</span>
</template>
......
......@@ -42,7 +42,7 @@ export default {
required: false,
default: () => {},
},
disabledGroupNodes: {
groups: {
type: Array,
required: true,
},
......@@ -75,11 +75,13 @@ export default {
<devops-adoption-add-dropdown
class="gl-mb-3 gl-md-display-none"
:search-term="searchTerm"
:groups="disabledGroupNodes"
:groups="groups"
:enabled-namespaces="enabledNamespaces"
:is-loading-groups="isLoadingGroups"
:has-subgroups="hasSubgroups"
@fetchGroups="$emit('fetchGroups', $event)"
@enabledNamespacesAdded="$emit('enabledNamespacesAdded', $event)"
@enabledNamespacesRemoved="$emit('enabledNamespacesRemoved', $event)"
@trackModalOpenState="$emit('trackModalOpenState', $event)"
/>
</div>
......
......@@ -34,13 +34,13 @@ export const I18N_TABLE_REMOVE_BUTTON_DISABLED = s__(
'DevopsAdoption|You cannot remove the group you are currently in.',
);
export const I18N_GROUP_DROPDOWN_TEXT = s__('DevopsAdoption|Add sub-group to table');
export const I18N_GROUP_DROPDOWN_HEADER = s__('DevopsAdoption|Add sub-group');
export const I18N_ADMIN_DROPDOWN_TEXT = s__('DevopsAdoption|Add group to table');
export const I18N_ADMIN_DROPDOWN_HEADER = s__('DevopsAdoption|Add group');
export const I18N_GROUP_DROPDOWN_TEXT = s__('DevopsAdoption|Add or remove subgroups');
export const I18N_GROUP_DROPDOWN_HEADER = s__('DevopsAdoption|Edit subgroups');
export const I18N_ADMIN_DROPDOWN_TEXT = s__('DevopsAdoption|Add or remove groups');
export const I18N_ADMIN_DROPDOWN_HEADER = s__('DevopsAdoption|Edit groups');
export const I18N_NO_RESULTS = s__('DevopsAdoption|No results…');
export const I18N_NO_SUB_GROUPS = s__('DevopsAdoption|This group has no sub-groups');
export const I18N_NO_SUB_GROUPS = s__('DevopsAdoption|This group has no subgroups');
export const I18N_FEATURES_ADOPTED_TEXT = s__(
'DevopsAdoption|%{adoptedCount}/%{featuresCount} %{title} features adopted',
......
......@@ -4,7 +4,15 @@ 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 {
I18N_GROUP_DROPDOWN_TEXT,
I18N_GROUP_DROPDOWN_HEADER,
I18N_ADMIN_DROPDOWN_TEXT,
I18N_ADMIN_DROPDOWN_HEADER,
I18N_NO_SUB_GROUPS,
} from 'ee/analytics/devops_report/devops_adoption/constants';
import bulkEnableDevopsAdoptionNamespacesMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/bulk_enable_devops_adoption_namespaces.mutation.graphql';
import disableDevopsAdoptionNamespaceMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/disable_devops_adoption_namespace.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';
......@@ -18,7 +26,7 @@ import {
const localVue = createLocalVue();
Vue.use(VueApollo);
const mutate = jest.fn().mockResolvedValue({
const mutateAdd = jest.fn().mockResolvedValue({
data: {
bulkEnableDevopsAdoptionNamespaces: {
enabledNamespaces: [devopsAdoptionNamespaceData.nodes[0]],
......@@ -26,14 +34,28 @@ const mutate = jest.fn().mockResolvedValue({
},
},
});
const mutateDisable = jest.fn().mockResolvedValue({
data: {
disableDevopsAdoptionNamespace: {
errors: [],
},
},
});
const mutateWithErrors = jest.fn().mockRejectedValue(genericDeleteErrorMessage);
describe('DevopsAdoptionAddDropdown', () => {
let wrapper;
const createComponent = ({ enableNamespaceSpy = mutate, provide = {}, props = {} } = {}) => {
const createComponent = ({
enableNamespaceSpy = mutateAdd,
disableNamespaceSpy = mutateDisable,
provide = {},
props = {},
} = {}) => {
const mockApollo = createMockApollo([
[bulkEnableDevopsAdoptionNamespacesMutation, enableNamespaceSpy],
[disableDevopsAdoptionNamespaceMutation, disableNamespaceSpy],
]);
wrapper = shallowMountExtended(DevopsAdoptionAddDropdown, {
......@@ -55,7 +77,7 @@ describe('DevopsAdoptionAddDropdown', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const clickFirstRow = () => wrapper.findByTestId('group-row').vm.$emit('click');
const clickFirstRow = () => wrapper.findByTestId('group-row').trigger('click');
describe('default behaviour', () => {
beforeEach(() => {
......@@ -69,8 +91,8 @@ describe('DevopsAdoptionAddDropdown', () => {
it('displays the correct text', () => {
const dropdown = findDropdown();
expect(dropdown.props('text')).toBe('Add group to table');
expect(dropdown.props('headerText')).toBe('Add group');
expect(dropdown.props('text')).toBe(I18N_ADMIN_DROPDOWN_TEXT);
expect(dropdown.props('headerText')).toBe(I18N_ADMIN_DROPDOWN_HEADER);
});
it('is disabled', () => {
......@@ -81,7 +103,7 @@ describe('DevopsAdoptionAddDropdown', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe('This group has no sub-groups');
expect(tooltip.value).toBe(I18N_NO_SUB_GROUPS);
});
});
......@@ -91,8 +113,8 @@ describe('DevopsAdoptionAddDropdown', () => {
const dropdown = findDropdown();
expect(dropdown.props('text')).toBe('Add sub-group to table');
expect(dropdown.props('headerText')).toBe('Add sub-group');
expect(dropdown.props('text')).toBe(I18N_GROUP_DROPDOWN_TEXT);
expect(dropdown.props('headerText')).toBe(I18N_GROUP_DROPDOWN_HEADER);
});
});
......@@ -129,31 +151,47 @@ describe('DevopsAdoptionAddDropdown', () => {
describe('on row click', () => {
describe.each`
level | groupGid
${'group'} | ${groupGids[0]}
${'admin'} | ${null}
`('$level level sucessful request', ({ groupGid }) => {
level | groupGid | enabledNamespaces
${'group'} | ${groupGids[0]} | ${undefined}
${'group'} | ${groupGids[0]} | ${devopsAdoptionNamespaceData}
${'admin'} | ${null} | ${undefined}
${'admin'} | ${null} | ${devopsAdoptionNamespaceData}
`('$level level sucessful request', ({ groupGid, enabledNamespaces }) => {
beforeEach(() => {
createComponent({
props: { hasSubgroups: true, groups: groupNodes },
props: { hasSubgroups: true, groups: groupNodes, enabledNamespaces },
provide: { groupGid },
});
clickFirstRow();
});
it('makes a request to enable the selected group', () => {
expect(mutate).toHaveBeenCalledWith({
displayNamespaceId: groupGid,
namespaceIds: ['gid://gitlab/Group/1'],
if (!enabledNamespaces) {
it('makes a request to enable the selected group', () => {
expect(mutateAdd).toHaveBeenCalledWith({
displayNamespaceId: groupGid,
namespaceIds: ['gid://gitlab/Group/1'],
});
});
});
it('emits the enabledNamespacesAdded event', () => {
const [params] = wrapper.emitted().enabledNamespacesAdded[0];
it('emits the enabledNamespacesAdded event', () => {
const [params] = wrapper.emitted().enabledNamespacesAdded[0];
expect(params).toStrictEqual([devopsAdoptionNamespaceData.nodes[0]]);
});
expect(params).toStrictEqual([devopsAdoptionNamespaceData.nodes[0]]);
});
} else {
it('makes a request to disable the selected group', () => {
expect(mutateDisable).toHaveBeenCalledWith({
id: devopsAdoptionNamespaceData.nodes[0].id,
});
});
it('emits the enabledNamespacesRemoved event', () => {
const [params] = wrapper.emitted().enabledNamespacesRemoved[0];
expect(params).toBe(devopsAdoptionNamespaceData.nodes[0].id);
});
}
});
describe('on error', () => {
......
......@@ -22,7 +22,7 @@ describe('DevopsAdoptionSection', () => {
hasGroupData: true,
cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols,
enabledNamespaces: devopsAdoptionNamespaceData,
disabledGroupNodes: groupNodes,
groups: groupNodes,
searchTerm: '',
isLoadingGroups: false,
hasSubgroups: true,
......
export const groupData = [
{ id: '1', full_name: 'Foo' },
{ id: '2', full_name: 'Bar' },
{ id: 1, full_name: 'Foo' },
{ id: 2, full_name: 'Bar' },
];
export const groupNodes = [
{
__typename: 'Group',
full_name: 'Foo',
id: '1',
id: 1,
},
{
__typename: 'Group',
full_name: 'Bar',
id: '2',
id: 2,
},
];
export const groupNodeLabelValues = [
{ label: 'Foo', value: '1' },
{ label: 'Bar', value: '2' },
{ label: 'Foo', value: 1 },
{ label: 'Bar', value: 2 },
];
export const groupIds = [1, 2];
......@@ -28,7 +28,7 @@ export const groupGids = ['gid://gitlab/Group/1', 'gid://gitlab/Group/2'];
export const devopsAdoptionNamespaceData = {
nodes: [
{
id: 1,
id: 'gid://gitlab/EnabledNamespace/1',
namespace: {
fullName: 'Group 1',
id: 'gid://gitlab/Group/1',
......@@ -51,7 +51,7 @@ export const devopsAdoptionNamespaceData = {
__typename: 'devopsAdoptionEnabledNamespace',
},
{
id: 2,
id: 'gid://gitlab/EnabledNamespace/2',
namespace: {
fullName: 'Group 2',
id: 'gid://gitlab/Group/2',
......
......@@ -11337,16 +11337,10 @@ msgstr ""
msgid "DevopsAdoption|Add a group to get started"
msgstr ""
msgid "DevopsAdoption|Add group"
msgid "DevopsAdoption|Add or remove groups"
msgstr ""
msgid "DevopsAdoption|Add group to table"
msgstr ""
msgid "DevopsAdoption|Add sub-group"
msgstr ""
msgid "DevopsAdoption|Add sub-group to table"
msgid "DevopsAdoption|Add or remove subgroups"
msgstr ""
msgid "DevopsAdoption|Adopted"
......@@ -11406,6 +11400,12 @@ msgstr ""
msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
msgstr ""
msgid "DevopsAdoption|Edit groups"
msgstr ""
msgid "DevopsAdoption|Edit subgroups"
msgstr ""
msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}."
msgstr ""
......@@ -11466,7 +11466,7 @@ msgstr ""
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page."
msgstr ""
msgid "DevopsAdoption|This group has no sub-groups"
msgid "DevopsAdoption|This group has no subgroups"
msgstr ""
msgid "DevopsAdoption|You cannot remove the group you are currently in."
......
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