Commit 6aa8044f authored by Brandon Labuschagne's avatar Brandon Labuschagne

Move Devops Score empty into vue

The empty state is currently being handled outside
of the vue app. This commit moves it into the
devops score vue app.

Changelog: changed
parent 52bf0fff
<script> <script>
import { GlBadge, GlTable } from '@gitlab/ui'; import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
...@@ -13,11 +13,19 @@ export default { ...@@ -13,11 +13,19 @@ export default {
GlBadge, GlBadge,
GlTable, GlTable,
GlSingleStat, GlSingleStat,
GlLink,
GlEmptyState,
}, },
inject: { inject: {
devopsScoreMetrics: { devopsScoreMetrics: {
default: null, default: null,
}, },
devopsReportDocsPath: {
default: '',
},
noDataImagePath: {
default: '',
},
}, },
computed: { computed: {
titleHelperText() { titleHelperText() {
...@@ -28,6 +36,9 @@ export default { ...@@ -28,6 +36,9 @@ export default {
{ timestamp: this.devopsScoreMetrics.createdAt }, { timestamp: this.devopsScoreMetrics.createdAt },
); );
}, },
isEmpty() {
return this.devopsScoreMetrics.averageScore === undefined;
},
}, },
tableHeaderFields: [ tableHeaderFields: [
{ {
...@@ -54,7 +65,19 @@ export default { ...@@ -54,7 +65,19 @@ export default {
}; };
</script> </script>
<template> <template>
<div data-testid="devops-score-app"> <gl-empty-state
v-if="isEmpty"
:title="__('Data is still calculating...')"
:svg-path="noDataImagePath"
>
<template #description>
<div>{{ __('It may be several days before you see feature usage data.') }}</div>
<gl-link :href="devopsReportDocsPath">{{
__('Our documentation includes an example DevOps Score report.')
}}</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"> <div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
{{ titleHelperText }} {{ titleHelperText }}
</div> </div>
......
...@@ -6,12 +6,14 @@ export default () => { ...@@ -6,12 +6,14 @@ export default () => {
if (!el) return false; if (!el) return false;
const { devopsScoreMetrics } = el.dataset; const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
return new Vue({ return new Vue({
el, el,
provide: { provide: {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics), devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
}, },
render(h) { render(h) {
return h(DevopsScore); return h(DevopsScore);
......
...@@ -6,7 +6,7 @@ export default () => { ...@@ -6,7 +6,7 @@ export default () => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new UserCallout(); new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state'); const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
if (!emptyStateContainer) return false; if (!emptyStateContainer) return false;
......
import initDevOpsScore from '~/analytics/devops_report/devops_score'; 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(); initDevOpsScore();
@import 'mixins_and_variables_and_functions';
.devops-empty svg {
margin: 64px auto 32px;
max-width: 420px;
}
...@@ -2,11 +2,15 @@ ...@@ -2,11 +2,15 @@
module DevOpsReportHelper module DevOpsReportHelper
def devops_score_metrics(metric) def devops_score_metrics(metric)
{ if metric.blank?
averageScore: average_score_data(metric), {}
cards: devops_score_card_data(metric), else
createdAt: metric.created_at.strftime('%Y-%m-%d %H:%M') {
} averageScore: average_score_data(metric),
cards: devops_score_card_data(metric),
createdAt: metric.created_at.strftime('%Y-%m-%d %H:%M')
}
end
end end
private private
......
...@@ -4,9 +4,7 @@ ...@@ -4,9 +4,7 @@
= render 'callout' = render 'callout'
- if !usage_ping_enabled - 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') } } #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') } }
- elsif @metric.blank?
= render 'no_data'
- else - 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 { .circle {
width: $gl-spacing-scale-3; width: $gl-spacing-scale-3;
......
import { GlTable, GlBadge } from '@gitlab/ui'; import { GlTable, GlBadge, GlEmptyState, GlLink } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue'; 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', () => { describe('DevopsScore', () => {
let wrapper; let wrapper;
const createComponent = () => { const createComponent = ({ devopsScoreMetrics = devopsScoreMetricsData } = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(DevopsScore, { mount(DevopsScore, {
provide: { provide: {
devopsScoreMetrics: { devopsScoreMetrics,
createdAt, devopsReportDocsPath,
cards, noDataImagePath,
averageScore,
},
}, },
}), }),
); );
}; };
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`); const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`);
const findUsageCol = () => findCol('usageCol'); const findUsageCol = () => findCol('usageCol');
const findDevopsScoreApp = () => wrapper.findByTestId('devops-score-app');
beforeEach(() => { describe('with no data', () => {
createComponent(); beforeEach(() => {
}); createComponent({ devopsScoreMetrics: {} });
});
it('displays the title note', () => { describe('empty state', () => {
expect(wrapper.findByTestId('devops-score-note-text').text()).toBe( it('displays the empty state', () => {
'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.', 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. Our documentation includes an example DevOps Score report.',
);
});
it('displays the single stat section', () => { it('contains a link to the feature documentation', () => {
const component = wrapper.find(GlSingleStat); expect(wrapper.find(GlLink).exists()).toBe(true);
});
});
expect(component.exists()).toBe(true); it('does not display the devops score app', () => {
expect(component.props('value')).toBe(averageScore.value); expect(findDevopsScoreApp().exists()).toBe(false);
});
}); });
describe('devops score table', () => { describe('with data', () => {
it('displays the table', () => { beforeEach(() => {
expect(findTable().exists()).toBe(true); createComponent();
}); });
describe('table headings', () => { it('does not display the empty state', () => {
let headers; expect(findEmptyState().exists()).toBe(false);
});
beforeEach(() => { it('displays the devops score app', () => {
headers = findTable().findAll("[data-testid='header']"); expect(findDevopsScoreApp().exists()).toBe(true);
}); });
it('displays the correct number of headings', () => { describe('devops score app', () => {
expect(headers).toHaveLength(devopsScoreTableHeaders.length); 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 }) => { it('displays the single stat section', () => {
let headerWrapper; const component = wrapper.find(GlSingleStat);
beforeEach(() => { expect(component.exists()).toBe(true);
headerWrapper = headers.at(index); expect(component.props('value')).toBe(devopsScoreMetricsData.averageScore.value);
}); });
it(`displays the correct table heading text for "${label}"`, () => { describe('devops score table', () => {
expect(headerWrapper.text()).toContain(label); it('displays the table', () => {
expect(findTable().exists()).toBe(true);
}); });
});
});
describe('table columns', () => { describe('table headings', () => {
describe('Your usage', () => { let headers;
it('displays the corrrect value', () => {
expect(findUsageCol().text()).toContain('3.2'); 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', () => { describe('table columns', () => {
const badge = findUsageCol().find(GlBadge); 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.exists()).toBe(true);
expect(badge.props('variant')).toBe('muted'); expect(badge.props('variant')).toBe('muted');
expect(badge.text()).toBe('Low'); 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 = [ export const devopsScoreTableHeaders = [
{ {
index: 0, index: 0,
...@@ -40,3 +16,31 @@ export const devopsScoreTableHeaders = [ ...@@ -40,3 +16,31 @@ export const devopsScoreTableHeaders = [
label: 'Score', 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';
...@@ -9,6 +9,9 @@ RSpec.describe DevOpsReportHelper do ...@@ -9,6 +9,9 @@ RSpec.describe DevOpsReportHelper do
describe '#devops_score_metrics' do describe '#devops_score_metrics' do
let(:devops_score_metrics) { helper.devops_score_metrics(subject) } let(:devops_score_metrics) { helper.devops_score_metrics(subject) }
let(:devops_score_metrics_blank) { helper.devops_score_metrics({}) }
it { expect(devops_score_metrics_blank).to eq({}) }
it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status-alert", label: "Moderate", variant: "warning" }, value: "55.9" } ) } it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status-alert", label: "Moderate", variant: "warning" }, value: "55.9" } ) }
......
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