Commit da1decdf authored by Brandon Labuschagne's avatar Brandon Labuschagne

Move DevOps Score callout to Vue

We also move the callout into the
DevOps Score tab within the EE
feature.

Changelog: changed
parent d982b664
......@@ -2,6 +2,7 @@
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { sprintf, s__ } from '~/locale';
import DevopsScoreCallout from './devops_score_callout.vue';
const defaultHeaderAttrs = {
thClass: 'gl-bg-white!',
......@@ -15,6 +16,7 @@ export default {
GlSingleStat,
GlLink,
GlEmptyState,
DevopsScoreCallout,
},
inject: {
devopsScoreMetrics: {
......@@ -65,46 +67,49 @@ export default {
};
</script>
<template>
<gl-empty-state
v-if="isEmpty"
:title="__('Data is still calculating...')"
:svg-path="noDataImagePath"
>
<template #description>
<p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
<gl-link :href="devopsReportDocsPath">{{
__('See example DevOps Score page in our documentation.')
}}</gl-link>
</template>
</gl-empty-state>
<div v-else data-testid="devops-score-app">
<div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
{{ titleHelperText }}
</div>
<gl-single-stat
unit="%"
size="sm"
:title="s__('DevopsReport|Your score')"
:should-animate="true"
:value="devopsScoreMetrics.averageScore.value"
:meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
:meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
:variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
/>
<gl-table
:fields="$options.tableHeaderFields"
:items="devopsScoreMetrics.cards"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
stacked="sm"
<div data-testid="devops-score-container">
<devops-score-callout />
<gl-empty-state
v-if="isEmpty"
:title="__('Data is still calculating...')"
:svg-path="noDataImagePath"
>
<template #cell(usage)="{ item }">
<div data-testid="usageCol">
<span>{{ item.usage }}</span>
<gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
item.scoreLevel.label
}}</gl-badge>
</div>
<template #description>
<p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
<gl-link :href="devopsReportDocsPath">{{
__('See example DevOps Score page in our documentation.')
}}</gl-link>
</template>
</gl-table>
</gl-empty-state>
<div v-else data-testid="devops-score-app">
<div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
{{ titleHelperText }}
</div>
<gl-single-stat
unit="%"
size="sm"
:title="s__('DevopsReport|Your score')"
:should-animate="true"
:value="devopsScoreMetrics.averageScore.value"
:meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
:meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
:variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
/>
<gl-table
:fields="$options.tableHeaderFields"
:items="devopsScoreMetrics.cards"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
stacked="sm"
>
<template #cell(usage)="{ item }">
<div data-testid="usageCol">
<span>{{ item.usage }}</span>
<gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
item.scoreLevel.label
}}</gl-badge>
</div>
</template>
</gl-table>
</div>
</div>
</template>
<script>
import { GlBanner } from '@gitlab/ui';
import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
import {
INTRO_COOKIE_KEY,
INTRO_BANNER_TITLE,
INTRO_BANNER_BODY,
INTRO_BANNER_ACTION_TEXT,
} from '../constants';
export default {
name: 'DevopsScoreCallout',
components: {
GlBanner,
},
inject: {
devopsReportDocsPath: {
default: '',
},
devopsScoreIntroImagePath: {
default: '',
},
},
data() {
return {
bannerDismissed: parseBoolean(getCookie(INTRO_COOKIE_KEY)),
};
},
i18n: {
title: INTRO_BANNER_TITLE,
body: INTRO_BANNER_BODY,
action: INTRO_BANNER_ACTION_TEXT,
},
methods: {
dismissBanner() {
setCookie(INTRO_COOKIE_KEY, 'true');
this.bannerDismissed = true;
},
},
};
</script>
<template>
<gl-banner
v-if="!bannerDismissed"
class="gl-mt-3"
variant="introduction"
:title="$options.i18n.title"
:button-text="$options.i18n.action"
:button-link="devopsReportDocsPath"
:svg-path="devopsScoreIntroImagePath"
@close="dismissBanner"
>
<p>{{ $options.i18n.body }}</p>
</gl-banner>
</template>
import { __ } from '~/locale';
export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
export const INTRO_BANNER_BODY = __(
'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
);
export const INTRO_BANNER_ACTION_TEXT = __('Read more');
......@@ -6,7 +6,12 @@ export default () => {
if (!el) return false;
const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
const {
devopsScoreMetrics,
devopsReportDocsPath,
noDataImagePath,
devopsScoreIntroImagePath,
} = el.dataset;
return new Vue({
el,
......@@ -14,6 +19,7 @@ export default () => {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
devopsScoreIntroImagePath,
},
render(h) {
return h(DevopsScore);
......
.gl-mt-3
.user-callout{ data: { uid: 'dev_ops_report_intro_callout_dismissed' } }
.bordered-box.landing.content-block
%button.gl-button.btn.btn-default-tertiary.close.js-close-callout{ type: 'button',
'aria-label' => _('Dismiss DevOps Report introduction') }
= sprite_icon('close', size: 16, css_class: 'dismiss-icon')
.user-callout-copy
%h4
= _('Introducing Your DevOps Report')
%p
= _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.')
.svg-container.devops
= custom_icon('dev_ops_report_overview')
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- if service_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
= render 'callout'
- if !service_ping_enabled
#js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/service_ping/index.md') } }
- else
#js-devops-score{ data: { 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') } }
#js-devops-score{ data: { 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'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
......@@ -45,15 +45,6 @@ export default {
groupGid: {
default: null,
},
devopsScoreMetrics: {
default: null,
},
devopsReportDocsPath: {
default: '',
},
noDataImagePath: {
default: '',
},
},
trackDevopsTabClickEvent: TRACK_ADOPTION_TAB_CLICK_EVENT,
trackDevopsScoreTabClickEvent: TRACK_DEVOPS_SCORE_TAB_CLICK_EVENT,
......
......@@ -14,6 +14,7 @@ export default () => {
groupId,
devopsScoreMetrics,
devopsReportDocsPath,
devopsScoreIntroImagePath,
noDataImagePath,
} = el.dataset;
......@@ -29,6 +30,7 @@ export default () => {
devopsScoreMetrics: isGroup ? null : JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
devopsScoreIntroImagePath,
},
render(h) {
return h(DevopsAdoptionApp);
......
......@@ -3,10 +3,7 @@
%h2
= _('DevOps Report')
- if service_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
= render_ce 'admin/dev_ops_report/callout'
- if !service_ping_enabled
#js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/service_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') } }
.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'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
......@@ -121,7 +121,9 @@ RSpec.describe 'DevOps Report page', :js do
expect(page).to have_content 'Introducing Your DevOps Report'
find('.js-close-callout').click
page.within(find('[data-testid="devops-score-container"]')) do
find('[data-testid="close-icon"]').click
end
expect(page).not_to have_content 'Introducing Your DevOps Report'
end
......
......@@ -11666,9 +11666,6 @@ msgid_plural "Dismiss %d selected vulnerabilities as"
msgstr[0] ""
msgstr[1] ""
msgid "Dismiss DevOps Report introduction"
msgstr ""
msgid "Dismiss Value Stream Analytics introduction box"
msgstr ""
......
......@@ -19,7 +19,9 @@ RSpec.describe 'DevOps Report page', :js do
expect(page).to have_content 'Introducing Your DevOps Report'
find('.js-close-callout').click
page.within(find('[data-testid="devops-score-container"]')) do
find('[data-testid="close-icon"]').click
end
expect(page).not_to have_content 'Introducing Your DevOps Report'
end
......
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
import * as utils from '~/lib/utils/common_utils';
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
describe('DevopsScoreCallout', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(DevopsScoreCallout, {
provide: {
devopsReportDocsPath,
devopsScoreIntroImagePath,
},
});
};
const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
wrapper.destroy();
});
describe('with no cookie set', () => {
beforeEach(() => {
utils.setCookie = jest.fn();
createComponent();
});
it('displays the banner', () => {
expect(findBanner().exists()).toBe(true);
});
it('does not call setCookie', () => {
expect(utils.setCookie).not.toHaveBeenCalled();
});
describe('when the close button is clicked', () => {
beforeEach(() => {
findBanner().vm.$emit('close');
});
it('sets the dismissed cookie', () => {
expect(utils.setCookie).toHaveBeenCalledWith(INTRO_COOKIE_KEY, 'true');
});
it('hides the banner', () => {
expect(findBanner().exists()).toBe(false);
});
});
});
describe('with the dismissed cookie set', () => {
beforeEach(() => {
jest.spyOn(utils, 'getCookie').mockReturnValue('true');
createComponent();
});
it('hides the banner', () => {
expect(findBanner().exists()).toBe(false);
});
});
});
......@@ -3,6 +3,7 @@ import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
import {
devopsScoreMetricsData,
devopsReportDocsPath,
......@@ -30,12 +31,17 @@ describe('DevopsScore', () => {
const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`);
const findUsageCol = () => findCol('usageCol');
const findDevopsScoreApp = () => wrapper.findByTestId('devops-score-app');
const bannerExists = () => wrapper.findComponent(DevopsScoreCallout).exists();
describe('with no data', () => {
beforeEach(() => {
createComponent({ devopsScoreMetrics: {} });
});
it('includes the DevopsScoreCallout component ', () => {
expect(bannerExists()).toBe(true);
});
describe('empty state', () => {
it('displays the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
......@@ -62,6 +68,10 @@ describe('DevopsScore', () => {
createComponent();
});
it('includes the DevopsScoreCallout component ', () => {
expect(bannerExists()).toBe(true);
});
it('does not display the empty state', () => {
expect(findEmptyState().exists()).toBe(false);
});
......
......@@ -44,3 +44,5 @@ export const devopsScoreMetricsData = {
export const devopsReportDocsPath = 'docs-path';
export const noDataImagePath = 'image-path';
export const devopsScoreIntroImagePath = 'image-path';
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