Commit db4d88f9 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '322126-vsa-remove-days-to-completion-scatterplot-dropdown' into 'master'

[VSA] Remove total time chart stage dropdown

See merge request gitlab-org/gitlab!80365
parents b39a7218 a5a58e7e
......@@ -280,7 +280,7 @@ Shown metrics and charts includes:
- [Lead time](#how-metrics-are-measured)
- [Cycle time](#how-metrics-are-measured)
- [Days to completion chart](#days-to-completion-chart)
- [Total time chart](#total-time-chart)
- [Tasks by type chart](#type-of-work---tasks-by-type-chart)
### Stage table
......@@ -413,7 +413,7 @@ To delete a custom value stream:
![Delete value stream](img/delete_value_stream_v13_12.png "Deleting a custom value stream")
## Days to completion chart
## Total time chart
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21631) in GitLab 12.6.
> - Chart median line [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235455) in GitLab 13.4.
......@@ -422,8 +422,9 @@ To delete a custom value stream:
This chart visually depicts the average number of days it takes for cycles to be completed.
This chart uses the global page filters for displaying data based on the selected
group, projects, and time frame. In addition, specific stages can be selected
from the chart itself.
group, projects, and time frame.
When a stage is selected the chart only displays data relevant to the selected stage. On the overview the chart displays a sum of the times for all stages in the value stream.
The chart data is limited to the last 500 items.
......
<script>
import { GlAlert, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { dataVizBlue500 } from '@gitlab/ui/scss_to_js/scss_variables';
import { mapActions, mapState, mapGetters } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import { dateFormats } from '~/analytics/shared/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
......@@ -15,7 +15,6 @@ import {
DURATION_TOTAL_TIME_NO_DATA,
DURATION_TOTAL_TIME_LABEL,
} from '../constants';
import StageDropdownFilter from './stage_dropdown_filter.vue';
export default {
name: 'DurationChart',
......@@ -23,7 +22,6 @@ export default {
GlAlert,
GlIcon,
Scatterplot,
StageDropdownFilter,
ChartSkeletonLoader,
},
directives: {
......@@ -64,12 +62,6 @@ export default {
: DURATION_STAGE_TIME_DESCRIPTION;
},
},
methods: {
...mapActions('durationChart', ['updateSelectedDurationChartStages']),
onDurationStageSelect(stages) {
this.updateSelectedDurationChartStages(stages);
},
},
durationChartTooltipDateFormat: dateFormats.defaultDate,
medianAdditionalOptions: {
lineStyle: {
......@@ -85,12 +77,6 @@ export default {
<h4 class="gl-mt-0">
{{ title }}&nbsp;<gl-icon v-gl-tooltip.hover name="information-o" :title="tooltipText" />
</h4>
<stage-dropdown-filter
v-if="isOverviewStageSelected && stages.length"
class="gl-ml-auto"
:stages="stages"
@selected="onDurationStageSelect"
/>
<scatterplot
v-if="hasData"
:x-axis-title="s__('CycleAnalytics|Date')"
......
<script>
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
export default {
name: 'StageDropdownFilter',
components: {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
},
props: {
stages: {
type: Array,
required: true,
},
label: {
type: String,
required: false,
default: s__('CycleAnalytics|stage dropdown'),
},
},
data() {
return {
selectedStages: this.stages,
};
},
computed: {
selectedStagesLabel() {
const { stages, selectedStages } = this;
if (selectedStages.length === 1) {
return selectedStages[0].title;
} else if (selectedStages.length === stages.length) {
return s__('CycleAnalytics|All stages');
} else if (selectedStages.length > 1) {
return sprintf(s__('CycleAnalytics|%{stageCount} stages selected'), {
stageCount: selectedStages.length,
});
}
return s__('CycleAnalytics|No stages selected');
},
},
methods: {
isStageSelected(stageId) {
return this.selectedStages.some(({ id }) => id === stageId);
},
onClick({ stage, isMarking }) {
this.selectedStages = isMarking
? this.selectedStages.filter((s) => s.id !== stage.id)
: this.selectedStages.concat([stage]);
this.$emit('selected', this.selectedStages);
},
},
};
</script>
<template>
<gl-dropdown
ref="stagesDropdown"
class="js-dropdown-stages"
toggle-class="gl-shadow-none"
:text="selectedStagesLabel"
right
>
<gl-dropdown-section-header>{{ s__('CycleAnalytics|Stages') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="stage in stages"
:key="stage.id"
:active="isStageSelected(stage.id)"
:is-check-item="true"
:is-checked="isStageSelected(stage.id)"
@click="onClick({ stage, isMarking: isStageSelected(stage.id) })"
>
{{ stage.title }}
</gl-dropdown-item>
</gl-dropdown>
</template>
......@@ -40,25 +40,3 @@ export const fetchDurationData = ({ dispatch, commit, rootGetters }) => {
.then((data) => commit(types.RECEIVE_DURATION_DATA_SUCCESS, data))
.catch((error) => dispatch('receiveDurationDataError', error));
};
export const updateSelectedDurationChartStages = ({ state, commit }, stages) => {
const setSelectedPropertyOnStages = (data) =>
data.map((stage) => {
const selected = stages.reduce((result, object) => {
if (object.id === stage.id) return true;
return result;
}, false);
return {
...stage,
selected,
};
});
const { durationData } = state;
const updatedDurationStageData = setSelectedPropertyOnStages(durationData);
commit(types.UPDATE_SELECTED_DURATION_CHART_STAGES, {
updatedDurationStageData,
});
};
......@@ -5,7 +5,7 @@ export const durationChartPlottableData = (state, _, rootState, rootGetters) =>
const { durationData } = state;
const { isOverviewStageSelected } = rootGetters;
const selectedStagesDurationData = isOverviewStageSelected
? durationData.filter((stage) => stage.selected)
? durationData
: durationData.filter((stage) => stage.id === selectedStage.id);
const plottableData = getDurationChartData(
selectedStagesDurationData,
......
export const SET_LOADING = 'SET_LOADING';
export const UPDATE_SELECTED_DURATION_CHART_STAGES = 'UPDATE_SELECTED_DURATION_CHART_STAGES';
export const REQUEST_DURATION_DATA = 'REQUEST_DURATION_DATA';
export const RECEIVE_DURATION_DATA_SUCCESS = 'RECEIVE_DURATION_DATA_SUCCESS';
export const RECEIVE_DURATION_DATA_ERROR = 'RECEIVE_DURATION_DATA_ERROR';
......@@ -4,9 +4,6 @@ export default {
[types.SET_LOADING](state, loading) {
state.isLoading = loading;
},
[types.UPDATE_SELECTED_DURATION_CHART_STAGES](state, { updatedDurationStageData }) {
state.durationData = updatedDurationStageData;
},
[types.REQUEST_DURATION_DATA](state) {
state.isLoading = true;
state.errorCode = null;
......
......@@ -32,63 +32,29 @@ RSpec.describe 'Value stream analytics charts', :js do
sign_in(user)
end
shared_examples 'has all the default stages' do
it 'has all the default stages in the duration dropdown' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).to eq(translated_default_stage_names + [latest_custom_stage_name])
end
end
context 'Duration chart' do
duration_stage_selector = '.js-dropdown-stages'
let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
let(:custom_value_stream_name) { "New created value stream" }
let_it_be(:translated_default_stage_names) do
Gitlab::Analytics::CycleAnalytics::DefaultStages.names.map do |name|
stage = Analytics::CycleAnalytics::GroupStage.new(name: name)
Analytics::CycleAnalytics::StagePresenter.new(stage).title
end.freeze
end
def duration_chart_stages
duration_chart_dropdown.all('.dropdown-item').collect(&:text)
end
def toggle_duration_chart_dropdown
duration_chart_dropdown.click
end
def hide_vsa_stage(index = 0)
page.find_button(_('Edit')).click
page.find("[data-testid='stage-action-hide-#{index}']").click
click_save_value_stream_button
wait_for_requests
end
def latest_custom_stage_name
index = duration_chart_stages.length
"Cool custom stage - name #{index}"
end
before do
select_group(group)
create_custom_value_stream(custom_value_stream_name)
end
it_behaves_like 'has all the default stages'
it 'displays data for all stages on the overview' do
page.within('[data-testid="vsa-path-navigation"]') do
click_button "Overview"
end
it 'hidden stages will not appear in the duration chart dropdown' do
first_stage_name = duration_chart_stages.first
expect(page).to have_text("Total time")
end
hide_vsa_stage
toggle_duration_chart_dropdown
it 'displays data for a specific stage when selected' do
page.within('[data-testid="vsa-path-navigation"]') do
click_button "Issue"
end
expect(duration_chart_stages).not_to include(first_stage_name)
expect(page).to have_text("Stage time: Issue")
end
end
......
......@@ -17,12 +17,6 @@ exports[`DurationChart with the overiew stage selected renders the duration char
/>
</h4>
<stagedropdownfilter-stub
class="gl-ml-auto"
label="stage dropdown"
stages="[object Object],[object Object],[object Object]"
/>
<scatterplot-stub
medianlinedata="2019-01-01,17,2019-01-01,2019-01-02,40,2019-01-02"
medianlineoptions="[object Object]"
......
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import {
......@@ -9,7 +9,6 @@ import {
DURATION_TOTAL_TIME_NO_DATA,
} from 'ee/analytics/cycle_analytics/constants';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { allowedStages as stages, durationChartPlottableData as durationData } from '../mock_data';
......@@ -18,7 +17,6 @@ Vue.use(Vuex);
const actionSpies = {
fetchDurationData: jest.fn(),
updateSelectedDurationChartStages: jest.fn(),
};
const fakeStore = ({ initialGetters, initialState, rootGetters, rootState }) =>
......@@ -47,7 +45,6 @@ const fakeStore = ({ initialGetters, initialState, rootGetters, rootState }) =>
});
function createComponent({
mountFn = shallowMount,
stubs = {},
initialState = {},
initialGetters = {},
......@@ -55,7 +52,7 @@ function createComponent({
rootState = {},
props = {},
} = {}) {
return mountFn(DurationChart, {
return shallowMount(DurationChart, {
store: fakeStore({ initialState, initialGetters, rootGetters, rootState }),
propsData: {
stages,
......@@ -64,7 +61,6 @@ function createComponent({
stubs: {
ChartSkeletonLoader: true,
Scatterplot: true,
StageDropdownFilter: true,
...stubs,
},
});
......@@ -76,13 +72,8 @@ describe('DurationChart', () => {
const findContainer = (_wrapper) => _wrapper.find('[data-testid="vsa-duration-chart"]');
const findChartDescription = (_wrapper) => _wrapper.findComponent(GlIcon);
const findScatterPlot = (_wrapper) => _wrapper.findComponent(Scatterplot);
const findStageDropdown = (_wrapper) => _wrapper.findComponent(StageDropdownFilter);
const findLoader = (_wrapper) => _wrapper.findComponent(ChartSkeletonLoader);
const selectStage = (_wrapper, index = 0) => {
findStageDropdown(_wrapper).findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -101,10 +92,6 @@ describe('DurationChart', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true);
});
it('renders the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(true);
});
it('renders the chart description', () => {
expect(findChartDescription(wrapper).attributes('title')).toBe(
DURATION_TOTAL_TIME_DESCRIPTION,
......@@ -126,22 +113,6 @@ describe('DurationChart', () => {
});
});
describe('when a stage is selected', () => {
const selectedIndex = 1;
const selectedStages = stages.filter((_, index) => index !== selectedIndex);
beforeEach(() => {
wrapper = createComponent({ stubs: { StageDropdownFilter } });
selectStage(wrapper, selectedIndex);
});
it('calls the `updateSelectedDurationChartStages` action', () => {
expect(actionSpies.updateSelectedDurationChartStages).toHaveBeenCalledWith(
expect.any(Object),
selectedStages,
);
});
});
describe('with a value stream stage selected', () => {
const [selectedStage] = stages;
......@@ -160,10 +131,6 @@ describe('DurationChart', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true);
});
it('does not render the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(false);
});
it('renders the stage title', () => {
expect(wrapper.text()).toContain(`Stage time: ${selectedStage.title}`);
});
......@@ -203,20 +170,6 @@ describe('DurationChart', () => {
});
});
describe('with no stages', () => {
beforeEach(() => {
wrapper = createComponent({
mountFn: mount,
props: { stages: [] },
stubs: { StageDropdownFilter: false },
});
});
it('does not render the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(false);
});
});
describe('when isLoading=true', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { isLoading: true } });
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
const stages = [
{
id: 1,
title: 'Issue',
},
{
id: 2,
title: 'Plan',
},
{
id: 3,
title: 'Code',
},
];
describe('StageDropdownFilter component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(StageDropdownFilter, {
propsData: {
stages,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
beforeEach(() => {
createComponent();
});
const findDropdown = () => wrapper.findComponent(GlDropdown);
const selectDropdownItemAtIndex = (index) =>
findDropdown().findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
describe('on stage click', () => {
describe('clicking a selected stage', () => {
it('should remove from selection', () => {
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([[[stages[1], stages[2]]]]);
});
});
describe('clicking a deselected stage', () => {
beforeEach(() => {
selectDropdownItemAtIndex(0);
});
it('should add to selection', () => {
selectDropdownItemAtIndex(0);
expect(wrapper.emitted().selected).toEqual([
[[stages[1], stages[2]]],
[[stages[1], stages[2], stages[0]]],
]);
});
});
});
});
......@@ -192,79 +192,4 @@ describe('DurationChart actions', () => {
});
});
});
describe('updateSelectedDurationChartStages', () => {
it("commits the 'UPDATE_SELECTED_DURATION_CHART_STAGES' mutation with all the selected stages in the duration data", () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
};
testAction(
actions.updateSelectedDurationChartStages,
activeStages,
stateWithDurationData,
[
{
type: types.UPDATE_SELECTED_DURATION_CHART_STAGES,
payload: {
updatedDurationStageData: transformedDurationData,
},
},
],
[],
);
});
it("commits the 'UPDATE_SELECTED_DURATION_CHART_STAGES' mutation with all the selected and deselected stages in the duration data", () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
};
testAction(
actions.updateSelectedDurationChartStages,
[activeStages[0], activeStages[1]],
stateWithDurationData,
[
{
type: types.UPDATE_SELECTED_DURATION_CHART_STAGES,
payload: {
updatedDurationStageData: [
transformedDurationData[0],
transformedDurationData[1],
{ ...transformedDurationData[2], selected: false },
],
},
},
],
[],
);
});
it("commits the 'UPDATE_SELECTED_DURATION_CHART_STAGES' mutation with all deselected stages in the duration data", () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
};
testAction(
actions.updateSelectedDurationChartStages,
[],
stateWithDurationData,
[
{
type: types.UPDATE_SELECTED_DURATION_CHART_STAGES,
payload: {
updatedDurationStageData: transformedDurationData.map((d) => ({
...d,
selected: false,
})),
},
},
],
[],
);
});
});
});
......@@ -25,9 +25,8 @@ describe('DurationChart mutations', () => {
});
it.each`
mutation | payload | expectedState
${types.UPDATE_SELECTED_DURATION_CHART_STAGES} | ${{ updatedDurationStageData: transformedDurationData }} | ${{ durationData: transformedDurationData }}
${types.SET_LOADING} | ${true} | ${{ isLoading: true }}
mutation | payload | expectedState
${types.SET_LOADING} | ${true} | ${{ isLoading: true }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......
......@@ -10868,12 +10868,6 @@ msgstr ""
msgid "CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)"
msgstr ""
msgid "CycleAnalytics|%{stageCount} stages selected"
msgstr ""
msgid "CycleAnalytics|All stages"
msgstr ""
msgid "CycleAnalytics|Average time to completion"
msgstr ""
......@@ -10886,9 +10880,6 @@ msgstr ""
msgid "CycleAnalytics|Lead Time for Changes"
msgstr ""
msgid "CycleAnalytics|No stages selected"
msgstr ""
msgid "CycleAnalytics|Number of tasks"
msgstr ""
......@@ -10920,9 +10911,6 @@ msgstr ""
msgid "CycleAnalytics|Stage time: %{title}"
msgstr ""
msgid "CycleAnalytics|Stages"
msgstr ""
msgid "CycleAnalytics|Tasks by type"
msgstr ""
......@@ -10956,9 +10944,6 @@ msgstr ""
msgid "CycleAnalytics|project dropdown filter"
msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
msgid "DAG visualization requires at least 3 dependent jobs."
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