Commit 956fd910 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Douglas Barbosa Alexandre

Resolve "[DevOps Adoption] Introduce Dev, Sec, Ops tabs"

parent 8f84338e
<script> <script>
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; 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 API from '~/api';
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,
...@@ -11,6 +14,8 @@ import { ...@@ -11,6 +14,8 @@ import {
DEFAULT_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL,
DEVOPS_ADOPTION_GROUP_LEVEL_LABEL, DEVOPS_ADOPTION_GROUP_LEVEL_LABEL,
DEVOPS_ADOPTION_TABLE_CONFIGURATION, DEVOPS_ADOPTION_TABLE_CONFIGURATION,
TRACK_ADOPTION_TAB_CLICK_EVENT,
TRACK_DEVOPS_SCORE_TAB_CLICK_EVENT,
} from '../constants'; } from '../constants';
import bulkFindOrCreateDevopsAdoptionSegmentsMutation from '../graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql'; import bulkFindOrCreateDevopsAdoptionSegmentsMutation from '../graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql';
import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.query.graphql'; import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.query.graphql';
...@@ -26,6 +31,9 @@ export default { ...@@ -26,6 +31,9 @@ export default {
GlAlert, GlAlert,
DevopsAdoptionSection, DevopsAdoptionSection,
DevopsAdoptionSegmentModal, DevopsAdoptionSegmentModal,
DevopsScore,
GlTabs,
GlTab,
}, },
inject: { inject: {
isGroup: { isGroup: {
...@@ -34,11 +42,22 @@ export default { ...@@ -34,11 +42,22 @@ export default {
groupGid: { groupGid: {
default: null, default: null,
}, },
devopsScoreMetrics: {
default: null,
},
devopsReportDocsPath: {
default: '',
},
noDataImagePath: {
default: '',
},
}, },
i18n: { i18n: {
groupLevelLabel: DEVOPS_ADOPTION_GROUP_LEVEL_LABEL, groupLevelLabel: DEVOPS_ADOPTION_GROUP_LEVEL_LABEL,
...DEVOPS_ADOPTION_STRINGS.app, ...DEVOPS_ADOPTION_STRINGS.app,
}, },
trackDevopsTabClickEvent: TRACK_ADOPTION_TAB_CLICK_EVENT,
trackDevopsScoreTabClickEvent: TRACK_DEVOPS_SCORE_TAB_CLICK_EVENT,
maxSegments: MAX_SEGMENTS, maxSegments: MAX_SEGMENTS,
devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION, devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION,
data() { data() {
...@@ -63,6 +82,9 @@ export default { ...@@ -63,6 +82,9 @@ export default {
directDescendantsOnly: false, directDescendantsOnly: false,
} }
: {}, : {},
adoptionTabClicked: false,
devopsScoreTabClicked: false,
selectedTab: 0,
}; };
}, },
apollo: { apollo: {
...@@ -88,6 +110,9 @@ export default { ...@@ -88,6 +110,9 @@ export default {
}, },
}, },
computed: { computed: {
isAdmin() {
return !this.isGroup;
},
hasGroupData() { hasGroupData() {
return Boolean(this.groups?.nodes?.length); return Boolean(this.groups?.nodes?.length);
}, },
...@@ -121,9 +146,15 @@ export default { ...@@ -121,9 +146,15 @@ export default {
canRenderModal() { canRenderModal() {
return this.hasGroupData && !this.isLoading; return this.hasGroupData && !this.isLoading;
}, },
tabIndexValues() {
const tabs = this.$options.devopsAdoptionTableConfiguration.map((item) => item.tab);
return this.isGroup ? tabs : [...tabs, 'devops-score'];
},
}, },
created() { created() {
this.fetchGroups(); this.fetchGroups();
this.selectTab();
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.pollingTableData); clearInterval(this.pollingTableData);
...@@ -219,10 +250,53 @@ export default { ...@@ -219,10 +250,53 @@ export default {
deleteSegmentsFromCache(cache, ids, this.segmentsQueryVariables); deleteSegmentsFromCache(cache, ids, this.segmentsQueryVariables);
}, },
selectTab() {
const [value] = getParameterValues('tab');
if (value) {
this.selectedTab = this.tabIndexValues.indexOf(value);
}
},
onTabChange(index) {
if (index > 0) {
if (index !== this.selectedTab) {
const path = mergeUrlParams(
{ tab: this.tabIndexValues[index] },
window.location.pathname,
);
updateHistory({ url: path, title: window.title });
}
} else {
updateHistory({ url: window.location.pathname, title: window.title });
}
this.selectedTab = index;
},
trackDevopsScoreTabClick() {
if (!this.devopsScoreTabClicked) {
API.trackRedisHllUserEvent(this.$options.trackDevopsScoreTabClickEvent);
this.devopsScoreTabClicked = true;
}
},
trackDevopsTabClick() {
if (!this.adoptionTabClicked) {
API.trackRedisHllUserEvent(this.$options.trackDevopsTabClickEvent);
this.adoptionTabClicked = true;
}
},
}, },
}; };
</script> </script>
<template> <template>
<div>
<gl-tabs :value="selectedTab" @input="onTabChange">
<gl-tab
v-for="tab in $options.devopsAdoptionTableConfiguration"
:key="tab.title"
data-testid="devops-adoption-tab"
@click="trackDevopsTabClick"
>
<template #title>{{ tab.title }}</template>
<div v-if="hasLoadingError"> <div v-if="hasLoadingError">
<template v-for="(error, key) in errors"> <template v-for="(error, key) in errors">
<gl-alert v-if="error" :key="key" variant="danger" :dismissible="false" class="gl-mt-3"> <gl-alert v-if="error" :key="key" variant="danger" :dismissible="false" class="gl-mt-3">
...@@ -231,27 +305,35 @@ export default { ...@@ -231,27 +305,35 @@ export default {
</template> </template>
</div> </div>
<div v-else>
<devops-adoption-segment-modal
v-if="canRenderModal"
ref="addRemoveModal"
:groups="groups.nodes"
:enabled-groups="devopsAdoptionSegments.nodes"
@segmentsAdded="addSegmentsToCache"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
<devops-adoption-section <devops-adoption-section
v-else
:is-loading="isLoading" :is-loading="isLoading"
:has-segments-data="hasSegmentsData" :has-segments-data="hasSegmentsData"
:timestamp="timestamp" :timestamp="timestamp"
:has-group-data="hasGroupData" :has-group-data="hasGroupData"
:segment-limit-reached="segmentLimitReached" :segment-limit-reached="segmentLimitReached"
:edit-groups-button-label="editGroupsButtonLabel" :edit-groups-button-label="editGroupsButtonLabel"
:cols="$options.devopsAdoptionTableConfiguration[0].cols" :cols="tab.cols"
:segments="devopsAdoptionSegments" :segments="devopsAdoptionSegments"
@segmentsRemoved="deleteSegmentsFromCache" @segmentsRemoved="deleteSegmentsFromCache"
@openAddRemoveModal="openAddRemoveModal" @openAddRemoveModal="openAddRemoveModal"
/> />
</gl-tab>
<gl-tab v-if="isAdmin" data-testid="devops-score-tab" @click="trackDevopsScoreTabClick">
<template #title>{{ s__('DevopsReport|DevOps Score') }}</template>
<devops-score />
</gl-tab>
</gl-tabs>
<devops-adoption-segment-modal
v-if="canRenderModal"
ref="addRemoveModal"
:groups="groups.nodes"
:enabled-groups="devopsAdoptionSegments.nodes"
@segmentsAdded="addSegmentsToCache"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
</div> </div>
</template> </template>
...@@ -102,7 +102,8 @@ export const DEVOPS_ADOPTION_GROUP_COL_LABEL = __('Group'); ...@@ -102,7 +102,8 @@ export const DEVOPS_ADOPTION_GROUP_COL_LABEL = __('Group');
export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
{ {
title: s__('DevopsAdoption|Adoption'), title: s__('DevopsAdoption|Dev'),
tab: 'dev',
cols: [ cols: [
{ {
key: 'issueOpened', key: 'issueOpened',
...@@ -122,6 +123,24 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ ...@@ -122,6 +123,24 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
tooltip: s__('DevopsAdoption|At least 1 approval on an MR'), tooltip: s__('DevopsAdoption|At least 1 approval on an MR'),
testId: 'approvalsCol', testId: 'approvalsCol',
}, },
],
},
{
title: s__('DevopsAdoption|Sec'),
tab: 'sec',
cols: [
{
key: 'securityScanSucceeded',
label: s__('DevopsAdoption|Scanning'),
tooltip: s__('DevopsAdoption|At least 1 security scan of any type run in pipeline'),
testId: 'scanningCol',
},
],
},
{
title: s__('DevopsAdoption|Ops'),
tab: 'ops',
cols: [
{ {
key: 'runnerConfigured', key: 'runnerConfigured',
label: s__('DevopsAdoption|Runners'), label: s__('DevopsAdoption|Runners'),
...@@ -140,12 +159,10 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [ ...@@ -140,12 +159,10 @@ export const DEVOPS_ADOPTION_TABLE_CONFIGURATION = [
tooltip: s__('DevopsAdoption|At least 1 deploy'), tooltip: s__('DevopsAdoption|At least 1 deploy'),
testId: 'deploysCol', testId: 'deploysCol',
}, },
{
key: 'securityScanSucceeded',
label: s__('DevopsAdoption|Scanning'),
tooltip: s__('DevopsAdoption|At least 1 security scan of any type run in pipeline'),
testId: 'scanningCol',
},
], ],
}, },
]; ];
export const TRACK_ADOPTION_TAB_CLICK_EVENT = 'i_analytics_dev_ops_adoption';
export const TRACK_DEVOPS_SCORE_TAB_CLICK_EVENT = 'i_analytics_dev_ops_score';
...@@ -8,7 +8,13 @@ export default () => { ...@@ -8,7 +8,13 @@ export default () => {
if (!el) return false; if (!el) return false;
const { emptyStateSvgPath, groupId } = el.dataset; const {
emptyStateSvgPath,
groupId,
devopsScoreMetrics,
devopsReportDocsPath,
noDataImagePath,
} = el.dataset;
const isGroup = Boolean(groupId); const isGroup = Boolean(groupId);
...@@ -19,6 +25,9 @@ export default () => { ...@@ -19,6 +25,9 @@ export default () => {
emptyStateSvgPath, emptyStateSvgPath,
isGroup, isGroup,
groupGid: isGroup ? convertToGraphQLId(TYPE_GROUP, groupId) : null, groupGid: isGroup ? convertToGraphQLId(TYPE_GROUP, groupId) : null,
devopsScoreMetrics: isGroup ? null : JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
}, },
render(h) { render(h) {
return h(DevopsAdoptionApp); return h(DevopsAdoptionApp);
......
import Api from '~/api';
import { historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
const DEVOPS_ADOPTION_PANE = 'devops-adoption';
const DEVOPS_ADOPTION_PANE_TAB_CLICK_EVENT = 'i_analytics_dev_ops_adoption';
const tabClickHandler = (e) => {
const { hash } = e.currentTarget;
let tab = null;
if (hash === `#${DEVOPS_ADOPTION_PANE}`) {
tab = DEVOPS_ADOPTION_PANE;
Api.trackRedisHllUserEvent(DEVOPS_ADOPTION_PANE_TAB_CLICK_EVENT);
}
const newUrl = mergeUrlParams({ tab }, window.location.href);
historyPushState(newUrl);
};
const initTabs = () => {
const tabLinks = document.querySelectorAll('.js-devops-tab-item a');
if (tabLinks.length) {
tabLinks.forEach((tabLink) => {
tabLink.addEventListener('click', (e) => tabClickHandler(e));
});
}
};
export default initTabs;
import initDevopAdoption from 'ee/analytics/devops_report/devops_adoption'; import initDevopAdoption from 'ee/analytics/devops_report/devops_adoption';
import initTabs from 'ee/analytics/devops_report/tabs';
initTabs();
initDevopAdoption(); initDevopAdoption();
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
} }
} }
.actions-cell {
width: $gl-spacing-scale-6;
}
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
.actions-cell { .actions-cell {
div { div {
......
...@@ -5,11 +5,11 @@ module EE ...@@ -5,11 +5,11 @@ module EE
module DevOpsReportController module DevOpsReportController
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
track_redis_hll_event :show, name: 'i_analytics_dev_ops_adoption', if: -> { params[:tab] == 'devops-adoption' } track_redis_hll_event :show, name: 'i_analytics_dev_ops_adoption', if: -> { params[:tab] != 'devops-score' }
end end
def should_track_devops_score? def should_track_devops_score?
params[:tab] != 'devops-adoption' params[:tab] == 'devops-score'
end end
def show_adoption? def show_adoption?
......
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
%h2 %h2
= _('DevOps Report') = _('DevOps Report')
%ul.nav-links.nav-tabs.nav.js-devops-tabs{ role: 'tablist' }
= render 'tab', active: params[:tab] != 'devops-adoption', title: s_('DevopsReport|DevOps Score'), target: '#devops-score'
= render 'tab', active: params[:tab] == 'devops-adoption', title: s_('DevopsReport|Adoption'), target: '#devops-adoption'
.tab-content - if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
.tab-pane{ id: 'devops-score', class: ('active' if params[:tab] != 'devops-adoption') } = render_ce 'admin/dev_ops_report/callout'
= render_ce 'admin/dev_ops_report/report'
.tab-pane{ id: 'devops-adoption', class: ('active' if params[:tab] == 'devops-adoption') } - if !usage_ping_enabled
.js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } } #js-devops-usage-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
- else
.js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg'), devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg') } }
%li.nav-item.js-devops-tab-item{ role: 'presentation' }
%a.nav-link{ href: target, class: active_when(active), data: { toggle: 'tab' }, role: 'tab' }
= title
...@@ -38,13 +38,21 @@ RSpec.describe Admin::DevOpsReportController do ...@@ -38,13 +38,21 @@ RSpec.describe Admin::DevOpsReportController do
sign_in(user) sign_in(user)
end end
context 'when devops_adoption tab selected' do shared_examples 'tracks usage event' do |event, tab|
it 'tracks devops_adoption usage event' do it "tracks #{event} usage event for #{tab}" do
expect(Gitlab::UsageDataCounters::HLLRedisCounter) expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event).with('i_analytics_dev_ops_adoption', values: kind_of(String)) .to receive(:track_event).with(event, values: kind_of(String))
get :show, params: { tab: 'devops-adoption' }, format: :html get :show, params: { tab: tab }, format: :html
end end
end end
context 'when browsing to specific tabs' do
['', 'dev', 'sec', 'ops'].each do |tab|
it_behaves_like 'tracks usage event', 'i_analytics_dev_ops_adoption', tab
end
it_behaves_like 'tracks usage event', 'i_analytics_dev_ops_score', 'devops-score'
end
end end
end end
...@@ -3,9 +3,23 @@ ...@@ -3,9 +3,23 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'DevOps Report page', :js do RSpec.describe 'DevOps Report page', :js do
tabs_selector = '.js-devops-tabs' tabs_selector = '.gl-tabs-nav'
tab_item_selector = '.js-devops-tab-item' tab_item_selector = '.nav-item'
active_tab_selector = '.nav-link.active' active_tab_selector = '.nav-link.active'
tabs = [
{
value: 'sec',
text: 'Sec'
},
{
value: 'ops',
text: 'Ops'
},
{
value: 'devops-score',
text: 'DevOps Score'
}
]
before do before do
admin = create(:admin) admin = create(:admin)
...@@ -40,56 +54,68 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -40,56 +54,68 @@ 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(2) expect(page.all(:css, tab_item_selector).length).to be(4)
expect(page).to have_text 'DevOps Score Adoption' expect(page).to have_text 'Dev Sec Ops DevOps Score'
end end
end end
it 'defaults to the DevOps Score tab' do it 'defaults to the Dev 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: 'DevOps Score' expect(page).to have_selector active_tab_selector, text: 'Dev'
end end
end end
it 'displays the Adoption tab content when selected' do shared_examples 'displays tab content' do |tab|
it "displays the #{tab} tab content when selected" do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
click_link 'Adoption' click_link tab
within tabs_selector do within tabs_selector do
expect(page).to have_selector active_tab_selector, text: 'Adoption' expect(page).to have_selector active_tab_selector, text: tab
end
end end
end end
it 'does not add the tab param when the DevOps Score tab is selected' do tabs.each do |tab|
it_behaves_like 'displays tab content', tab[:text]
end
it 'does not add the tab param when the Dev tab is selected' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
click_link 'DevOps Score' click_link 'Dev'
expect(page).to have_current_path(admin_dev_ops_report_path) expect(page).to have_current_path(admin_dev_ops_report_path)
end end
it 'adds the ?tab=devops-adoption param when the Adoption tab is selected' do shared_examples 'appends the tab param to the url' do |tab, text|
it "adds the ?tab=#{tab} param when the #{text} tab is selected" do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path
click_link 'Adoption' click_link text
expect(page).to have_current_path(admin_dev_ops_report_path(tab: tab))
end
end
expect(page).to have_current_path(admin_dev_ops_report_path(tab: 'devops-adoption')) tabs.each do |tab|
it_behaves_like 'appends the tab param to the url', tab[:value], tab[:text]
end end
it 'shows the devops adoption tab when the tab param is set' do it 'shows the devops core tab when the tab param is set' do
visit admin_dev_ops_report_path(tab: 'devops-adoption') visit admin_dev_ops_report_path(tab: 'devops-score')
within tabs_selector do within tabs_selector do
expect(page).to have_selector active_tab_selector, text: 'Adoption' expect(page).to have_selector active_tab_selector, text: 'DevOps Score'
end end
end end
context 'the devops score tab' do context 'the devops score tab' do
it 'has dismissable intro callout' do it 'has dismissable intro callout' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path(tab: 'devops-score')
expect(page).to have_content 'Introducing Your DevOps Report' expect(page).to have_content 'Introducing Your DevOps Report'
...@@ -104,13 +130,13 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -104,13 +130,13 @@ RSpec.describe 'DevOps Report page', :js do
end end
it 'shows empty state' do it 'shows empty state' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path(tab: 'devops-score')
expect(page).to have_selector(".js-empty-state") expect(page).to have_selector(".js-empty-state")
end end
it 'hides the intro callout' do it 'hides the intro callout' do
visit admin_dev_ops_report_path visit admin_dev_ops_report_path(tab: 'devops-score')
expect(page).not_to have_content 'Introducing Your DevOps Report' expect(page).not_to have_content 'Introducing Your DevOps Report'
end end
...@@ -120,7 +146,7 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -120,7 +146,7 @@ RSpec.describe 'DevOps Report page', :js do
it 'shows empty state' do it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true) stub_application_setting(usage_ping_enabled: true)
visit admin_dev_ops_report_path visit admin_dev_ops_report_path(tab: 'devops-score')
expect(page).to have_content('Data is still calculating') expect(page).to have_content('Data is still calculating')
end end
...@@ -131,7 +157,7 @@ RSpec.describe 'DevOps Report page', :js do ...@@ -131,7 +157,7 @@ RSpec.describe 'DevOps Report page', :js do
stub_application_setting(usage_ping_enabled: true) stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_report_metric) create(:dev_ops_report_metric)
visit admin_dev_ops_report_path visit admin_dev_ops_report_path(tab: 'devops-score')
expect(page).to have_selector('[data-testid="devops-score-app"]') expect(page).to have_selector('[data-testid="devops-score-app"]')
end end
......
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount } 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 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';
...@@ -9,13 +9,17 @@ import DevopsAdoptionSegmentModal from 'ee/analytics/devops_report/devops_adopti ...@@ -9,13 +9,17 @@ import DevopsAdoptionSegmentModal from 'ee/analytics/devops_report/devops_adopti
import { import {
DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_STRINGS,
DEFAULT_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL,
DEVOPS_ADOPTION_TABLE_CONFIGURATION,
} from 'ee/analytics/devops_report/devops_adoption/constants'; } from 'ee/analytics/devops_report/devops_adoption/constants';
import bulkFindOrCreateDevopsAdoptionSegmentsMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql'; import bulkFindOrCreateDevopsAdoptionSegmentsMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql';
import devopsAdoptionSegments from 'ee/analytics/devops_report/devops_adoption/graphql/queries/devops_adoption_segments.query.graphql'; import devopsAdoptionSegments from 'ee/analytics/devops_report/devops_adoption/graphql/queries/devops_adoption_segments.query.graphql';
import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/queries/get_groups.query.graphql'; import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/queries/get_groups.query.graphql';
import { addSegmentsToCache } from 'ee/analytics/devops_report/devops_adoption/utils/cache_updates'; import { addSegmentsToCache } from 'ee/analytics/devops_report/devops_adoption/utils/cache_updates';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import API from '~/api';
import { import {
groupNodes, groupNodes,
nextGroupNode, nextGroupNode,
...@@ -84,7 +88,7 @@ describe('DevopsAdoptionApp', () => { ...@@ -84,7 +88,7 @@ describe('DevopsAdoptionApp', () => {
function createComponent(options = {}) { function createComponent(options = {}) {
const { mockApollo, data = {}, provide = {} } = options; const { mockApollo, data = {}, provide = {} } = options;
return shallowMount(DevopsAdoptionApp, { return shallowMountExtended(DevopsAdoptionApp, {
localVue, localVue,
apolloProvider: mockApollo, apolloProvider: mockApollo,
provide, provide,
...@@ -94,6 +98,8 @@ describe('DevopsAdoptionApp', () => { ...@@ -94,6 +98,8 @@ describe('DevopsAdoptionApp', () => {
}); });
} }
const findDevopsScoreTab = () => wrapper.findByTestId('devops-score-tab');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -444,4 +450,90 @@ describe('DevopsAdoptionApp', () => { ...@@ -444,4 +450,90 @@ describe('DevopsAdoptionApp', () => {
}); });
}); });
}); });
describe('tabs', () => {
const eventTrackingBehaviour = (testId, event) => {
describe('event tracking', () => {
it(`tracks the ${event} event when clicked`, () => {
jest.spyOn(API, 'trackRedisHllUserEvent');
expect(API.trackRedisHllUserEvent).not.toHaveBeenCalled();
wrapper.findByTestId(testId).vm.$emit('click');
expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
});
it('only tracks the event once', () => {
jest.spyOn(API, 'trackRedisHllUserEvent');
expect(API.trackRedisHllUserEvent).not.toHaveBeenCalled();
const { vm } = wrapper.findByTestId(testId);
vm.$emit('click');
vm.$emit('click');
expect(API.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
});
});
};
const defaultDevopsAdoptionTabBehavior = () => {
describe('devops adoption tabs', () => {
it('displays the configured number of tabs', () => {
expect(wrapper.findAllByTestId('devops-adoption-tab')).toHaveLength(
DEVOPS_ADOPTION_TABLE_CONFIGURATION.length,
);
});
it('displays the devops section component with the tab', () => {
expect(
wrapper.findByTestId('devops-adoption-tab').find(DevopsAdoptionSection).exists(),
).toBe(true);
});
eventTrackingBehaviour('devops-adoption-tab', 'i_analytics_dev_ops_adoption');
});
};
describe('admin level', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
wrapper = createComponent({ mockApollo });
});
defaultDevopsAdoptionTabBehavior();
describe('devops score tab', () => {
it('displays the devops score tab', () => {
expect(findDevopsScoreTab().exists()).toBe(true);
});
it('displays the devops score component', () => {
expect(findDevopsScoreTab().find(DevopsScore).exists()).toBe(true);
});
eventTrackingBehaviour('devops-score-tab', 'i_analytics_dev_ops_score');
});
});
describe('group level', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
wrapper = createComponent({
mockApollo,
provide: {
isGroup: true,
groupGid: devopsAdoptionSegmentsData.nodes[0].namespace.id,
},
});
});
defaultDevopsAdoptionTabBehavior();
it('does not display the devops score tab', () => {
expect(findDevopsScoreTab().exists()).toBe(false);
});
});
});
}); });
...@@ -101,26 +101,6 @@ export const devopsAdoptionTableHeaders = [ ...@@ -101,26 +101,6 @@ export const devopsAdoptionTableHeaders = [
}, },
{ {
index: 4, index: 4,
label: 'Runners',
tooltip: 'Runner configured for project/group',
},
{
index: 5,
label: 'Pipelines',
tooltip: 'At least 1 pipeline successfully run',
},
{
index: 6,
label: 'Deploys',
tooltip: 'At least 1 deploy',
},
{
index: 7,
label: 'Scanning',
tooltip: 'At least 1 security scan of any type run in pipeline',
},
{
index: 8,
label: '', label: '',
tooltip: null, tooltip: null,
}, },
......
import initTabs from 'ee/analytics/devops_report/tabs';
import Api from '~/api';
jest.mock('~/api.js');
jest.mock('~/lib/utils/common_utils');
describe('tabs', () => {
beforeEach(() => {
setFixtures(`
<div>
<div class="js-devops-tab-item">
<a href="#devops-score" data-testid='score-tab'>Score</a>
</div>
<div class="js-devops-tab-item">
<a href="#devops-adoption" data-testid='devops-adoption-tab'>Adoption</a>
</div>
</div`);
initTabs();
});
afterEach(() => {});
describe('tracking', () => {
it('tracks event when adoption tab is clicked', () => {
document.querySelector('[data-testid="devops-adoption-tab"]').click();
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith('i_analytics_dev_ops_adoption');
});
it('does not track an event when score tab is clicked', () => {
document.querySelector('[data-testid="score-tab"]').click();
expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled();
});
});
});
...@@ -15,7 +15,7 @@ RSpec.describe 'admin/dev_ops_report/show.html.haml' do ...@@ -15,7 +15,7 @@ RSpec.describe 'admin/dev_ops_report/show.html.haml' do
it 'disables the feature' do it 'disables the feature' do
render render
expect(rendered).not_to have_selector('#devops-adoption') expect(rendered).not_to have_selector('.js-devops-adoption')
end end
end end
...@@ -25,7 +25,7 @@ RSpec.describe 'admin/dev_ops_report/show.html.haml' do ...@@ -25,7 +25,7 @@ RSpec.describe 'admin/dev_ops_report/show.html.haml' do
render render
expect(rendered).to have_selector('#devops-adoption') expect(rendered).to have_selector('.js-devops-adoption')
end end
end end
end end
...@@ -11339,9 +11339,6 @@ msgstr "" ...@@ -11339,9 +11339,6 @@ msgstr ""
msgid "DevopsAdoption|Adopted" msgid "DevopsAdoption|Adopted"
msgstr "" msgstr ""
msgid "DevopsAdoption|Adoption"
msgstr ""
msgid "DevopsAdoption|An error occurred while removing the group. Please try again." msgid "DevopsAdoption|An error occurred while removing the group. Please try again."
msgstr "" msgstr ""
...@@ -11378,6 +11375,9 @@ msgstr "" ...@@ -11378,6 +11375,9 @@ msgstr ""
msgid "DevopsAdoption|Deploys" msgid "DevopsAdoption|Deploys"
msgstr "" msgstr ""
msgid "DevopsAdoption|Dev"
msgstr ""
msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin." msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
msgstr "" msgstr ""
...@@ -11405,6 +11405,9 @@ msgstr "" ...@@ -11405,6 +11405,9 @@ msgstr ""
msgid "DevopsAdoption|Not adopted" msgid "DevopsAdoption|Not adopted"
msgstr "" msgstr ""
msgid "DevopsAdoption|Ops"
msgstr ""
msgid "DevopsAdoption|Pipelines" msgid "DevopsAdoption|Pipelines"
msgstr "" msgstr ""
...@@ -11426,6 +11429,9 @@ msgstr "" ...@@ -11426,6 +11429,9 @@ msgstr ""
msgid "DevopsAdoption|Scanning" msgid "DevopsAdoption|Scanning"
msgstr "" msgstr ""
msgid "DevopsAdoption|Sec"
msgstr ""
msgid "DevopsAdoption|There was an error enabling the current group. Please refresh the page." msgid "DevopsAdoption|There was an error enabling the current group. Please refresh the page."
msgstr "" msgstr ""
...@@ -11438,9 +11444,6 @@ msgstr "" ...@@ -11438,9 +11444,6 @@ 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 ""
msgid "DevopsReport|Adoption"
msgstr ""
msgid "DevopsReport|DevOps Score" msgid "DevopsReport|DevOps Score"
msgstr "" msgstr ""
......
...@@ -9,12 +9,6 @@ RSpec.describe Admin::DevOpsReportController do ...@@ -9,12 +9,6 @@ RSpec.describe Admin::DevOpsReportController do
end end
end end
describe 'should_track_devops_score?' do
it 'is always true' do
expect(controller.should_track_devops_score?).to be_truthy
end
end
describe 'GET #show' do describe 'GET #show' do
context 'as admin' do context 'as admin' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
...@@ -31,6 +25,8 @@ RSpec.describe Admin::DevOpsReportController do ...@@ -31,6 +25,8 @@ RSpec.describe Admin::DevOpsReportController do
it_behaves_like 'tracking unique visits', :show do it_behaves_like 'tracking unique visits', :show do
let(:target_id) { 'i_analytics_dev_ops_score' } let(:target_id) { 'i_analytics_dev_ops_score' }
let(:request_params) { { tab: 'devops-score' } }
end end
end end
end end
......
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