Commit 3df54c86 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '9788-add-generic-metrics-report-type-to-merge-requests' into 'master'

Resolve "Add generic 'metrics' report type to merge requests" frontend

See merge request gitlab-org/gitlab-ee!10380
parents 3a0d9df1 2ca109da
......@@ -13,6 +13,11 @@ export default {
type: String,
required: true,
},
statusIconSize: {
type: Number,
required: false,
default: 32,
},
},
computed: {
iconName() {
......@@ -45,6 +50,6 @@ export default {
}"
class="report-block-list-icon"
>
<icon :name="iconName" :size="32" />
<icon :name="iconName" :size="statusIconSize" />
</div>
</template>
......@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
statusIconSize: {
type: Number,
required: false,
default: 32,
},
isNew: {
type: Boolean,
required: false,
......@@ -34,7 +39,7 @@ export default {
</script>
<template>
<li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue">
<issue-status-icon :status="status" class="append-right-5" />
<issue-status-icon :status="status" :status-icon-size="statusIconSize" class="append-right-5" />
<component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" />
</li>
......
......@@ -33,7 +33,7 @@ export default {
</script>
<template>
<div class="space-children d-flex append-right-10 widget-status-icon">
<div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon /></div>
<div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="md" /></div>
<ci-icon v-else :status="statusObj" :size="24" />
......
<script>
import ReportSection from '~/reports/components/report_section.vue';
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import GroupedMetricsReportsApp from 'ee/vue_shared/metrics_reports/grouped_metrics_reports_app.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import MrWidgetLicenses from 'ee/vue_shared/license_management/mr_widget_license_report.vue';
......@@ -16,6 +17,7 @@ export default {
MrWidgetApprovals,
MrWidgetGeoSecondaryNode,
GroupedSecurityReportsApp,
GroupedMetricsReportsApp,
ReportSection,
},
extends: CEWidgetOptions,
......@@ -239,6 +241,11 @@ export default {
:component="$options.componentNames.PerformanceIssueBody"
class="js-performance-widget mr-widget-border-top mr-report"
/>
<grouped-metrics-reports-app
v-if="mr.metricsReportsPath"
:endpoint="mr.metricsReportsPath"
class="js-metrics-reports-container"
/>
<grouped-security-reports-app
v-if="shouldRenderSecurityReport"
:head-blob-path="mr.headBlobPath"
......
......@@ -26,6 +26,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initCodeclimate(data);
this.initPerformanceReport(data);
this.licenseManagement = data.license_management;
this.metricsReportsPath = data.metrics_reports_path;
}
setData(data, isRebased) {
......
......@@ -8,6 +8,7 @@ import LicenseIssueBody from 'ee/vue_shared/license_management/components/licens
import SastIssueBody from 'ee/vue_shared/security_reports/components/sast_issue_body.vue';
import SastContainerIssueBody from 'ee/vue_shared/security_reports/components/sast_container_issue_body.vue';
import DastIssueBody from 'ee/vue_shared/security_reports/components/dast_issue_body.vue';
import MetricsReportsIssueBody from 'ee/vue_shared/metrics_reports/components/metrics_reports_issue_body.vue';
export const components = {
...componentsCE,
......@@ -17,6 +18,7 @@ export const components = {
SastContainerIssueBody,
SastIssueBody,
DastIssueBody,
MetricsReportsIssueBody,
};
export const componentNames = {
......@@ -27,4 +29,5 @@ export const componentNames = {
SastContainerIssueBody: SastContainerIssueBody.name,
SastIssueBody: SastIssueBody.name,
DastIssueBody: DastIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name,
};
<script>
import { __ } from '~/locale';
import { GlBadge } from '@gitlab/ui';
export default {
name: 'MetricsReportsIssueBody',
components: {
GlBadge,
},
props: {
issue: {
type: Object,
required: true,
validator(obj) {
return obj.name !== undefined && obj.value !== undefined;
},
},
},
computed: {
shouldShowBadge() {
return this.issue.isNew || this.issue.wasRemoved;
},
badgeText() {
if (this.issue.isNew) {
return __('New');
}
return __('Removed');
},
/*
* If metric is new or removed, we do not need to show previous value
*/
previousValue() {
if (this.shouldShowBadge) {
return '';
}
if (this.issue.previous_value) {
return `(${this.issue.previous_value})`;
}
return __('(No changes)');
},
},
};
</script>
<template>
<div class="report-block-list-issue-description">
<div class="report-block-list-issue-description-text js-metrics-reports-issue-text">
{{ issue.name }}: {{ issue.value }} {{ previousValue }}
</div>
<gl-badge v-if="shouldShowBadge" variant="info" class="js-metrics-reports-issue-badge">{{
badgeText
}}</gl-badge>
</div>
</template>
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue';
import ReportItem from '~/reports/components/report_item.vue';
import { n__, s__, sprintf } from '~/locale';
import createStore from './store';
export default {
name: 'GroupedMetricsReportsApp',
store: createStore(),
components: {
ReportSection,
SummaryRow,
ReportItem,
SmartVirtualList,
},
props: {
endpoint: {
type: String,
required: true,
},
},
componentNames,
// Typical height of a report item in px
typicalReportItemHeight: 32,
/*
* The maximum amount of shown issues. This is calculated by
* ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin
* We will use VirtualList if we have more items than this number.
* For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly.
*/
maxShownReportItems: 20,
computed: {
...mapState(['numberOfChanges', 'isLoading', 'hasError']),
...mapGetters(['summaryStatus', 'metrics']),
groupedSummaryText() {
if (this.isLoading) {
return s__('Reports|Metrics reports are loading');
}
if (this.hasError) {
return s__('Reports|Metrics reports failed loading results');
}
if (this.numberOfChanges < 1) {
return s__('Reports|Metrics reports did not change');
}
const pointsString = n__('point', 'points', this.numberOfChanges);
return sprintf(s__('Reports|Metrics reports changed on %{numberOfChanges} %{pointsString}'), {
numberOfChanges: this.numberOfChanges,
pointsString,
});
},
hasChanges() {
return this.numberOfChanges > 0;
},
hasMetrics() {
return this.metrics.length > 0;
},
},
created() {
this.setEndpoint(this.endpoint);
this.fetchMetrics();
},
methods: {
...mapActions(['setEndpoint', 'fetchMetrics']),
},
};
</script>
<template>
<report-section
:status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="hasMetrics"
class="mr-widget-border-top grouped-security-reports mr-report"
>
<div slot="body" class="mr-widget-grouped-section report-block">
<smart-virtual-list
:length="metrics.length"
:remain="$options.maxShownReportItems"
:size="$options.typicalReportItemHeight"
class="report-block-container"
wtag="ul"
wclass="report-block-list"
>
<report-item
v-for="(metric, index) in metrics"
:key="index"
:issue="metric"
status="none"
:status-icon-size="24"
:component="$options.componentNames.MetricsReportsIssueBody"
class="prepend-left-4 prepend-top-4 append-bottom-8"
/>
</smart-virtual-list>
</div>
</report-section>
</template>
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const requestMetrics = ({ commit }) => commit(types.REQUEST_METRICS);
export const fetchMetrics = ({ state, dispatch }) => {
dispatch('requestMetrics');
return axios
.get(state.endpoint)
.then(response => dispatch('receiveMetricsSuccess', response.data))
.catch(() => dispatch('receiveMetricsError'));
};
export const receiveMetricsSuccess = ({ commit }, response) => {
commit(types.RECEIVE_METRICS_SUCCESS, response);
};
export const receiveMetricsError = ({ commit }) => commit(types.RECEIVE_METRICS_ERROR);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { LOADING, ERROR, SUCCESS } from '../constants';
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
}
if (state.hasError || state.numberOfChanges > 0) {
return ERROR;
}
return SUCCESS;
};
export const metrics = state => [
...state.newMetrics.map(metric => ({ ...metric, isNew: true })),
...state.existingMetrics,
...state.removedMetrics.map(metric => ({ ...metric, wasRemoved: true })),
];
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_METRICS = 'REQUEST_METRICS';
export const RECEIVE_METRICS_SUCCESS = 'RECEIVE_METRICS_SUCCESS';
export const RECEIVE_METRICS_ERROR = 'RECEIVE_METRICS_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.REQUEST_METRICS](state) {
state.isLoading = true;
},
[types.RECEIVE_METRICS_SUCCESS](state, response) {
// Make sure to clean previous state in case it was an error
state.hasError = false;
state.isLoading = false;
state.newMetrics = response.new_metrics || [];
state.existingMetrics = response.existing_metrics || [];
state.removedMetrics = response.removed_metrics || [];
state.numberOfChanges =
state.existingMetrics.filter(metric => metric.previous_value !== undefined).length +
state.newMetrics.length +
state.removedMetrics.length;
},
[types.RECEIVE_METRICS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.newMetrics = [];
state.existingMetrics = [];
state.removedMetrics = [];
state.numberOfChanges = 0;
},
};
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
/**
* Each metric will have the following format:
* {
* name: {String},
* value: {String},
* previous_value: {String}
* }
*/
newMetrics: [],
existingMetrics: [],
removedMetrics: [],
numberOfChanges: 0,
});
---
title: Added metrics reports widget to merge request page
merge_request: 10380
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MetricReportsIssueBody from 'ee/vue_shared/metrics_reports/components/metrics_reports_issue_body.vue';
const localVue = createLocalVue();
describe('Metrics reports issue body', () => {
const Component = localVue.extend(MetricReportsIssueBody);
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('when metric did not change', () => {
it('should render metric with no changes text', () => {
wrapper = shallowMount(Component, {
sync: false,
localVue,
propsData: {
issue: {
name: 'name',
value: 'value',
},
},
});
const metric = wrapper.element.querySelector('.js-metrics-reports-issue-text');
expect(metric.innerText.trim()).toEqual('name: value (No changes)');
});
});
describe('when metric changed', () => {
it('should render metric with change', () => {
wrapper = shallowMount(Component, {
sync: false,
localVue,
propsData: {
issue: {
name: 'name',
value: 'value',
previous_value: 'prev',
},
},
});
const metric = wrapper.element.querySelector('.js-metrics-reports-issue-text');
expect(metric.innerText.trim()).toEqual('name: value (prev)');
});
});
describe('when metric is new', () => {
it('should render metric with new badge', () => {
wrapper = shallowMount(Component, {
sync: false,
localVue,
propsData: {
issue: {
name: 'name',
value: 'value',
isNew: true,
},
},
});
const metric = wrapper.element.querySelector('.js-metrics-reports-issue-text');
const badge = wrapper.element.querySelector('.js-metrics-reports-issue-badge');
expect(metric.innerText.trim()).toEqual('name: value');
expect(badge.innerText.trim()).toEqual('New');
});
});
describe('when metric was removed', () => {
it('should render metric with removed badge', () => {
wrapper = shallowMount(Component, {
sync: false,
localVue,
propsData: {
issue: {
name: 'name',
value: 'value',
wasRemoved: true,
},
},
});
const metric = wrapper.element.querySelector('.js-metrics-reports-issue-text');
const badge = wrapper.element.querySelector('.js-metrics-reports-issue-badge');
expect(metric.innerText.trim()).toEqual('name: value');
expect(badge.innerText.trim()).toEqual('Removed');
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedMetricsReportsApp from 'ee/vue_shared/metrics_reports/grouped_metrics_reports_app.vue';
import MetricsReportsIssueBody from 'ee/vue_shared/metrics_reports/components/metrics_reports_issue_body.vue';
import store from 'ee/vue_shared/metrics_reports/store';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Grouped metrics reports app', () => {
const Component = localVue.extend(GroupedMetricsReportsApp);
let wrapper;
let mockStore;
const mountComponent = () => {
wrapper = mount(Component, {
sync: false,
store: mockStore,
localVue,
propsData: {
endpoint: 'metrics.json',
},
methods: {
fetchMetrics: () => {},
},
});
};
beforeEach(() => {
mockStore = store();
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('while loading', () => {
beforeEach(() => {
mockStore.state.isLoading = true;
mountComponent();
});
it('renders loading state', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toEqual('Metrics reports are loading');
});
});
describe('with error', () => {
beforeEach(() => {
mockStore.state.isLoading = false;
mockStore.state.hasError = true;
mountComponent();
});
it('renders error state', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toEqual('Metrics reports failed loading results');
});
});
describe('with metrics', () => {
describe('with no changes', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 0;
mockStore.state.existingMetrics = [
{
name: 'name',
value: 'value',
},
];
mountComponent();
});
it('renders no changes header', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toContain('Metrics reports did not change');
});
});
describe('with one change', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 1;
mockStore.state.existingMetrics = [
{
name: 'name',
value: 'value',
previous_value: 'prev',
},
];
mountComponent();
});
it('renders one change header', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toContain('Metrics reports changed on 1 point');
});
});
describe('with multiple changes', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 2;
mockStore.state.existingMetrics = [
{
name: 'name',
value: 'value',
previous_value: 'prev',
},
{
name: 'name',
value: 'value',
previous_value: 'prev',
},
];
mountComponent();
});
it('renders multiple changes header', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toContain('Metrics reports changed on 2 points');
});
});
describe('with new metrics', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 1;
mockStore.state.newMetrics = [
{
name: 'name',
value: 'value',
},
];
mountComponent();
});
it('renders new changes header', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toContain('Metrics reports changed on 1 point');
});
});
describe('with removed metrics', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 1;
mockStore.state.removedMetrics = [
{
name: 'name',
value: 'value',
},
];
mountComponent();
});
it('renders new changes header', () => {
const header = wrapper.element.querySelector('.js-code-text');
expect(header.innerText.trim()).toContain('Metrics reports changed on 1 point');
});
});
describe('when has metrics', () => {
beforeEach(() => {
mockStore.state.numberOfChanges = 1;
mockStore.state.existingMetrics = [
{
name: 'name',
value: 'value',
previous_value: 'prev',
},
];
mountComponent();
});
it('renders custom metric issue body', () => {
const issueBody = wrapper.find(MetricsReportsIssueBody);
expect(issueBody.props('issue').name).toEqual('name');
expect(issueBody.props('issue').value).toEqual('value');
expect(issueBody.props('issue').previous_value).toEqual('prev');
});
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setEndpoint,
requestMetrics,
fetchMetrics,
receiveMetricsSuccess,
receiveMetricsError,
} from 'ee/vue_shared/metrics_reports/store/actions';
import * as types from 'ee/vue_shared/metrics_reports/store/mutation_types';
import state from 'ee/vue_shared/metrics_reports/store/state';
import testAction from 'spec/helpers/vuex_action_helper';
describe('metrics reports actions', () => {
let mockedState;
let mock;
beforeEach(() => {
mockedState = state();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('setEndpoint', () => {
it('should commit set endpoint', done => {
testAction(
setEndpoint,
'path',
mockedState,
[
{
type: types.SET_ENDPOINT,
payload: 'path',
},
],
[],
done,
);
});
});
describe('requestMetrics', () => {
it('should commit request mutation', done => {
testAction(
requestMetrics,
null,
mockedState,
[
{
type: types.REQUEST_METRICS,
},
],
[],
done,
);
});
});
describe('fetchMetrics', () => {
it('should call metrics endpoint', done => {
const data = {
metrics: [
{
name: 'name',
value: 'value',
},
],
};
const endpoint = '/mock-endpoint.json';
mockedState.endpoint = endpoint;
mock.onGet(endpoint).replyOnce(200, data);
testAction(
fetchMetrics,
null,
mockedState,
[],
[
{
type: 'requestMetrics',
},
{
payload: data,
type: 'receiveMetricsSuccess',
},
],
done,
);
});
it('handles errors', done => {
const endpoint = '/mock-endpoint.json';
mockedState.endpoint = endpoint;
mock.onGet(endpoint).replyOnce(500);
testAction(
fetchMetrics,
null,
mockedState,
[],
[
{
type: 'requestMetrics',
},
{
type: 'receiveMetricsError',
},
],
done,
);
});
});
describe('receiveMetricsSuccess', () => {
it('should commit request mutation', done => {
const response = { metrics: [] };
testAction(
receiveMetricsSuccess,
response,
mockedState,
[
{
type: types.RECEIVE_METRICS_SUCCESS,
payload: response,
},
],
[],
done,
);
});
});
describe('receiveMetricsError', () => {
it('should commit request mutation', done => {
testAction(
receiveMetricsError,
null,
mockedState,
[
{
type: types.RECEIVE_METRICS_ERROR,
},
],
[],
done,
);
});
});
});
import state from 'ee/vue_shared/metrics_reports/store/state';
import { summaryStatus, metrics } from 'ee/vue_shared/metrics_reports/store/getters';
import { LOADING, ERROR, SUCCESS } from 'ee/vue_shared/metrics_reports/constants';
describe('metrics reports getters', () => {
describe('summaryStatus', () => {
describe('when loading', () => {
it('returns loading status', () => {
const mockState = state();
mockState.isLoading = true;
expect(summaryStatus(mockState)).toEqual(LOADING);
});
});
describe('when there are errors', () => {
it('returns error status', () => {
const mockState = state();
mockState.hasError = true;
mockState.numberOfChanges = 0;
expect(summaryStatus(mockState)).toEqual(ERROR);
});
});
describe('when there are changes', () => {
it('returns changes status', () => {
const mockState = state();
mockState.numberOfChanges = 1;
expect(summaryStatus(mockState)).toEqual(ERROR);
});
});
describe('when successful', () => {
it('returns loading status', () => {
const mockState = state();
mockState.numberOfChanges = 0;
expect(summaryStatus(mockState)).toEqual(SUCCESS);
});
});
});
describe('metrics', () => {
describe('when state has new metrics', () => {
it('returns array with new metrics', () => {
const mockState = state();
mockState.newMetrics = [{ name: 'name', value: 'value' }];
const metricsResult = metrics(mockState);
expect(metricsResult.length).toEqual(1);
expect(metricsResult[0].name).toEqual('name');
expect(metricsResult[0].value).toEqual('value');
expect(metricsResult[0].isNew).toEqual(true);
});
});
describe('when state has existing metrics', () => {
it('returns array with existing metrics', () => {
const mockState = state();
mockState.existingMetrics = [{ name: 'name', value: 'value', previous_value: 'prev' }];
const metricsResult = metrics(mockState);
expect(metricsResult.length).toEqual(1);
expect(metricsResult[0].name).toEqual('name');
expect(metricsResult[0].value).toEqual('value');
expect(metricsResult[0].previous_value).toEqual('prev');
});
});
describe('when state has removed metrics', () => {
it('returns array with removed metrics', () => {
const mockState = state();
mockState.removedMetrics = [{ name: 'name', value: 'value' }];
const metricsResult = metrics(mockState);
expect(metricsResult.length).toEqual(1);
expect(metricsResult[0].name).toEqual('name');
expect(metricsResult[0].value).toEqual('value');
expect(metricsResult[0].wasRemoved).toEqual(true);
});
});
describe('when state has new, existing, and removed metrics', () => {
it('returns array with new, existing, and removed metrics combined', () => {
const mockState = state();
mockState.newMetrics = [{ name: 'name1', value: 'value1' }];
mockState.existingMetrics = [{ name: 'name2', value: 'value2', previous_value: 'prev' }];
mockState.removedMetrics = [{ name: 'name3', value: 'value3' }];
const metricsResult = metrics(mockState);
expect(metricsResult.length).toEqual(3);
expect(metricsResult[0].name).toEqual('name1');
expect(metricsResult[0].value).toEqual('value1');
expect(metricsResult[0].isNew).toEqual(true);
expect(metricsResult[1].name).toEqual('name2');
expect(metricsResult[1].value).toEqual('value2');
expect(metricsResult[2].name).toEqual('name3');
expect(metricsResult[2].value).toEqual('value3');
expect(metricsResult[2].wasRemoved).toEqual(true);
});
});
describe('when state has no metrics', () => {
it('returns empty array', () => {
const mockState = state();
const metricsResult = metrics(mockState);
expect(metricsResult.length).toEqual(0);
});
});
});
});
import state from 'ee/vue_shared/metrics_reports/store/state';
import mutations from 'ee/vue_shared/metrics_reports/store/mutations';
import * as types from 'ee/vue_shared/metrics_reports/store/mutation_types';
describe('metrics reports mutations', () => {
let mockState;
beforeEach(() => {
mockState = state();
});
describe('SET_ENDPOINT', () => {
it('should set endpoint', () => {
mutations[types.SET_ENDPOINT](mockState, 'endpoint');
expect(mockState.endpoint).toEqual('endpoint');
});
});
describe('REQUEST_METRICS', () => {
it('should set isLoading to true', () => {
mutations[types.REQUEST_METRICS](mockState);
expect(mockState.isLoading).toEqual(true);
});
});
describe('RECEIVE_METRICS_SUCCESS', () => {
it('should set metrics with zero changes', () => {
const data = {
existing_metrics: [
{
name: 'name',
value: 'value',
},
],
};
mutations[types.RECEIVE_METRICS_SUCCESS](mockState, data);
expect(mockState.existingMetrics[0].name).toEqual(data.existing_metrics[0].name);
expect(mockState.existingMetrics[0].value).toEqual(data.existing_metrics[0].value);
expect(mockState.numberOfChanges).toEqual(0);
expect(mockState.isLoading).toEqual(false);
});
it('should set metrics with one changes', () => {
const data = {
existing_metrics: [
{
name: 'name',
value: 'value',
previous_value: 'prev',
},
],
};
mutations[types.RECEIVE_METRICS_SUCCESS](mockState, data);
expect(mockState.existingMetrics[0].name).toEqual(data.existing_metrics[0].name);
expect(mockState.existingMetrics[0].value).toEqual(data.existing_metrics[0].value);
expect(mockState.existingMetrics[0].previous_value).toEqual(
data.existing_metrics[0].previous_value,
);
expect(mockState.numberOfChanges).toEqual(1);
expect(mockState.isLoading).toEqual(false);
});
});
describe('RECEIVE_METRICS_ERROR', () => {
it('should set endpoint', () => {
mutations[types.RECEIVE_METRICS_ERROR](mockState);
expect(mockState.hasError).toEqual(true);
expect(mockState.isLoading).toEqual(false);
});
});
});
......@@ -260,6 +260,9 @@ msgstr ""
msgid "%{user_name} profile page"
msgstr ""
msgid "(No changes)"
msgstr ""
msgid "(external source)"
msgstr ""
......@@ -8759,6 +8762,9 @@ msgstr ""
msgid "Remove this label? This will affect all projects within the group. Are you sure?"
msgstr ""
msgid "Removed"
msgstr ""
msgid "Removed group can not be restored!"
msgstr ""
......@@ -8819,6 +8825,18 @@ msgstr ""
msgid "Reports|Failure"
msgstr ""
msgid "Reports|Metrics reports are loading"
msgstr ""
msgid "Reports|Metrics reports changed on %{numberOfChanges} %{pointsString}"
msgstr ""
msgid "Reports|Metrics reports did not change"
msgstr ""
msgid "Reports|Metrics reports failed loading results"
msgstr ""
msgid "Reports|Severity"
msgstr ""
......@@ -13216,6 +13234,11 @@ msgstr ""
msgid "personal access token"
msgstr ""
msgid "point"
msgid_plural "points"
msgstr[0] ""
msgstr[1] ""
msgid "private"
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