Commit 160454be authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '330401-devops-adoption-show-total-number-of-features-adopted' into 'master'

DevOps Adoption - Show total number of features adopted

See merge request gitlab-org/gitlab!64484
parents 63ab8014 1c1117bd
...@@ -38,13 +38,14 @@ collected before this feature is available. ...@@ -38,13 +38,14 @@ collected before this feature is available.
## DevOps Adoption **(ULTIMATE SELF)** ## DevOps Adoption **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247112) in GitLab 13.7 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247112) in GitLab 13.7 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
> - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. > - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59267) in GitLab 14.0. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59267) in GitLab 14.0.
> - Enabled on GitLab.com. > - Enabled on GitLab.com.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-or-enable-devops-adoption). **(ULTIMATE SELF)** > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-or-enable-devops-adoption). **(ULTIMATE SELF)**
The DevOps Adoption tab shows you which groups within your organization are using the most essential features of GitLab: DevOps Adoption shows you which groups within your organization are using the most essential features of GitLab:
- Dev - Dev
- Issues - Issues
...@@ -67,7 +68,7 @@ DevOps Adoption allows you to: ...@@ -67,7 +68,7 @@ DevOps Adoption allows you to:
- Identify specific groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey. - 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. - 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_0.png) ![DevOps Report](img/admin_devops_adoption_v14_1.png)
### Disable or enable DevOps Adoption ### Disable or enable DevOps Adoption
......
...@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta). > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/333556) in GitLab 14.1. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/333556) in GitLab 14.1.
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
Prerequisites: Prerequisites:
...@@ -38,7 +39,7 @@ With DevOps Adoption you can: ...@@ -38,7 +39,7 @@ With DevOps Adoption you can:
- Identify specific sub-groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey. - 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. - Find the sub-groups that have adopted certain features and can provide guidance to other sub-groups on how to use those features.
![DevOps Report](img/group_devops_adoption_v14_0.png) ![DevOps Report](img/group_devops_adoption_v14_1.png)
## Enable data processing ## Enable data processing
......
...@@ -22,6 +22,7 @@ import getGroupsQuery from '../graphql/queries/get_groups.query.graphql'; ...@@ -22,6 +22,7 @@ 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 DevopsAdoptionAddDropdown from './devops_adoption_add_dropdown.vue';
import DevopsAdoptionOverview from './devops_adoption_overview.vue';
import DevopsAdoptionSection from './devops_adoption_section.vue'; import DevopsAdoptionSection from './devops_adoption_section.vue';
export default { export default {
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
GlAlert, GlAlert,
DevopsAdoptionAddDropdown, DevopsAdoptionAddDropdown,
DevopsAdoptionSection, DevopsAdoptionSection,
DevopsAdoptionOverview,
DevopsScore, DevopsScore,
GlTabs, GlTabs,
GlTab, GlTab,
...@@ -140,7 +142,10 @@ export default { ...@@ -140,7 +142,10 @@ export default {
); );
}, },
tabIndexValues() { tabIndexValues() {
const tabs = this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab); const tabs = [
'overview',
...this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab),
];
return this.isGroup ? tabs : [...tabs, 'devops-score']; return this.isGroup ? tabs : [...tabs, 'devops-score'];
}, },
...@@ -295,6 +300,15 @@ export default { ...@@ -295,6 +300,15 @@ export default {
<template> <template>
<div> <div>
<gl-tabs :value="selectedTab" @input="onTabChange"> <gl-tabs :value="selectedTab" @input="onTabChange">
<gl-tab data-testid="devops-overview-tab">
<template #title>{{ s__('DevopsReport|Overview') }}</template>
<devops-adoption-overview
:loading="isLoadingAdoptionData"
:data="devopsAdoptionEnabledNamespaces"
:timestamp="timestamp"
/>
</gl-tab>
<gl-tab <gl-tab
v-for="tab in $options.devopsAdoptionTableConfiguration" v-for="tab in $options.devopsAdoptionTableConfiguration"
:key="tab.title" :key="tab.title"
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import {
DEVOPS_ADOPTION_TABLE_CONFIGURATION,
DEVOPS_ADOPTION_OVERALL_CONFIGURATION,
TABLE_HEADER_TEXT,
} from '../constants';
import DevopsAdoptionOverviewCard from './devops_adoption_overview_card.vue';
export default {
name: 'DevopsAdoptionOverview',
components: {
DevopsAdoptionOverviewCard,
GlLoadingIcon,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
data: {
type: Object,
required: false,
default: () => ({}),
},
timestamp: {
type: String,
required: true,
},
},
computed: {
featuresData() {
return DEVOPS_ADOPTION_TABLE_CONFIGURATION.map((item) => ({
...item,
featureMeta: item.cols.map((feature) => ({
title: feature.label,
adopted: this.data.nodes?.some((node) =>
node.latestSnapshot ? node.latestSnapshot[feature.key] : false,
),
})),
}));
},
overallData() {
return {
...DEVOPS_ADOPTION_OVERALL_CONFIGURATION,
featureMeta: this.featuresData.reduce(
(features, section) => [...features, ...section.featureMeta],
[],
),
displayMeta: false,
};
},
overviewData() {
return [this.overallData, ...this.featuresData];
},
headerText() {
return sprintf(TABLE_HEADER_TEXT, { timestamp: this.timestamp });
},
},
};
</script>
<template>
<gl-loading-icon v-if="loading" size="md" class="gl-mt-5" />
<div v-else data-testid="overview-container">
<p class="gl-text-gray-400 gl-my-3" data-testid="overview-container-header">{{ headerText }}</p>
<div
class="gl-display-flex gl-justify-content-space-between gl-flex-direction-column gl-md-flex-direction-row gl-mt-5"
>
<devops-adoption-overview-card
v-for="item in overviewData"
:key="item.title"
class="gl-mb-5"
:icon="item.icon"
:title="item.title"
:variant="item.variant"
:feature-meta="item.featureMeta"
:display-meta="item.displayMeta"
/>
</div>
</div>
</template>
<script>
import { GlIcon, GlProgressBar } from '@gitlab/ui';
import { sprintf } from '~/locale';
import {
DEVOPS_ADOPTION_FEATURES_ADOPTED_TEXT,
DEVOPS_ADOPTION_PROGRESS_BAR_HEIGHT,
} from '../constants';
import DevopsAdoptionTableCellFlag from './devops_adoption_table_cell_flag.vue';
export default {
name: 'DevopsAdoptionOverviewCard',
progressBarHeight: DEVOPS_ADOPTION_PROGRESS_BAR_HEIGHT,
components: {
GlIcon,
GlProgressBar,
DevopsAdoptionTableCellFlag,
},
props: {
icon: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
variant: {
type: String,
required: false,
default: 'primary',
},
featureMeta: {
type: Array,
required: false,
default: () => [],
},
displayMeta: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
featuresCount() {
return this.featureMeta.length;
},
adoptedCount() {
return this.featureMeta.filter((feature) => feature.adopted).length;
},
description() {
return sprintf(DEVOPS_ADOPTION_FEATURES_ADOPTED_TEXT, {
adoptedCount: this.adoptedCount,
featuresCount: this.featuresCount,
title: this.displayMeta ? this.title : '',
});
},
},
};
</script>
<template>
<div
class="devops-overview-card gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-md-mr-5 gl-mb-4"
>
<div class="gl-display-flex gl-align-items-center gl-mb-3" data-testid="card-title">
<gl-icon :name="icon" class="gl-mr-3 gl-text-gray-500" />
<span class="gl-font-md gl-font-weight-bold" data-testid="card-title-text">{{ title }}</span>
</div>
<gl-progress-bar
:value="adoptedCount"
:max="featuresCount"
class="gl-mb-2 gl-md-mr-5"
:variant="variant"
:height="$options.progressBarHeight"
/>
<div class="gl-text-gray-400 gl-mb-1" data-testid="card-description">{{ description }}</div>
<template v-if="displayMeta">
<div
v-for="feature in featureMeta"
:key="feature.title"
class="gl-display-flex gl-align-items-center gl-mt-2"
data-testid="card-meta-row"
>
<devops-adoption-table-cell-flag
:enabled="feature.adopted"
:variant="variant"
class="gl-mr-3"
/>
<span class="gl-text-gray-600 gl-font-sm" data-testid="card-meta-row-title">{{
feature.title
}}</span>
</div>
</template>
</div>
</template>
...@@ -65,15 +65,15 @@ export default { ...@@ -65,15 +65,15 @@ export default {
<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 class="gl-my-3" data-testid="tableHeader"> <div class="gl-mb-3" data-testid="tableHeader">
<span class="gl-text-gray-400"> <p 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> </p>
<devops-adoption-add-dropdown <devops-adoption-add-dropdown
class="gl-mt-4 gl-mb-3 gl-md-display-none" class="gl-mb-3 gl-md-display-none"
:search-term="searchTerm" :search-term="searchTerm"
:groups="disabledGroupNodes" :groups="disabledGroupNodes"
:is-loading-groups="isLoadingGroups" :is-loading-groups="isLoadingGroups"
......
...@@ -6,6 +6,8 @@ export const PER_PAGE = 20; ...@@ -6,6 +6,8 @@ export const PER_PAGE = 20;
export const DEBOUNCE_DELAY = 500; export const DEBOUNCE_DELAY = 500;
export const DEVOPS_ADOPTION_PROGRESS_BAR_HEIGHT = '8px';
export const DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID = 'devopsSegmentDeleteModal'; export const DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID = 'devopsSegmentDeleteModal';
export const DATE_TIME_FORMAT = 'yyyy-mm-dd HH:MM'; export const DATE_TIME_FORMAT = 'yyyy-mm-dd HH:MM';
...@@ -35,6 +37,10 @@ export const DEVOPS_ADOPTION_NO_RESULTS = s__('DevopsAdoption|No results…'); ...@@ -35,6 +37,10 @@ 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_NO_SUB_GROUPS = s__('DevopsAdoption|This group has no sub-groups');
export const DEVOPS_ADOPTION_FEATURES_ADOPTED_TEXT = s__(
'DevopsAdoption|%{adoptedCount}/%{featuresCount} %{title} features adopted',
);
export const DEVOPS_ADOPTION_STRINGS = { export const DEVOPS_ADOPTION_STRINGS = {
app: { app: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: s__( [DEVOPS_ADOPTION_ERROR_KEYS.groups]: s__(
...@@ -100,23 +106,20 @@ export const DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_DESC_STORAGE_KEY = ...@@ -100,23 +106,20 @@ export const DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_DESC_STORAGE_KEY =
export const DEVOPS_ADOPTION_GROUP_COL_LABEL = __('Group'); export const DEVOPS_ADOPTION_GROUP_COL_LABEL = __('Group');
export const DEVOPS_ADOPTION_OVERALL_CONFIGURATION = {
title: s__('DevopsAdoption|Overall adoption'),
icon: 'tanuki',
variant: 'primary',
cols: [],
};
export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
{ {
title: s__('DevopsAdoption|Dev'), title: s__('DevopsAdoption|Dev'),
tab: 'dev', tab: 'dev',
icon: 'code',
variant: 'warning',
cols: [ cols: [
{
key: 'issueOpened',
label: s__('DevopsAdoption|Issues'),
tooltip: s__('DevopsAdoption|At least one issue opened'),
testId: 'issuesCol',
},
{
key: 'mergeRequestOpened',
label: s__('DevopsAdoption|MRs'),
tooltip: s__('DevopsAdoption|At least one MR opened'),
testId: 'mrsCol',
},
{ {
key: 'mergeRequestApproved', key: 'mergeRequestApproved',
label: s__('DevopsAdoption|Approvals'), label: s__('DevopsAdoption|Approvals'),
...@@ -129,11 +132,25 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ ...@@ -129,11 +132,25 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
tooltip: s__('DevopsAdoption|Code owners enabled for at least one project'), tooltip: s__('DevopsAdoption|Code owners enabled for at least one project'),
testId: 'codeownersCol', testId: 'codeownersCol',
}, },
{
key: 'issueOpened',
label: s__('DevopsAdoption|Issues'),
tooltip: s__('DevopsAdoption|At least one issue opened'),
testId: 'issuesCol',
},
{
key: 'mergeRequestOpened',
label: s__('DevopsAdoption|MRs'),
tooltip: s__('DevopsAdoption|At least one MR opened'),
testId: 'mrsCol',
},
], ],
}, },
{ {
title: s__('DevopsAdoption|Sec'), title: s__('DevopsAdoption|Sec'),
tab: 'sec', tab: 'sec',
icon: 'shield',
variant: 'info',
cols: [ cols: [
{ {
key: 'securityScanSucceeded', key: 'securityScanSucceeded',
...@@ -146,12 +163,14 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ ...@@ -146,12 +163,14 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
{ {
title: s__('DevopsAdoption|Ops'), title: s__('DevopsAdoption|Ops'),
tab: 'ops', tab: 'ops',
icon: 'rocket',
variant: 'success',
cols: [ cols: [
{ {
key: 'runnerConfigured', key: 'deploySucceeded',
label: s__('DevopsAdoption|Runners'), label: s__('DevopsAdoption|Deploys'),
tooltip: s__('DevopsAdoption|Runner configured for project/group'), tooltip: s__('DevopsAdoption|At least one deploy'),
testId: 'runnersCol', testId: 'deploysCol',
}, },
{ {
key: 'pipelineSucceeded', key: 'pipelineSucceeded',
...@@ -160,10 +179,10 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ ...@@ -160,10 +179,10 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
testId: 'pipelinesCol', testId: 'pipelinesCol',
}, },
{ {
key: 'deploySucceeded', key: 'runnerConfigured',
label: s__('DevopsAdoption|Deploys'), label: s__('DevopsAdoption|Runners'),
tooltip: s__('DevopsAdoption|At least one deploy'), tooltip: s__('DevopsAdoption|Runner configured for project/group'),
testId: 'deploysCol', testId: 'runnersCol',
}, },
], ],
}, },
......
...@@ -54,16 +54,16 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -54,16 +54,16 @@ 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(5) expect(page.all(:css, tab_item_selector).length).to be(6)
expect(page).to have_text 'Dev Sec Ops DevOps Score' expect(page).to have_text 'Overview Dev Sec Ops DevOps Score'
end end
end end
it 'defaults to the Dev tab' do it 'defaults to the Overview tab' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
within tabs_selector do within tabs_selector do
expect(page).to have_selector active_tab_selector, text: 'Dev' expect(page).to have_selector active_tab_selector, text: 'Overview'
end end
end end
...@@ -83,10 +83,12 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -83,10 +83,12 @@ RSpec.describe 'DevOps Report page', :js do
it_behaves_like 'displays tab content', tab[:text] it_behaves_like 'displays tab content', tab[:text]
end end
it 'does not add the tab param when the Dev tab is selected' do it 'does not add the tab param when the Overview tab is selected' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
click_link 'Dev' within tabs_selector do
click_link 'Overview'
end
expect(page).to have_current_path(admin_dev_ops_report_path) expect(page).to have_current_path(admin_dev_ops_report_path)
end end
......
...@@ -5,6 +5,7 @@ import Vue from 'vue'; ...@@ -5,6 +5,7 @@ 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 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 DevopsAdoptionOverview from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_overview.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 { import {
DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_STRINGS,
...@@ -101,6 +102,7 @@ describe('DevopsAdoptionApp', () => { ...@@ -101,6 +102,7 @@ describe('DevopsAdoptionApp', () => {
} }
const findDevopsScoreTab = () => wrapper.findByTestId('devops-score-tab'); const findDevopsScoreTab = () => wrapper.findByTestId('devops-score-tab');
const findOverviewTab = () => wrapper.findByTestId('devops-overview-tab');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -142,7 +144,7 @@ describe('DevopsAdoptionApp', () => { ...@@ -142,7 +144,7 @@ describe('DevopsAdoptionApp', () => {
}); });
it('displays the error message and calls Sentry', () => { it('displays the error message and calls Sentry', () => {
const alert = wrapper.find(GlAlert); const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.groupsError); expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.groupsError);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(error); expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(error);
...@@ -239,11 +241,11 @@ describe('DevopsAdoptionApp', () => { ...@@ -239,11 +241,11 @@ describe('DevopsAdoptionApp', () => {
}); });
it('does not render the devops section', () => { it('does not render the devops section', () => {
expect(wrapper.find(DevopsAdoptionSection).exists()).toBe(false); expect(wrapper.findComponent(DevopsAdoptionSection).exists()).toBe(false);
}); });
it('displays the error message ', () => { it('displays the error message ', () => {
const alert = wrapper.find(GlAlert); const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.addSegmentsError); expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.addSegmentsError);
}); });
...@@ -270,11 +272,11 @@ describe('DevopsAdoptionApp', () => { ...@@ -270,11 +272,11 @@ describe('DevopsAdoptionApp', () => {
}); });
it('does not render the devops section', () => { it('does not render the devops section', () => {
expect(wrapper.find(DevopsAdoptionSection).exists()).toBe(false); expect(wrapper.findComponent(DevopsAdoptionSection).exists()).toBe(false);
}); });
it('displays the error message ', () => { it('displays the error message ', () => {
const alert = wrapper.find(GlAlert); const alert = wrapper.findComponent(GlAlert);
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.segmentsError); expect(alert.text()).toBe(DEVOPS_ADOPTION_STRINGS.app.segmentsError);
}); });
...@@ -344,6 +346,16 @@ describe('DevopsAdoptionApp', () => { ...@@ -344,6 +346,16 @@ describe('DevopsAdoptionApp', () => {
}; };
const defaultDevopsAdoptionTabBehavior = () => { const defaultDevopsAdoptionTabBehavior = () => {
describe('overview tab', () => {
it('displays the overview tab', () => {
expect(findOverviewTab().exists()).toBe(true);
});
it('displays the devops adoption overview component', () => {
expect(findOverviewTab().findComponent(DevopsAdoptionOverview).exists()).toBe(true);
});
});
describe('devops adoption tabs', () => { describe('devops adoption tabs', () => {
it('displays the configured number of tabs', () => { it('displays the configured number of tabs', () => {
expect(wrapper.findAllByTestId('devops-adoption-tab')).toHaveLength( expect(wrapper.findAllByTestId('devops-adoption-tab')).toHaveLength(
...@@ -353,12 +365,15 @@ describe('DevopsAdoptionApp', () => { ...@@ -353,12 +365,15 @@ describe('DevopsAdoptionApp', () => {
it('displays the devops section component with the tab', () => { it('displays the devops section component with the tab', () => {
expect( expect(
wrapper.findByTestId('devops-adoption-tab').find(DevopsAdoptionSection).exists(), wrapper
.findByTestId('devops-adoption-tab')
.findComponent(DevopsAdoptionSection)
.exists(),
).toBe(true); ).toBe(true);
}); });
it('displays the DevopsAdoptionAddDropdown as the last tab', () => { it('displays the DevopsAdoptionAddDropdown as the last tab', () => {
expect(wrapper.find(DevopsAdoptionAddDropdown).exists()).toBe(true); expect(wrapper.findComponent(DevopsAdoptionAddDropdown).exists()).toBe(true);
}); });
eventTrackingBehaviour('devops-adoption-tab', 'i_analytics_dev_ops_adoption'); eventTrackingBehaviour('devops-adoption-tab', 'i_analytics_dev_ops_adoption');
...@@ -379,7 +394,7 @@ describe('DevopsAdoptionApp', () => { ...@@ -379,7 +394,7 @@ describe('DevopsAdoptionApp', () => {
}); });
it('displays the devops score component', () => { it('displays the devops score component', () => {
expect(findDevopsScoreTab().find(DevopsScore).exists()).toBe(true); expect(findDevopsScoreTab().findComponent(DevopsScore).exists()).toBe(true);
}); });
eventTrackingBehaviour('devops-score-tab', 'i_analytics_dev_ops_score'); eventTrackingBehaviour('devops-score-tab', 'i_analytics_dev_ops_score');
......
import { GlIcon, GlProgressBar } from '@gitlab/ui';
import DevopsAdoptionOverviewCard from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_overview_card.vue';
import DevopsAdoptionTableCellFlag from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table_cell_flag.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { overallAdoptionData } from '../mock_data';
describe('DevopsAdoptionOverview', () => {
let wrapper;
const createComponent = (props) => {
wrapper = shallowMountExtended(DevopsAdoptionOverviewCard, {
propsData: {
...overallAdoptionData,
displayMeta: true,
...props,
},
});
};
describe('default state', () => {
beforeEach(() => {
createComponent();
});
describe('title', () => {
it('displays a icon', () => {
const icon = wrapper.findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe(overallAdoptionData.icon);
});
it('displays the title text', () => {
const text = wrapper.findByTestId('card-title-text');
expect(text.exists()).toBe(true);
expect(text.text()).toBe(overallAdoptionData.title);
});
});
it('displays the progress bar', () => {
expect(wrapper.findComponent(GlProgressBar).exists()).toBe(true);
});
it('displays the description correctly', () => {
const text = wrapper.findByTestId('card-description');
expect(text.exists()).toBe(true);
expect(text.text()).toBe('3/8 Overall adoption features adopted');
});
describe('meta', () => {
it('displays the meta', () => {
expect(wrapper.findByTestId('card-meta-row').exists()).toBe(true);
});
it('displays the correct number of rows', () => {
expect(wrapper.findAllByTestId('card-meta-row')).toHaveLength(
overallAdoptionData.featureMeta.length,
);
});
describe('meta row', () => {
it('displays a cell flag component', () => {
expect(wrapper.findComponent(DevopsAdoptionTableCellFlag).exists()).toBe(true);
});
it('displays the feature title', () => {
expect(wrapper.findByTestId('card-meta-row-title').text()).toBe(
overallAdoptionData.featureMeta[0].title,
);
});
});
});
});
describe('when not displaying meta', () => {
beforeEach(() => {
createComponent({ displayMeta: false });
});
it('displays the description correctly', () => {
const text = wrapper.findByTestId('card-description');
expect(text.exists()).toBe(true);
expect(text.text()).toBe('3/8 features adopted');
});
it('does not display the meta', () => {
expect(wrapper.findByTestId('card-meta-row').exists()).toBe(false);
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import DevopsAdoptionOverview from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_overview.vue';
import DevopsAdoptionOverviewCard from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_overview_card.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { devopsAdoptionNamespaceData, overallAdoptionData } from '../mock_data';
describe('DevopsAdoptionOverview', () => {
let wrapper;
const createComponent = (props) => {
wrapper = shallowMountExtended(DevopsAdoptionOverview, {
propsData: {
timestamp: '2020-10-31 23:59',
data: devopsAdoptionNamespaceData,
...props,
},
});
};
describe('default state', () => {
beforeEach(() => {
createComponent();
});
it('displays the overview container', () => {
expect(wrapper.findByTestId('overview-container').exists()).toBe(true);
});
describe('overview container', () => {
it('displays the header text', () => {
const text = wrapper.findByTestId('overview-container-header');
expect(text.exists()).toBe(true);
expect(text.text()).toBe(
'Feature adoption is based on usage in the previous calendar month. Last updated: 2020-10-31 23:59.',
);
});
it('displays the correct numnber of overview cards', () => {
expect(wrapper.findAllComponents(DevopsAdoptionOverviewCard)).toHaveLength(4);
});
it('passes the cards the correct data', () => {
expect(wrapper.findComponent(DevopsAdoptionOverviewCard).props()).toStrictEqual(
overallAdoptionData,
);
});
});
});
describe('loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('displays a loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not display the overview container', () => {
expect(wrapper.findByTestId('overview-container').exists()).toBe(false);
});
});
});
...@@ -234,7 +234,7 @@ describe('DevopsAdoptionTable', () => { ...@@ -234,7 +234,7 @@ describe('DevopsAdoptionTable', () => {
await nextTick(); await nextTick();
expect(findSortByLocalStorageSync().props('value')).toBe('issueOpened'); expect(findSortByLocalStorageSync().props('value')).toBe('mergeRequestApproved');
}); });
it('should update local storage when the sort direction changes', async () => { it('should update local storage when the sort direction changes', async () => {
......
...@@ -77,23 +77,23 @@ export const devopsAdoptionTableHeaders = [ ...@@ -77,23 +77,23 @@ export const devopsAdoptionTableHeaders = [
}, },
{ {
index: 1, index: 1,
label: 'Issues', label: 'Approvals',
tooltip: 'At least one issue opened', tooltip: 'At least one approval on an MR',
}, },
{ {
index: 2, index: 2,
label: 'MRs', label: 'Code owners',
tooltip: 'At least one MR opened', tooltip: 'Code owners enabled for at least one project',
}, },
{ {
index: 3, index: 3,
label: 'Approvals', label: 'Issues',
tooltip: 'At least one approval on an MR', tooltip: 'At least one issue opened',
}, },
{ {
index: 4, index: 4,
label: 'Code owners', label: 'MRs',
tooltip: 'Code owners enabled for at least one project', tooltip: 'At least one MR opened',
}, },
{ {
index: 5, index: 5,
...@@ -110,3 +110,44 @@ export const dataErrorMessage = 'Name already taken.'; ...@@ -110,3 +110,44 @@ export const dataErrorMessage = 'Name already taken.';
export const genericDeleteErrorMessage = export const genericDeleteErrorMessage =
'An error occurred while removing the group. Please try again.'; 'An error occurred while removing the group. Please try again.';
export const overallAdoptionData = {
displayMeta: false,
featureMeta: [
{
adopted: false,
title: 'Approvals',
},
{
adopted: false,
title: 'Code owners',
},
{
adopted: true,
title: 'Issues',
},
{
adopted: true,
title: 'MRs',
},
{
adopted: false,
title: 'Scanning',
},
{
adopted: false,
title: 'Deploys',
},
{
adopted: false,
title: 'Pipelines',
},
{
adopted: true,
title: 'Runners',
},
],
icon: 'tanuki',
title: 'Overall adoption',
variant: 'primary',
};
...@@ -11072,6 +11072,9 @@ msgstr "" ...@@ -11072,6 +11072,9 @@ msgstr ""
msgid "DevOps adoption" msgid "DevOps adoption"
msgstr "" msgstr ""
msgid "DevopsAdoption|%{adoptedCount}/%{featuresCount} %{title} features adopted"
msgstr ""
msgid "DevopsAdoption|Add Group" msgid "DevopsAdoption|Add Group"
msgstr "" msgstr ""
...@@ -11174,6 +11177,9 @@ msgstr "" ...@@ -11174,6 +11177,9 @@ msgstr ""
msgid "DevopsAdoption|Ops" msgid "DevopsAdoption|Ops"
msgstr "" msgstr ""
msgid "DevopsAdoption|Overall adoption"
msgstr ""
msgid "DevopsAdoption|Pipelines" msgid "DevopsAdoption|Pipelines"
msgstr "" msgstr ""
...@@ -11231,6 +11237,9 @@ msgstr "" ...@@ -11231,6 +11237,9 @@ msgstr ""
msgid "DevopsReport|Moderate" msgid "DevopsReport|Moderate"
msgstr "" msgstr ""
msgid "DevopsReport|Overview"
msgstr ""
msgid "DevopsReport|Score" msgid "DevopsReport|Score"
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