Commit 895cb36f authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '197336-value-stream-analytics-move-duration-chart-to-separate-module' into 'master'

Move duration chart to separate component

See merge request gitlab-org/gitlab!28624
parents c3e7e5c4 8d8555f1
......@@ -6,11 +6,10 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PROJECTS_PER_PAGE } from '../constants';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
import Scatterplot from '../../shared/components/scatterplot.vue';
import { LAST_ACTIVITY_AT, dateFormats, DATE_RANGE_LIMIT } from '../../shared/constants';
import { LAST_ACTIVITY_AT, DATE_RANGE_LIMIT } from '../../shared/constants';
import DateRange from '../../shared/components/daterange.vue';
import StageDropdownFilter from './stage_dropdown_filter.vue';
import StageTable from './stage_table.vue';
import DurationChart from './duration_chart.vue';
import TasksByTypeChart from './tasks_by_type_chart.vue';
import UrlSyncMixin from '../../shared/mixins/url_sync_mixin';
import { toYmd } from '../../shared/utils';
......@@ -20,13 +19,12 @@ export default {
name: 'CycleAnalytics',
components: {
DateRange,
DurationChart,
GlLoadingIcon,
GlEmptyState,
GroupsDropdownFilter,
ProjectsDropdownFilter,
StageTable,
StageDropdownFilter,
Scatterplot,
TasksByTypeChart,
RecentActivityCard,
},
......@@ -214,7 +212,6 @@ export default {
order_by: LAST_ACTIVITY_AT,
include_subgroups: true,
},
durationChartTooltipDateFormat: dateFormats.defaultDate,
maxDateRange: DATE_RANGE_LIMIT,
};
</script>
......@@ -323,31 +320,15 @@ export default {
/>
</div>
</div>
<template v-if="shouldDisplayDurationChart">
<template v-if="isDurationChartLoaded">
<div class="mt-3 d-flex">
<h4 class="mt-0">{{ s__('CycleAnalytics|Days to completion') }}</h4>
<stage-dropdown-filter
v-if="activeStages.length"
class="ml-auto"
<div v-if="shouldDisplayDurationChart" class="mt-3">
<duration-chart
:is-loading="isLoading"
:stages="activeStages"
@selected="onDurationStageSelect"
/>
</div>
<scatterplot
v-if="durationChartPlottableData"
:x-axis-title="s__('CycleAnalytics|Date')"
:y-axis-title="s__('CycleAnalytics|Total days to completion')"
:tooltip-date-format="$options.durationChartTooltipDateFormat"
:scatter-data="durationChartPlottableData"
:median-line-data="durationChartMedianData"
@stageSelected="onDurationStageSelect"
/>
<div v-else ref="duration-chart-no-data" class="bs-callout bs-callout-info">
{{ __('There is no data available. Please change your selection.') }}
</div>
</template>
<gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" />
</template>
<template v-if="shouldDisplayTasksByTypeChart">
<div class="js-tasks-by-type-chart">
<div v-if="isTasksByTypeChartLoaded">
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { dateFormats } from '../../shared/constants';
import Scatterplot from '../../shared/components/scatterplot.vue';
import StageDropdownFilter from './stage_dropdown_filter.vue';
export default {
name: 'DurationChart',
components: {
GlLoadingIcon,
Scatterplot,
StageDropdownFilter,
},
props: {
isLoading: {
type: Boolean,
required: false,
default: false,
},
stages: {
type: Array,
required: true,
},
scatterData: {
type: Array,
required: true,
},
medianLineData: {
type: Array,
required: true,
},
},
computed: {
hasData() {
return Boolean(this.scatterData.length);
},
},
methods: {
onSelectStage(selectedStages) {
this.$emit('stageSelected', selectedStages);
},
},
durationChartTooltipDateFormat: dateFormats.defaultDate,
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="md" class="my-4 py-4" />
<div v-else>
<div class="d-flex">
<h4 class="mt-0">{{ s__('CycleAnalytics|Days to completion') }}</h4>
<stage-dropdown-filter
v-if="stages.length"
class="ml-auto"
:stages="stages"
@selected="onSelectStage"
/>
</div>
<scatterplot
v-if="hasData"
:x-axis-title="s__('CycleAnalytics|Date')"
:y-axis-title="s__('CycleAnalytics|Total days to completion')"
:tooltip-date-format="$options.durationChartTooltipDateFormat"
:scatter-data="scatterData"
:median-line-data="medianLineData"
/>
<div v-else ref="duration-chart-no-data" class="bs-callout bs-callout-info">
{{ __('There is no data available. Please change your selection.') }}
</div>
</div>
</template>
......@@ -23,7 +23,7 @@ export const durationChartPlottableData = state => {
const selectedStagesDurationData = durationData.filter(stage => stage.selected);
const plottableData = getDurationChartData(selectedStagesDurationData, startDate, endDate);
return plottableData.length ? plottableData : null;
return plottableData.length ? plottableData : [];
};
export const durationChartMedianData = state => {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DurationChart renders the duration chart 1`] = `
"<div>
<div class=\\"d-flex\\">
<h4 class=\\"mt-0\\">Days to completion</h4>
<stagedropdownfilter-stub stages=\\"[object Object],[object Object],[object Object]\\" label=\\"stage dropdown\\" class=\\"ml-auto\\"></stagedropdownfilter-stub>
</div>
<scatterplot-stub xaxistitle=\\"Date\\" yaxistitle=\\"Total days to completion\\" scatterdata=\\"2019-01-01,29,2019-01-01,2019-01-02,100,2019-01-02\\" medianlinedata=\\"2018-12-31,29,2019-01-01,100\\" tooltipdateformat=\\"mmm d, yyyy\\"></scatterplot-stub>
</div>"
`;
......@@ -12,7 +12,7 @@ import RecentActivityCard from 'ee/analytics/cycle_analytics/components/recent_a
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import 'bootstrap';
import '~/gl_dropdown';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import Daterange from 'ee/analytics/shared/components/daterange.vue';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -39,6 +39,7 @@ const defaultStubs = {
'stage-nav-item': true,
'tasks-by-type-chart': true,
'labels-selector': true,
DurationChart: true,
};
function createComponent({
......@@ -127,8 +128,8 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(StageTable).exists()).toBe(flag);
};
const displaysDurationScatterPlot = flag => {
expect(wrapper.find(Scatterplot).exists()).toBe(flag);
const displaysDurationChart = flag => {
expect(wrapper.find(DurationChart).exists()).toBe(flag);
};
const displaysTasksByType = flag => {
......@@ -183,8 +184,8 @@ describe('Cycle Analytics component', () => {
displaysStageTable(false);
});
it('does not display the duration scatter plot', () => {
displaysDurationScatterPlot(false);
it('does not display the duration chart', () => {
displaysDurationChart(false);
});
describe('hideGroupDropDown = true', () => {
......@@ -206,7 +207,10 @@ describe('Cycle Analytics component', () => {
describe('after a filter has been selected', () => {
describe('the user has access to the group', () => {
beforeEach(() => {
wrapper = createComponent({ withStageSelected: true, tasksByTypeChartEnabled: false });
wrapper = createComponent({
withStageSelected: true,
tasksByTypeChartEnabled: false,
});
});
it('hides the empty state', () => {
......@@ -243,16 +247,7 @@ describe('Cycle Analytics component', () => {
describe('with no durationData', () => {
it('displays the duration chart', () => {
expect(wrapper.find(Scatterplot).exists()).toBe(false);
});
it('displays the no data message', () => {
const element = wrapper.find({ ref: 'duration-chart-no-data' });
expect(element.exists()).toBe(true);
expect(element.text()).toBe(
'There is no data available. Please change your selection.',
);
displaysDurationChart(true);
});
});
......@@ -271,7 +266,7 @@ describe('Cycle Analytics component', () => {
});
it('displays the duration chart', () => {
expect(wrapper.find(Scatterplot).exists()).toBe(true);
expect(wrapper.find(DurationChart).exists()).toBe(true);
});
});
......@@ -357,7 +352,7 @@ describe('Cycle Analytics component', () => {
});
it('does not display the duration chart', () => {
displaysDurationScatterPlot(false);
displaysDurationChart(false);
});
});
......
import { shallowMount, mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import 'bootstrap';
import '~/gl_dropdown';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
import {
allowedStages as stages,
durationChartPlottableData as scatterData,
durationChartPlottableMedianData as medianLineData,
} from '../mock_data';
function createComponent({ mountFn = shallowMount, props = {}, stubs = {} } = {}) {
return mountFn(DurationChart, {
propsData: {
isLoading: false,
stages,
scatterData,
medianLineData,
...props,
},
stubs: {
GlLoadingIcon: true,
Scatterplot: true,
StageDropdownFilter: true,
...stubs,
},
});
}
describe('DurationChart', () => {
let wrapper;
const findNoDataContainer = _wrapper => _wrapper.find({ ref: 'duration-chart-no-data' });
const findScatterPlot = _wrapper => _wrapper.find(Scatterplot);
const findStageDropdown = _wrapper => _wrapper.find(StageDropdownFilter);
const findLoader = _wrapper => _wrapper.find(GlLoadingIcon);
const openStageDropdown = _wrapper => {
$(findStageDropdown(_wrapper).element).trigger('shown.bs.dropdown');
return _wrapper.vm.$nextTick();
};
const selectStage = (_wrapper, index = 0) => {
findStageDropdown(_wrapper)
.findAll('a')
.at(index)
.trigger('click');
return _wrapper.vm.$nextTick();
};
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the duration chart', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('renders the scatter plot', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true);
});
it('renders the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(true);
});
describe('when a stage is selected', () => {
const selectedIndex = 1;
const selectedStages = stages.filter((_, index) => index !== selectedIndex);
beforeEach(() => {
wrapper = createComponent({ mountFn: mount, stubs: { StageDropdownFilter: false } });
return openStageDropdown(wrapper).then(() => selectStage(wrapper, selectedIndex));
});
it('emits the stageSelected event', () => {
expect(wrapper.emitted().stageSelected).toBeTruthy();
});
it('toggles the selected stage', () => {
expect(wrapper.emitted('stageSelected')[0]).toEqual([selectedStages]);
return selectStage(wrapper, selectedIndex).then(() => {
const [updatedStages] = wrapper.emitted('stageSelected')[1];
stages.forEach(stage => {
expect(updatedStages).toContain(stage);
});
});
});
});
describe('with no chart data', () => {
beforeEach(() => {
wrapper = createComponent({ props: { scatterData: [], medianLineData: [] } });
});
it('renders the no data available message', () => {
expect(findNoDataContainer(wrapper).text()).toEqual(
'There is no data available. Please change your selection.',
);
});
});
describe('while loading', () => {
beforeEach(() => {
wrapper = createComponent({ props: { isLoading: true } });
});
it('renders loading icon', () => {
expect(findLoader(wrapper).exists()).toBe(true);
});
});
});
......@@ -111,14 +111,14 @@ describe('Cycle analytics getters', () => {
);
});
it('returns null if there is no plottable data for the selected stages', () => {
it('returns an empty array if there is no plottable data for the selected stages', () => {
const stateWithDurationData = {
startDate,
endDate,
durationData: [],
};
expect(getters.durationChartPlottableData(stateWithDurationData)).toBeNull();
expect(getters.durationChartPlottableData(stateWithDurationData)).toEqual([]);
});
});
......
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