Commit 24281d62 authored by Martin Wortschack's avatar Martin Wortschack Committed by Filipa Lacerda

Add metric chart component to app

- Leverage MetricChart component in
productivity analytics app
parent 91aea099
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import MetricChart from './metric_chart.vue';
import MergeRequestTable from './mr_table.vue'; import MergeRequestTable from './mr_table.vue';
import { chartKeys } from '../constants'; import { chartKeys } from '../constants';
...@@ -22,6 +23,7 @@ export default { ...@@ -22,6 +23,7 @@ export default {
GlColumnChart, GlColumnChart,
GlButton, GlButton,
Icon, Icon,
MetricChart,
MergeRequestTable, MergeRequestTable,
}, },
directives: { directives: {
...@@ -47,7 +49,7 @@ export default { ...@@ -47,7 +49,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('filters', ['groupNamespace', 'projectPath']), ...mapState('filters', ['groupNamespace']),
...mapState('table', ['isLoadingTable', 'mergeRequests', 'pageInfo', 'columnMetric']), ...mapState('table', ['isLoadingTable', 'mergeRequests', 'pageInfo', 'columnMetric']),
...mapGetters(['getMetricTypes']), ...mapGetters(['getMetricTypes']),
...mapGetters('charts', [ ...mapGetters('charts', [
...@@ -56,7 +58,7 @@ export default { ...@@ -56,7 +58,7 @@ export default {
'getChartData', 'getChartData',
'getColumnChartDatazoomOption', 'getColumnChartDatazoomOption',
'getMetricDropdownLabel', 'getMetricDropdownLabel',
'isSelectedMetric', 'getSelectedMetric',
'hasNoAccessError', 'hasNoAccessError',
]), ]),
...mapGetters('table', [ ...mapGetters('table', [
...@@ -85,7 +87,6 @@ export default { ...@@ -85,7 +87,6 @@ export default {
}, },
methods: { methods: {
...mapActions(['setEndpoint']), ...mapActions(['setEndpoint']),
...mapActions('filters', ['setProjectPath']),
...mapActions('charts', ['fetchChartData', 'setMetricType', 'chartItemClicked']), ...mapActions('charts', ['fetchChartData', 'setMetricType', 'chartItemClicked']),
...mapActions('table', [ ...mapActions('table', [
'setSortField', 'setSortField',
...@@ -141,136 +142,109 @@ export default { ...@@ -141,136 +142,109 @@ export default {
/> />
<template v-if="showAppContent"> <template v-if="showAppContent">
<h4>{{ __('Merge Requests') }}</h4> <h4>{{ __('Merge Requests') }}</h4>
<div class="qa-time-to-merge mb-4"> <metric-chart
<h5>{{ __('Time to merge') }}</h5> ref="mainChart"
<gl-loading-icon v-if="chartLoading(chartKeys.main)" size="md" class="my-4 py-4" /> class="mb-4"
<template v-else> :title="__('Time to merge')"
<div v-if="!chartHasData(chartKeys.main)" class="bs-callout bs-callout-info"> :description="
{{ __('There is no data available. Please change your selection.') }} __('You can filter by \'days to merge\' by clicking on the columns in the chart.')
</div> "
<template v-else> :is-loading="chartLoading(chartKeys.main)"
<p class="text-muted"> :chart-data="getChartData(chartKeys.main)"
{{ __('You can filter by "days to merge" by clicking on the columns in the chart.') }} >
</p> <gl-column-chart
<gl-column-chart :data="{ full: getChartData(chartKeys.main) }"
:data="{ full: getChartData(chartKeys.main) }" :option="getColumnChartOption(chartKeys.main)"
:option="getColumnChartOption(chartKeys.main)" :y-axis-title="__('Merge requests')"
:y-axis-title="__('Merge requests')" :x-axis-title="__('Days')"
:x-axis-title="__('Days')" x-axis-type="category"
x-axis-type="category" @chartItemClicked="onMainChartItemClicked"
@chartItemClicked="onMainChartItemClicked" />
/> </metric-chart>
</template>
</template>
</div>
<template v-if="showSecondaryCharts"> <template v-if="showSecondaryCharts">
<div class="row"> <div ref="secondaryCharts">
<div class="qa-time-based col-lg-6 col-sm-12 mb-4"> <div class="row">
<gl-loading-icon <metric-chart
v-if="chartLoading(chartKeys.timeBasedHistogram)" ref="timeBasedChart"
size="md" class="col-lg-6 col-sm-12 mb-4"
class="my-4 py-4" :description="
/> __(
<template v-else> 'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.',
<div )
v-if="!chartHasData(chartKeys.timeBasedHistogram)" "
class="bs-callout bs-callout-info" :is-loading="chartLoading(chartKeys.timeBasedHistogram)"
> :metric-types="getMetricTypes(chartKeys.timeBasedHistogram)"
{{ __('There is no data for the selected metric. Please change your selection.') }} :selected-metric="getSelectedMetric(chartKeys.timeBasedHistogram)"
</div> :chart-data="getChartData(chartKeys.timeBasedHistogram)"
<template v-else> @metricTypeChange="
<gl-dropdown metric =>
class="mb-4 metric-dropdown" setMetricType({ metricType: metric, chartKey: chartKeys.timeBasedHistogram })
toggle-class="dropdown-menu-toggle w-100" "
menu-class="w-100 mw-100" >
:text="getMetricDropdownLabel(chartKeys.timeBasedHistogram)" <gl-column-chart
> :data="{ full: getChartData(chartKeys.timeBasedHistogram) }"
<gl-dropdown-item :option="getColumnChartOption(chartKeys.timeBasedHistogram)"
v-for="metric in getMetricTypes(chartKeys.timeBasedHistogram)" :y-axis-title="__('Merge requests')"
:key="metric.key" :x-axis-title="__('Hours')"
active-class="is-active" x-axis-type="category"
class="w-100" />
@click=" </metric-chart>
setMetricType({
metricType: metric.key, <metric-chart
chartKey: chartKeys.timeBasedHistogram, ref="commitBasedChart"
}) class="col-lg-6 col-sm-12 mb-4"
" :description="
> __(
<span class="d-flex"> 'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.',
<icon )
class="flex-shrink-0 append-right-4" "
:class="{ :is-loading="chartLoading(chartKeys.commitBasedHistogram)"
invisible: !isSelectedMetric({ :metric-types="getMetricTypes(chartKeys.commitBasedHistogram)"
metric: metric.key, :selected-metric="getSelectedMetric(chartKeys.commitBasedHistogram)"
chartKey: chartKeys.timeBasedHistogram, :chart-data="getChartData(chartKeys.commitBasedHistogram)"
}), @metricTypeChange="
}" metric =>
name="mobile-issue-close" setMetricType({ metricType: metric, chartKey: chartKeys.commitBasedHistogram })
/> "
{{ metric.label }} >
</span> <gl-column-chart
</gl-dropdown-item> :data="{ full: getChartData(chartKeys.commitBasedHistogram) }"
</gl-dropdown> :option="getColumnChartOption(chartKeys.commitBasedHistogram)"
<p class="text-muted"> :y-axis-title="__('Merge requests')"
{{ :x-axis-title="getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
__( x-axis-type="category"
'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.', />
) </metric-chart>
}}
</p>
<gl-column-chart
:data="{ full: getChartData(chartKeys.timeBasedHistogram) }"
:option="getColumnChartOption(chartKeys.timeBasedHistogram)"
:y-axis-title="__('Merge requests')"
:x-axis-title="__('Hours')"
x-axis-type="category"
/>
</template>
</template>
</div> </div>
<div class="qa-commit-based col-lg-6 col-sm-12 mb-4"> <div
<gl-loading-icon class="js-mr-table-sort d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2"
v-if="chartLoading(chartKeys.commitBasedHistogram)" >
size="md" <h5>{{ __('List') }}</h5>
class="my-4 py-4" <div
/> v-if="showMergeRequestTable"
<template v-else> class="d-flex flex-column flex-md-row align-items-md-center"
<div >
v-if="!chartHasData(chartKeys.commitBasedHistogram)" <strong class="mr-2">{{ __('Sort by') }}</strong>
class="bs-callout bs-callout-info" <div class="d-flex">
>
{{ __('There is no data for the selected metric. Please change your selection.') }}
</div>
<template v-else>
<gl-dropdown <gl-dropdown
class="mb-4 metric-dropdown" class="mr-2 flex-grow"
toggle-class="dropdown-menu-toggle w-100" toggle-class="dropdown-menu-toggle"
menu-class="w-100 mw-100" :text="sortFieldDropdownLabel"
:text="getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="metric in getMetricTypes(chartKeys.commitBasedHistogram)" v-for="metric in tableSortOptions"
:key="metric.key" :key="metric.key"
active-class="is-active" active-class="is-active"
class="w-100" class="w-100"
@click=" @click="setSortField(metric.key)"
setMetricType({
metricType: metric.key,
chartKey: chartKeys.commitBasedHistogram,
})
"
> >
<span class="d-flex"> <span class="d-flex">
<icon <icon
class="flex-shrink-0 append-right-4" class="flex-shrink-0 append-right-4"
:class="{ :class="{
invisible: !isSelectedMetric({ invisible: !isSelectedSortField(metric.key),
metric: metric.key,
chartKey: chartKeys.commitBasedHistogram,
}),
}" }"
name="mobile-issue-close" name="mobile-issue-close"
/> />
...@@ -278,66 +252,16 @@ export default { ...@@ -278,66 +252,16 @@ export default {
</span> </span>
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
<p class="text-muted"> <gl-button v-gl-tooltip.hover :title="sortTooltipTitle" @click="toggleSortOrder">
{{ <icon :name="sortIcon" />
__( </gl-button>
'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.', </div>
)
}}
</p>
<gl-column-chart
:data="{ full: getChartData(chartKeys.commitBasedHistogram) }"
:option="getColumnChartOption(chartKeys.commitBasedHistogram)"
:y-axis-title="__('Merge requests')"
:x-axis-title="getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
x-axis-type="category"
/>
</template>
</template>
</div>
</div>
<div
class="qa-mr-table-sort d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2"
>
<h5>{{ __('List') }}</h5>
<div
v-if="showMergeRequestTable"
class="d-flex flex-column flex-md-row align-items-md-center"
>
<strong class="mr-2">{{ __('Sort by') }}</strong>
<div class="d-flex">
<gl-dropdown
class="mr-2 flex-grow"
toggle-class="dropdown-menu-toggle"
:text="sortFieldDropdownLabel"
>
<gl-dropdown-item
v-for="metric in tableSortOptions"
:key="metric.key"
active-class="is-active"
class="w-100"
@click="setSortField(metric.key)"
>
<span class="d-flex">
<icon
class="flex-shrink-0 append-right-4"
:class="{
invisible: !isSelectedSortField(metric.key),
}"
name="mobile-issue-close"
/>
{{ metric.label }}
</span>
</gl-dropdown-item>
</gl-dropdown>
<gl-button v-gl-tooltip.hover :title="sortTooltipTitle" @click="toggleSortOrder">
<icon :name="sortIcon" />
</gl-button>
</div> </div>
</div> </div>
</div> </div>
<div class="qa-mr-table">
<div class="js-mr-table">
<div ref="foo"></div>
<gl-loading-icon v-if="isLoadingTable" size="md" class="my-4 py-4" /> <gl-loading-icon v-if="isLoadingTable" size="md" class="my-4 py-4" />
<merge-request-table <merge-request-table
v-if="showMergeRequestTable" v-if="showMergeRequestTable"
......
...@@ -107,8 +107,7 @@ export const getColumnChartDatazoomOption = state => chartKey => { ...@@ -107,8 +107,7 @@ export const getColumnChartDatazoomOption = state => chartKey => {
}; };
}; };
export const isSelectedMetric = state => ({ metric, chartKey }) => export const getSelectedMetric = state => chartKey => state.charts[chartKey].params.metricType;
state.charts[chartKey].params.metricType === metric;
export const hasNoAccessError = state => export const hasNoAccessError = state =>
state.charts[chartKeys.main].errorCode === httpStatus.FORBIDDEN; state.charts[chartKeys.main].errorCode === httpStatus.FORBIDDEN;
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import ProductivityApp from 'ee/analytics/productivity_analytics/components/app.vue'; import ProductivityApp from 'ee/analytics/productivity_analytics/components/app.vue';
import MergeRequestTable from 'ee/analytics/productivity_analytics/components/mr_table.vue'; import MergeRequestTable from 'ee/analytics/productivity_analytics/components/mr_table.vue';
import store from 'ee/analytics/productivity_analytics/store'; import store from 'ee/analytics/productivity_analytics/store';
...@@ -7,13 +9,13 @@ import { chartKeys } from 'ee/analytics/productivity_analytics/constants'; ...@@ -7,13 +9,13 @@ import { chartKeys } from 'ee/analytics/productivity_analytics/constants';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { GlEmptyState, GlLoadingIcon, GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import resetStore from '../helpers';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('ProductivityApp component', () => { describe('ProductivityApp component', () => {
let wrapper; let wrapper;
let mock;
const propsData = { const propsData = {
endpoint: TEST_HOST, endpoint: TEST_HOST,
...@@ -30,7 +32,10 @@ describe('ProductivityApp component', () => { ...@@ -30,7 +32,10 @@ describe('ProductivityApp component', () => {
setColumnMetric: jest.fn(), setColumnMetric: jest.fn(),
}; };
const mainChartData = { 1: 2, 2: 3 };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = shallowMount(localVue.extend(ProductivityApp), { wrapper = shallowMount(localVue.extend(ProductivityApp), {
localVue, localVue,
store, store,
...@@ -40,23 +45,22 @@ describe('ProductivityApp component', () => { ...@@ -40,23 +45,22 @@ describe('ProductivityApp component', () => {
...actionSpies, ...actionSpies,
}, },
}); });
jest.spyOn(store, 'dispatch').mockImplementation();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
resetStore(store); mock.restore();
}); });
const findTimeToMergeSection = () => wrapper.find('.qa-time-to-merge'); const findMainMetricChart = () => wrapper.find({ ref: 'mainChart' });
const findMrTableSortSection = () => wrapper.find('.qa-mr-table-sort'); const findSecondaryChartsSection = () => wrapper.find({ ref: 'secondaryCharts' });
const findMrTableSection = () => wrapper.find('.qa-mr-table'); const findTimeBasedMetricChart = () => wrapper.find({ ref: 'timeBasedChart' });
const findMrTable = () => findMrTableSection().find(MergeRequestTable); const findCommitBasedMetricChart = () => wrapper.find({ ref: 'commitBasedChart' });
const findMrTableSortSection = () => wrapper.find('.js-mr-table-sort');
const findSortFieldDropdown = () => findMrTableSortSection().find(GlDropdown); const findSortFieldDropdown = () => findMrTableSortSection().find(GlDropdown);
const findSortOrderToggle = () => findMrTableSortSection().find(GlButton); const findSortOrderToggle = () => findMrTableSortSection().find(GlButton);
const findTimeBasedSection = () => wrapper.find('.qa-time-based'); const findMrTableSection = () => wrapper.find('.js-mr-table');
const findCommitBasedSection = () => wrapper.find('.qa-commit-based'); const findMrTable = () => findMrTableSection().find(MergeRequestTable);
describe('template', () => { describe('template', () => {
describe('without a group being selected', () => { describe('without a group being selected', () => {
...@@ -70,12 +74,18 @@ describe('ProductivityApp component', () => { ...@@ -70,12 +74,18 @@ describe('ProductivityApp component', () => {
describe('with a group being selected', () => { describe('with a group being selected', () => {
beforeEach(() => { beforeEach(() => {
store.state.filters.groupNamespace = 'gitlab-org'; wrapper.vm.$store.dispatch('filters/setGroupNamespace', 'gitlab-org');
mock.onGet(wrapper.vm.$store.state.endpoint).replyOnce(200);
}); });
describe('and user has no access to the group', () => { describe('user has no access to the group', () => {
beforeEach(() => { beforeEach(() => {
store.state.charts.charts[chartKeys.main].errorCode = 403; const error = { response: { status: 403 } };
wrapper.vm.$store.dispatch('charts/receiveChartDataError', {
chartKey: chartKeys.main,
error,
});
wrapper.vm.$store.state.charts.charts[chartKeys.main].errorCode = 403;
}); });
it('renders the no access illustration', () => { it('renders the no access illustration', () => {
...@@ -86,350 +96,279 @@ describe('ProductivityApp component', () => { ...@@ -86,350 +96,279 @@ describe('ProductivityApp component', () => {
}); });
}); });
describe('and user has access to the group', () => { describe('user has access to the group', () => {
beforeEach(() => { beforeEach(() => {
store.state.charts.charts[chartKeys.main].errorCode = null; wrapper.vm.$store.state.charts.charts[chartKeys.main].errorCode = null;
}); });
describe('Time to merge chart', () => { describe('when the main chart is loading', () => {
it('renders the title', () => { beforeEach(() => {
expect(findTimeToMergeSection().text()).toContain('Time to merge'); wrapper.vm.$store.dispatch('charts/requestChartData', chartKeys.main);
}); });
describe('when chart is loading', () => { it('renders a metric chart component for the main chart', () => {
beforeEach(() => { expect(findMainMetricChart().exists()).toBe(true);
store.state.charts.charts[chartKeys.main].isLoading = true;
});
it('renders a loading indicator', () => {
expect(
findTimeToMergeSection()
.find(GlLoadingIcon)
.exists(),
).toBe(true);
});
}); });
describe('when chart finished loading', () => { it('sets isLoading=true on the metric chart', () => {
beforeEach(() => { expect(findMainMetricChart().props('isLoading')).toBe(true);
store.state.charts.charts[chartKeys.main].isLoading = false; });
});
describe('and the chart has data', () => {
beforeEach(() => {
store.state.charts.charts[chartKeys.main].data = { 1: 2, 2: 3 };
});
it('renders a column chart', () => {
expect(
findTimeToMergeSection()
.find(GlColumnChart)
.exists(),
).toBe(true);
});
describe('when an item on the chart is clicked', () => {
beforeEach(() => {
const data = {
chart: null,
params: {
data: {
value: [0, 1],
},
},
};
findTimeToMergeSection()
.find(GlColumnChart)
.vm.$emit('chartItemClicked', data);
});
it('dispatches chartItemClicked action', () => {
expect(actionSpies.chartItemClicked).toHaveBeenCalledWith({
chartKey: chartKeys.main,
item: 0,
});
});
it('dispatches setMergeRequestsPage action', () => {
expect(actionSpies.setMergeRequestsPage).toHaveBeenCalledWith(0);
});
});
});
describe("and the chart doesn't have any data", () => {
beforeEach(() => {
store.state.charts.charts[chartKeys.main].data = null;
});
it('renders a "no data" message', () => { it('does not render any other charts', () => {
expect(findTimeToMergeSection().text()).toContain( expect(findSecondaryChartsSection().exists()).toBe(false);
'There is no data available. Please change your selection.',
);
});
});
}); });
});
describe('Time based histogram', () => { it('does not render the MR table', () => {
beforeEach(() => { expect(findMrTableSortSection().exists()).toBe(false);
store.state.charts.charts[chartKeys.main].isLoading = false; expect(findMrTableSection().exists()).toBe(false);
store.state.charts.charts[chartKeys.main].data = { 1: 2, 2: 3 };
}); });
});
describe('when chart is loading', () => { describe('when the main chart finished loading', () => {
describe('and has data', () => {
beforeEach(() => { beforeEach(() => {
store.state.charts.charts[chartKeys.timeBasedHistogram].isLoading = true; wrapper.vm.$store.dispatch('charts/receiveChartDataSuccess', {
chartKey: chartKeys.main,
data: mainChartData,
});
}); });
it('renders a loading indicator', () => { it('sets isLoading=false on the metric chart', () => {
expect( expect(findMainMetricChart().props('isLoading')).toBe(false);
findTimeBasedSection()
.find(GlLoadingIcon)
.exists(),
).toBe(true);
}); });
});
describe('when chart finished loading', () => { it('passes non-empty chartData to the metric chart', () => {
beforeEach(() => { expect(findMainMetricChart().props('chartData')).not.toEqual([]);
store.state.charts.charts[chartKeys.timeBasedHistogram].isLoading = false;
}); });
describe('and the chart has data', () => { describe('when an item on the chart is clicked', () => {
beforeEach(() => { beforeEach(() => {
store.state.charts.charts[chartKeys.timeBasedHistogram].data = { 1: 2, 2: 3 }; const data = {
}); chart: null,
params: {
data: {
value: [0, 1],
},
},
};
it('renders a metric type dropdown', () => { findMainMetricChart()
expect( .find(GlColumnChart)
findTimeBasedSection() .vm.$emit('chartItemClicked', data);
.find(GlDropdown)
.exists(),
).toBe(true);
}); });
it('should change the metric type', () => { it('dispatches chartItemClicked action', () => {
findTimeBasedSection() expect(actionSpies.chartItemClicked).toHaveBeenCalledWith({
.findAll(GlDropdownItem) chartKey: chartKeys.main,
.at(0) item: 0,
.vm.$emit('click');
expect(actionSpies.setMetricType).toHaveBeenCalledWith({
metricType: 'time_to_first_comment',
chartKey: chartKeys.timeBasedHistogram,
}); });
}); });
it('renders a column chart', () => { it('dispatches setMergeRequestsPage action', () => {
expect( expect(actionSpies.setMergeRequestsPage).toHaveBeenCalledWith(0);
findTimeBasedSection()
.find(GlColumnChart)
.exists(),
).toBe(true);
}); });
}); });
describe("and the chart doesn't have any data", () => { describe('Time based histogram', () => {
beforeEach(() => { it('renders a metric chart component', () => {
store.state.charts.charts[chartKeys.timeBasedHistogram].data = null; expect(findTimeBasedMetricChart().exists()).toBe(true);
}); });
it('renders a "no data" message', () => { describe('when chart finished loading', () => {
expect(findTimeBasedSection().text()).toContain( describe('and the chart has data', () => {
'There is no data for the selected metric. Please change your selection.', beforeEach(() => {
); wrapper.vm.$store.dispatch('charts/receiveChartDataSuccess', {
}); chartKey: chartKeys.timeBasedHistogram,
}); data: { 1: 2, 2: 3 },
}); });
}); });
describe('Commit based histogram', () => { it('sets isLoading=false on the metric chart', () => {
beforeEach(() => { expect(findTimeBasedMetricChart().props('isLoading')).toBe(false);
store.state.charts.charts[chartKeys.main].isLoading = false; });
store.state.charts.charts[chartKeys.main].data = { 1: 2, 2: 3 };
});
describe('when chart is loading', () => { it('passes non-empty chartData to the metric chart', () => {
beforeEach(() => { expect(findTimeBasedMetricChart().props('chartData')).not.toEqual([]);
store.state.charts.charts[chartKeys.commitBasedHistogram].isLoading = true; });
});
it('renders a loading indicator', () => { it('should call setMetricType when `metricTypeChange` is emitted on the metric chart', () => {
expect( findTimeBasedMetricChart().vm.$emit('metricTypeChange', 'time_to_merge');
findCommitBasedSection()
.find(GlLoadingIcon)
.exists(),
).toBe(true);
});
});
describe('when chart finished loading', () => { expect(actionSpies.setMetricType).toHaveBeenCalledWith({
beforeEach(() => { metricType: 'time_to_merge',
store.state.charts.charts[chartKeys.commitBasedHistogram].isLoading = false; chartKey: chartKeys.timeBasedHistogram,
});
});
});
});
}); });
describe('and the chart has data', () => { describe('Commit based histogram', () => {
beforeEach(() => { it('renders a metric chart component', () => {
store.state.charts.charts[chartKeys.commitBasedHistogram].data = { 1: 2, 2: 3 }; expect(findCommitBasedMetricChart().exists()).toBe(true);
}); });
it('renders a column chart', () => { describe('when chart finished loading', () => {
expect( describe('and the chart has data', () => {
findCommitBasedSection() beforeEach(() => {
.find(GlColumnChart) wrapper.vm.$store.dispatch('charts/receiveChartDataSuccess', {
.exists(), chartKey: chartKeys.commitBasedHistogram,
).toBe(true); data: { 1: 2, 2: 3 },
}); });
});
describe('metric dropdown', () => { it('sets isLoading=false on the metric chart', () => {
it('renders a metric type dropdown', () => { expect(findCommitBasedMetricChart().props('isLoading')).toBe(false);
expect( });
findCommitBasedSection()
.find(GlDropdown)
.exists(),
).toBe(true);
});
describe('when the user changes the metric', () => { it('passes non-empty chartData to the metric chart', () => {
beforeEach(() => { expect(findCommitBasedMetricChart().props('chartData')).not.toEqual([]);
findCommitBasedSection()
.findAll(GlDropdownItem)
.at(0)
.vm.$emit('click');
}); });
it('should dispatch setMetricType action', () => { describe('when the user changes the metric', () => {
expect(actionSpies.setMetricType).toHaveBeenCalledWith({ beforeEach(() => {
metricType: 'commits_count', findCommitBasedMetricChart().vm.$emit('metricTypeChange', 'loc_per_commit');
chartKey: chartKeys.commitBasedHistogram,
}); });
});
it("should update the chart's x axis label", () => { it('should call setMetricType when `metricTypeChange` is emitted on the metric chart', () => {
const columnChart = findCommitBasedSection().find(GlColumnChart); expect(actionSpies.setMetricType).toHaveBeenCalledWith({
expect(columnChart.props('xAxisTitle')).toBe('Number of commits per MR'); metricType: 'loc_per_commit',
chartKey: chartKeys.commitBasedHistogram,
});
});
it("should update the chart's x axis label", () => {
const columnChart = findCommitBasedMetricChart().find(GlColumnChart);
expect(columnChart.props('xAxisTitle')).toBe('Number of commits per MR');
});
}); });
}); });
}); });
}); });
describe("and the chart doesn't have any data", () => { describe('MR table', () => {
beforeEach(() => { describe('when table is loading', () => {
store.state.charts.charts[chartKeys.commitBasedHistogram].data = null; beforeEach(() => {
}); wrapper.vm.$store.dispatch('table/requestMergeRequests');
});
it('renders a "no data" message', () => { it('renders a loading indicator', () => {
expect(findTimeBasedSection().text()).toContain( expect(
'There is no data for the selected metric. Please change your selection.', findMrTableSection()
); .find(GlLoadingIcon)
.exists(),
).toBe(true);
});
}); });
});
});
});
describe('MR table', () => {
beforeEach(() => {
store.state.charts.charts[chartKeys.main].isLoading = false;
store.state.charts.charts[chartKeys.main].data = { 1: 2, 2: 3 };
});
describe('when table is loading', () => { describe('when table finished loading', () => {
beforeEach(() => { describe('and the table has data', () => {
store.state.table.isLoadingTable = true; beforeEach(() => {
}); wrapper.vm.$store.dispatch('table/receiveMergeRequestsSuccess', {
headers: {},
data: [{ id: 1, title: 'This is a test MR' }],
});
});
it('renders a loading indicator', () => { it('renders the MR table', () => {
expect( expect(findMrTable().exists()).toBe(true);
findMrTableSection() });
.find(GlLoadingIcon)
.exists(),
).toBe(true);
});
});
describe('when table finished loading', () => { it('doesn’t render a "no data" message', () => {
beforeEach(() => { expect(
store.state.table.isLoadingTable = false; findMrTableSection()
}); .find('.js-no-data')
.exists(),
).toBe(false);
});
describe('and the table has data', () => { it('should change the column metric', () => {
beforeEach(() => { findMrTable().vm.$emit('columnMetricChange', 'time_to_first_comment');
store.state.table.mergeRequests = [{ id: 1, title: 'This is a test MR' }]; expect(actionSpies.setColumnMetric).toHaveBeenCalledWith(
}); 'time_to_first_comment',
);
});
it('renders the MR table', () => { it('should change the page', () => {
expect(findMrTable().exists()).toBe(true); const page = 2;
}); findMrTable().vm.$emit('pageChange', page);
expect(actionSpies.setMergeRequestsPage).toHaveBeenCalledWith(page);
});
it('doesn’t render a "no data" message', () => { describe('sort controls', () => {
expect( it('renders the sort dropdown and button', () => {
findMrTableSection() expect(findSortFieldDropdown().exists()).toBe(true);
.find('.js-no-data') expect(findSortOrderToggle().exists()).toBe(true);
.exists(), });
).toBe(false);
});
it('should change the column metric', () => { it('should change the sort field', () => {
findMrTable().vm.$emit('columnMetricChange', 'time_to_first_comment'); findSortFieldDropdown()
expect(actionSpies.setColumnMetric).toHaveBeenCalledWith('time_to_first_comment'); .findAll(GlDropdownItem)
}); .at(0)
.vm.$emit('click');
it('should change the page', () => { expect(actionSpies.setSortField).toHaveBeenCalled();
const page = 2; });
findMrTable().vm.$emit('pageChange', page);
expect(actionSpies.setMergeRequestsPage).toHaveBeenCalledWith(page);
});
describe('and there are merge requests available', () => { it('should toggle the sort order', () => {
beforeEach(() => { findSortOrderToggle().vm.$emit('click');
store.state.table.mergeRequests = [{ id: 1 }]; expect(actionSpies.toggleSortOrder).toHaveBeenCalled();
});
});
}); });
describe('sort controls', () => { describe("and the table doesn't have any data", () => {
it('renders the sort dropdown and button', () => { beforeEach(() => {
expect(findSortFieldDropdown().exists()).toBe(true); wrapper.vm.$store.dispatch('table/receiveMergeRequestsSuccess', {
expect(findSortOrderToggle().exists()).toBe(true); headers: {},
data: [],
});
}); });
it('should change the sort field', () => { it('renders a "no data" message', () => {
findSortFieldDropdown() expect(
.findAll(GlDropdownItem) findMrTableSection()
.at(0) .find('.js-no-data')
.vm.$emit('click'); .exists(),
).toBe(true);
});
expect(actionSpies.setSortField).toHaveBeenCalled(); it('doesn`t render the MR table', () => {
expect(findMrTable().exists()).not.toBe(true);
}); });
it('should toggle the sort order', () => { it('doesn`t render the sort dropdown and button', () => {
findSortOrderToggle().vm.$emit('click'); expect(findSortFieldDropdown().exists()).not.toBe(true);
expect(actionSpies.toggleSortOrder).toHaveBeenCalled(); expect(findSortOrderToggle().exists()).not.toBe(true);
}); });
}); });
}); });
}); });
});
describe("and the table doesn't have any data", () => { describe('and has no data', () => {
beforeEach(() => { beforeEach(() => {
store.state.table.mergeRequests = []; wrapper.vm.$store.dispatch('charts/receiveChartDataSuccess', {
chartKey: chartKeys.main,
data: {},
}); });
});
it('renders a "no data" message', () => { it('sets isLoading=false on the metric chart', () => {
expect( expect(findMainMetricChart().props('isLoading')).toBe(false);
findMrTableSection() });
.find('.js-no-data')
.exists(),
).toBe(true);
});
it('doesn`t render the MR table', () => { it('passes an empty array as chartData to the metric chart', () => {
expect(findMrTable().exists()).not.toBe(true); expect(findMainMetricChart().props('chartData')).toEqual([]);
}); });
it('doesn`t render the sort dropdown and button', () => { it('does not render any other charts', () => {
expect(findSortFieldDropdown().exists()).not.toBe(true); expect(findSecondaryChartsSection().exists()).toBe(false);
expect(findSortOrderToggle().exists()).not.toBe(true); });
});
it('does not render the MR table', () => {
expect(findMrTableSortSection().exists()).toBe(false);
expect(findMrTableSection().exists()).toBe(false);
}); });
}); });
}); });
......
...@@ -178,6 +178,19 @@ describe('Productivity analytics chart getters', () => { ...@@ -178,6 +178,19 @@ describe('Productivity analytics chart getters', () => {
}); });
}); });
describe('getSelectedMetric', () => {
it('returns the currently selected metric for a given chartKey', () => {
const metricType = 'time_to_last_commit';
state.charts[chartKeys.timeBasedHistogram].params = {
metricType,
};
expect(getters.getSelectedMetric(state)(chartKeys.timeBasedHistogram)).toBe(
'time_to_last_commit',
);
});
});
describe('hasNoAccessError', () => { describe('hasNoAccessError', () => {
it('returns true if errorCode is set to 403', () => { it('returns true if errorCode is set to 403', () => {
state.charts[chartKeys.main].errorCode = 403; state.charts[chartKeys.main].errorCode = 403;
......
...@@ -15728,9 +15728,6 @@ msgstr "" ...@@ -15728,9 +15728,6 @@ msgstr ""
msgid "There is no data available. Please change your selection." msgid "There is no data available. Please change your selection."
msgstr "" msgstr ""
msgid "There is no data for the selected metric. Please change your selection."
msgstr ""
msgid "There was a problem communicating with your device." msgid "There was a problem communicating with your device."
msgstr "" msgstr ""
...@@ -17981,7 +17978,7 @@ msgstr "" ...@@ -17981,7 +17978,7 @@ msgstr ""
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr "" msgstr ""
msgid "You can filter by \"days to merge\" by clicking on the columns in the chart." msgid "You can filter by 'days to merge' by clicking on the columns in the chart."
msgstr "" msgstr ""
msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group." msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group."
......
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