Commit 6cadce10 authored by Sean McGivern's avatar Sean McGivern

Merge branch '271244-fe-devops-report-convert-score-page-to-vue-components-2' into 'master'

DevOps Report: Convert Score empty state to Vue

See merge request gitlab-org/gitlab!60715
parents b771686c de5fc639
<script>
import { GlBadge, GlTable } from '@gitlab/ui';
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { sprintf, s__ } from '~/locale';
......@@ -13,11 +13,19 @@ export default {
GlBadge,
GlTable,
GlSingleStat,
GlLink,
GlEmptyState,
},
inject: {
devopsScoreMetrics: {
default: null,
},
devopsReportDocsPath: {
default: '',
},
noDataImagePath: {
default: '',
},
},
computed: {
titleHelperText() {
......@@ -28,6 +36,9 @@ export default {
{ timestamp: this.devopsScoreMetrics.createdAt },
);
},
isEmpty() {
return this.devopsScoreMetrics.averageScore === undefined;
},
},
tableHeaderFields: [
{
......@@ -54,7 +65,19 @@ export default {
};
</script>
<template>
<div data-testid="devops-score-app">
<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>
......
......@@ -6,12 +6,14 @@ export default () => {
if (!el) return false;
const { devopsScoreMetrics } = el.dataset;
const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
return new Vue({
el,
provide: {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
},
render(h) {
return h(DevopsScore);
......
......@@ -6,7 +6,7 @@ export default () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
if (!emptyStateContainer) return false;
......
import initDevOpsScore from '~/analytics/devops_report/devops_score';
import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
initDevOpsScoreEmptyState();
initDevOpsScoreDisabledUsagePing();
initDevOpsScore();
@import 'mixins_and_variables_and_functions';
.devops-empty svg {
margin: 64px auto 32px;
max-width: 420px;
}
......@@ -2,6 +2,8 @@
module DevOpsReportHelper
def devops_score_metrics(metric)
return {} if metric.blank?
{
averageScore: average_score_data(metric),
cards: devops_score_card_data(metric),
......
.container.devops-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('dev_ops_report_no_data')
%h4= _('Data is still calculating...')
%p
= _('It may be several days before you see feature usage data.')
= link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
......@@ -4,9 +4,7 @@
= render 'callout'
- if !usage_ping_enabled
#js-devops-empty-state{ 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') } }
- elsif @metric.blank?
= render 'no_data'
#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-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json } }
#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') } }
---
title: Migrate DevOps Score empty state to Vue
merge_request: 60715
author:
type: changed
@import '../../../../../app/assets/stylesheets/page_bundles/dev_ops_report';
@import 'page_bundles/mixins_and_variables_and_functions';
.circle {
width: $gl-spacing-scale-3;
......
......@@ -22993,9 +22993,6 @@ msgstr ""
msgid "Otherwise, click the link below to complete the process:"
msgstr ""
msgid "Our documentation includes an example DevOps Score report."
msgstr ""
msgid "Out-of-compliance with this project's policies and should be removed"
msgstr ""
......@@ -28813,6 +28810,9 @@ msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
msgstr ""
msgid "See example DevOps Score page in our documentation."
msgstr ""
msgid "See metrics"
msgstr ""
......
import { GlTable, GlBadge } from '@gitlab/ui';
import { GlTable, GlBadge, GlEmptyState, GlLink } from '@gitlab/ui';
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 { createdAt, cards, averageScore, devopsScoreTableHeaders } from '../mock_data';
import {
devopsScoreMetricsData,
devopsReportDocsPath,
noDataImagePath,
devopsScoreTableHeaders,
} from '../mock_data';
describe('DevopsScore', () => {
let wrapper;
const createComponent = () => {
const createComponent = ({ devopsScoreMetrics = devopsScoreMetricsData } = {}) => {
wrapper = extendedWrapper(
mount(DevopsScore, {
provide: {
devopsScoreMetrics: {
createdAt,
cards,
averageScore,
},
devopsScoreMetrics,
devopsReportDocsPath,
noDataImagePath,
},
}),
);
};
const findTable = () => wrapper.find(GlTable);
const findTable = () => wrapper.findComponent(GlTable);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`);
const findUsageCol = () => findCol('usageCol');
const findDevopsScoreApp = () => wrapper.findByTestId('devops-score-app');
beforeEach(() => {
createComponent();
});
describe('with no data', () => {
beforeEach(() => {
createComponent({ devopsScoreMetrics: {} });
});
it('displays the title note', () => {
expect(wrapper.findByTestId('devops-score-note-text').text()).toBe(
'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.',
);
});
describe('empty state', () => {
it('displays the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('displays the correct message', () => {
expect(findEmptyState().text()).toBe(
'Data is still calculating... It may be several days before you see feature usage data. See example DevOps Score page in our documentation.',
);
});
it('displays the single stat section', () => {
const component = wrapper.find(GlSingleStat);
it('contains a link to the feature documentation', () => {
expect(wrapper.findComponent(GlLink).exists()).toBe(true);
});
});
expect(component.exists()).toBe(true);
expect(component.props('value')).toBe(averageScore.value);
it('does not display the devops score app', () => {
expect(findDevopsScoreApp().exists()).toBe(false);
});
});
describe('devops score table', () => {
it('displays the table', () => {
expect(findTable().exists()).toBe(true);
describe('with data', () => {
beforeEach(() => {
createComponent();
});
describe('table headings', () => {
let headers;
it('does not display the empty state', () => {
expect(findEmptyState().exists()).toBe(false);
});
beforeEach(() => {
headers = findTable().findAll("[data-testid='header']");
});
it('displays the devops score app', () => {
expect(findDevopsScoreApp().exists()).toBe(true);
});
it('displays the correct number of headings', () => {
expect(headers).toHaveLength(devopsScoreTableHeaders.length);
describe('devops score app', () => {
it('displays the title note', () => {
expect(wrapper.findByTestId('devops-score-note-text').text()).toBe(
'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.',
);
});
describe.each(devopsScoreTableHeaders)('header fields', ({ label, index }) => {
let headerWrapper;
it('displays the single stat section', () => {
const component = wrapper.findComponent(GlSingleStat);
beforeEach(() => {
headerWrapper = headers.at(index);
});
expect(component.exists()).toBe(true);
expect(component.props('value')).toBe(devopsScoreMetricsData.averageScore.value);
});
it(`displays the correct table heading text for "${label}"`, () => {
expect(headerWrapper.text()).toContain(label);
describe('devops score table', () => {
it('displays the table', () => {
expect(findTable().exists()).toBe(true);
});
});
});
describe('table columns', () => {
describe('Your usage', () => {
it('displays the corrrect value', () => {
expect(findUsageCol().text()).toContain('3.2');
describe('table headings', () => {
let headers;
beforeEach(() => {
headers = findTable().findAll("[data-testid='header']");
});
it('displays the correct number of headings', () => {
expect(headers).toHaveLength(devopsScoreTableHeaders.length);
});
describe.each(devopsScoreTableHeaders)('header fields', ({ label, index }) => {
let headerWrapper;
beforeEach(() => {
headerWrapper = headers.at(index);
});
it(`displays the correct table heading text for "${label}"`, () => {
expect(headerWrapper.text()).toContain(label);
});
});
});
it('displays the corrrect badge', () => {
const badge = findUsageCol().find(GlBadge);
describe('table columns', () => {
describe('Your usage', () => {
it('displays the corrrect value', () => {
expect(findUsageCol().text()).toContain('3.2');
});
it('displays the corrrect badge', () => {
const badge = findUsageCol().find(GlBadge);
expect(badge.exists()).toBe(true);
expect(badge.props('variant')).toBe('muted');
expect(badge.text()).toBe('Low');
expect(badge.exists()).toBe(true);
expect(badge.props('variant')).toBe('muted');
expect(badge.text()).toBe('Low');
});
});
});
});
});
......
export const averageScore = {
value: '10',
scoreLevel: {
label: 'High',
icon: 'check-circle',
variant: 'success',
},
};
export const cards = [
{
title: 'Issues created per active user',
usage: '3.2',
leadInstance: '10.2',
score: '0',
scoreLevel: {
label: 'Low',
variant: 'muted',
},
},
];
export const createdAt = '2020-06-29 08:16';
export const devopsScoreTableHeaders = [
{
index: 0,
......@@ -40,3 +16,31 @@ export const devopsScoreTableHeaders = [
label: 'Score',
},
];
export const devopsScoreMetricsData = {
createdAt: '2020-06-29 08:16',
cards: [
{
title: 'Issues created per active user',
usage: '3.2',
leadInstance: '10.2',
score: '0',
scoreLevel: {
label: 'Low',
variant: 'muted',
},
},
],
averageScore: {
value: '10',
scoreLevel: {
label: 'High',
icon: 'check-circle',
variant: 'success',
},
},
};
export const devopsReportDocsPath = 'docs-path';
export const noDataImagePath = 'image-path';
......@@ -31,5 +31,11 @@ RSpec.describe DevOpsReportHelper do
it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status_success_solid", label: "High", variant: "success" }, value: "82.0" } ) }
end
describe 'with blank metrics' do
let(:devops_score_metrics) { helper.devops_score_metrics({}) }
it { expect(devops_score_metrics).to eq({}) }
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