Commit 78169e93 authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch '323982-remove-enable-value-stream-analytics-horizontal-navigation-ff' into 'master'

Remove `value_stream_analytics_path_navigation` feature flag [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!60449
parents cf2ba49f ec5f9c24
......@@ -189,9 +189,7 @@ GitLab allows users to create multiple value streams, hide default stages and cr
### Stage path
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](../../../administration/feature_flags.md). **(FREE SELF)**
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/323982) in GitLab 13.12.
![Value stream path navigation](img/vsa_path_nav_v13_11.png "Value stream path navigation")
......@@ -213,14 +211,6 @@ Hovering over a stage item displays a popover with the following information:
- Start event description for the given stage
- End event description
Horizontal path navigation is enabled by default. If you have a self-managed instance, an
administrator can [open a Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md)
and disable it with the following command:
```ruby
Feature.disable(:value_stream_analytics_path_navigation)
```
### Adding a stage
In the following example we're creating a new stage that measures and tracks issues from creation
......
......@@ -7,13 +7,10 @@ import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_fi
import { DATE_RANGE_LIMIT } from '../../shared/constants';
import { toYmd } from '../../shared/utils';
import { PROJECTS_PER_PAGE, OVERVIEW_STAGE_ID } from '../constants';
import CustomStageForm from './custom_stage_form.vue';
import DurationChart from './duration_chart.vue';
import FilterBar from './filter_bar.vue';
import Metrics from './metrics.vue';
import PathNavigation from './path_navigation.vue';
import StageTable from './stage_table.vue';
import StageTableNav from './stage_table_nav.vue';
import StageTableNew from './stage_table_new.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue';
import ValueStreamSelect from './value_stream_select.vue';
......@@ -25,10 +22,7 @@ export default {
DurationChart,
GlEmptyState,
ProjectsDropdownFilter,
StageTable,
TypeOfWorkCharts,
CustomStageForm,
StageTableNav,
StageTableNew,
PathNavigation,
FilterBar,
......@@ -89,31 +83,14 @@ export default {
return !this.currentGroup && !this.isLoading;
},
shouldDisplayFilters() {
return !this.errorCode;
return !this.errorCode && !this.hasNoAccessError;
},
shouldDisplayDurationChart() {
return (
!this.featureFlags.hasPathNavigation ||
(this.featureFlags.hasDurationChart &&
this.isOverviewStageSelected &&
!this.hasNoAccessError)
);
},
shouldDisplayTypeOfWorkCharts() {
return (
!this.featureFlags.hasPathNavigation ||
(this.isOverviewStageSelected && !this.hasNoAccessError)
);
return this.featureFlags.hasDurationChart;
},
selectedStageReady() {
return !this.hasNoAccessError && this.selectedStage;
},
shouldDisplayPathNavigation() {
return this.featureFlags.hasPathNavigation && this.selectedStageReady;
},
shouldDisplayVerticalNavigation() {
return !this.featureFlags.hasPathNavigation && this.selectedStageReady;
},
shouldDisplayCreateMultipleValueStreams() {
return Boolean(!this.shouldRenderEmptyState && !this.isLoadingValueStreams);
},
......@@ -122,12 +99,6 @@ export default {
},
query() {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
const stageParams = this.featureFlags.hasPathNavigation
? {
sort: (!this.isOverviewStageSelected && this.pagination?.sort) || null,
direction: (!this.isOverviewStageSelected && this.pagination?.direction) || null,
}
: {};
return {
value_stream_id: this.selectedValueStream?.id || null,
......@@ -135,7 +106,8 @@ export default {
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
stage_id: (!this.isOverviewStageSelected && this.selectedStage?.id) || null, // the `overview` stage is always the default, so dont persist the id if its selected
...stageParams,
sort: (!this.isOverviewStageSelected && this.pagination?.sort) || null,
direction: (!this.isOverviewStageSelected && this.pagination?.direction) || null,
};
},
stageCount() {
......@@ -224,7 +196,7 @@ export default {
/>
<div v-if="!shouldRenderEmptyState" class="gl-max-w-full">
<path-navigation
v-if="shouldDisplayPathNavigation"
v-if="selectedStageReady"
:key="`path_navigation_key_${pathNavigationData.length}`"
class="js-path-navigation gl-w-full gl-pb-2"
:loading="isLoading"
......@@ -276,65 +248,27 @@ export default {
"
/>
<template v-else>
<metrics
v-if="!featureFlags.hasPathNavigation || isOverviewStageSelected"
:group-path="currentGroupPath"
:request-params="cycleAnalyticsRequestParams"
/>
<template v-if="featureFlags.hasPathNavigation">
<stage-table-new
v-if="!isLoading && !isOverviewStageSelected"
:is-loading="isLoading || isLoadingStage"
:stage-events="currentStageEvents"
:selected-stage="selectedStage"
:empty-state-message="selectedStageError"
:no-data-svg-path="noDataSvgPath"
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
<template v-if="isOverviewStageSelected">
<metrics :group-path="currentGroupPath" :request-params="cycleAnalyticsRequestParams" />
<duration-chart
v-if="shouldDisplayDurationChart"
class="gl-mt-3"
:stages="activeStages"
/>
<type-of-work-charts />
</template>
<stage-table
<stage-table-new
v-else
:key="stageCount"
class="js-stage-table"
:current-stage="selectedStage"
:is-loading="isLoading"
:is-loading-stage="isLoadingStage"
:is-empty-stage="isEmptyStage"
:custom-stage-form-active="customStageFormActive"
:current-stage-events="currentStageEvents"
:no-data-svg-path="noDataSvgPath"
:is-loading="isLoading || isLoadingStage"
:stage-events="currentStageEvents"
:selected-stage="selectedStage"
:empty-state-message="selectedStageError"
:has-path-navigation="featureFlags.hasPathNavigation"
>
<template v-if="shouldDisplayVerticalNavigation" #nav>
<stage-table-nav
:current-stage="selectedStage"
:stages="activeStages"
:medians="medians"
:is-creating-custom-stage="isCreatingCustomStage"
:custom-ordering="enableCustomOrdering"
@reorderStage="onStageReorder"
@selectStage="onStageSelect"
@editStage="onShowEditStageForm"
@showAddStageForm="onShowAddStageForm"
@hideStage="onUpdateCustomStage"
@removeStage="onRemoveStage"
/>
</template>
<template v-if="customStageFormActive" #content>
<custom-stage-form
:events="formEvents"
@createStage="onCreateCustomStage"
@updateStage="onUpdateCustomStage"
@clearErrors="$emit('clear-form-errors')"
/>
</template>
</stage-table>
<url-sync :query="query" />
:no-data-svg-path="noDataSvgPath"
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/>
<url-sync v-if="selectedStageReady" :query="query" />
</template>
<duration-chart v-if="shouldDisplayDurationChart" class="gl-mt-3" :stages="activeStages" />
<type-of-work-charts v-if="shouldDisplayTypeOfWorkCharts" />
</div>
</div>
</template>
......@@ -19,10 +19,7 @@ export default () => {
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore();
const {
cycleAnalyticsScatterplotEnabled: hasDurationChart = false,
valueStreamAnalyticsPathNavigation: hasPathNavigation = false,
} = gon?.features;
const { cycleAnalyticsScatterplotEnabled: hasDurationChart = false } = gon?.features;
const {
author_username = null,
......@@ -39,7 +36,7 @@ export default () => {
selectedMilestone: milestone_title,
selectedAssigneeList: assignee_username,
selectedLabelList: label_name,
featureFlags: { hasDurationChart, hasPathNavigation },
featureFlags: { hasDurationChart },
pagination: { sort: sort?.value || null, direction: direction?.value || null },
});
......
......@@ -172,41 +172,12 @@ export const receiveGroupStagesError = ({ commit }, error) => {
});
};
export const setDefaultSelectedStage = ({ state: { featureFlags }, dispatch, getters }) => {
const { activeStages = [] } = getters;
export const setDefaultSelectedStage = ({ dispatch }) =>
dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
if (featureFlags?.hasPathNavigation) {
return dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
}
if (activeStages?.length) {
const [firstActiveStage] = activeStages;
return Promise.all([
dispatch('setSelectedStage', firstActiveStage),
dispatch('fetchStageData', firstActiveStage.slug),
]);
}
createFlash({
message: __('There was an error while fetching value stream analytics data.'),
});
return Promise.resolve();
};
export const receiveGroupStagesSuccess = (
{ state: { featureFlags }, commit, dispatch },
stages,
) => {
export const receiveGroupStagesSuccess = ({ commit }, stages) =>
commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages);
if (!featureFlags?.hasPathNavigation) {
return dispatch('setDefaultSelectedStage');
}
return Promise.resolve();
};
export const fetchGroupStagesAndEvents = ({ dispatch, getters }) => {
const {
currentValueStreamId: valueStreamId,
......
......@@ -51,17 +51,7 @@ export default {
state.medians = {};
},
[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians = []) {
if (state?.featureFlags?.hasPathNavigation) {
state.medians = formatMedianValuesWithOverview(medians);
} else {
state.medians = medians.reduce(
(acc, { id, value, error = null }) => ({
...acc,
[id]: { value, error },
}),
{},
);
}
state.medians = formatMedianValuesWithOverview(medians);
},
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {};
......
......@@ -14,7 +14,6 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati
before_action do
push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true)
push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group, default_enabled: :yaml)
render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
end
......
---
title: Remove the value_stream_analytics_path_navigation feature flag
merge_request: 60449
author:
type: changed
---
name: value_stream_analytics_path_navigation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31069
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323982
milestone: '13.0'
type: development
group: group::optimize
default_enabled: true
......@@ -42,11 +42,8 @@ RSpec.describe 'Value stream analytics charts', :js do
context 'Duration chart' do
duration_stage_selector = '.js-dropdown-stages'
stage_nav_selector = '.stage-nav'
stage_table_selector = '.js-stage-table'
let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
let(:first_default_stage) { page.find('.stage-nav-item-cell', text: 'Issue').ancestor('.stage-nav-item') }
let(:custom_value_stream_name) { "New created value stream" }
let_it_be(:translated_default_stage_names) do
......@@ -93,34 +90,6 @@ RSpec.describe 'Value stream analytics charts', :js do
expect(duration_chart_stages).not_to include(first_stage_name)
end
context 'With the path navigation feature flag disabled' do
let(:nav) { page.find(stage_nav_selector) }
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
it_behaves_like 'has all the default stages'
context 'hidden stage' do
before do
toggle_more_options(first_default_stage)
click_button(_('Hide stage'))
end
it 'will not appear in the duration chart dropdown' do
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
toggle_duration_chart_dropdown
expect(duration_chart_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
end
describe 'Tasks by type chart', :js do
......
......@@ -41,7 +41,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
start_field_label = 'custom-stage-start-event-label-0'
end_field_label = 'custom-stage-end-event-label-0'
name_field = 'custom-stage-name-0'
stage_table_selector = '.js-stage-table'
let(:add_stage_button) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
......@@ -84,77 +83,6 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
sign_in(user)
end
context 'Manual ordering' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
let(:default_stage_order) { %w[Issue Plan Code Test Review Staging].freeze }
def confirm_stage_order(stages)
page.within('.stage-nav>ul') do
stages.each_with_index do |stage, index|
expect(find("li:nth-child(#{index + 1})")).to have_content(stage)
end
end
end
context 'with only default stages' do
it 'does not allow stages to be draggable', :js do
confirm_stage_order(default_stage_order)
drag_from_index_to_index(0, 1)
confirm_stage_order(default_stage_order)
end
end
context 'with at least one custom stage', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/216745' do
default_custom_stage_order = %w[Issue Plan Code Test Review Staging Cool\ beans].freeze
stages_near_middle_swapped = %w[Issue Plan Test Code Review Staging Cool\ beans].freeze
stage_dragged_to_top = %w[Review Issue Plan Code Test Staging Cool\ beans].freeze
stage_dragged_to_bottom = %w[Issue Plan Code Test Staging Cool\ beans Review].freeze
shared_examples 'draggable stage' do |original_order, updated_order, start_index, end_index,|
before do
page.driver.browser.manage.window.resize_to(1650, 1150)
create_custom_stage
select_group(group)
end
it 'allows a stage to be dragged' do
confirm_stage_order(original_order)
drag_from_index_to_index(start_index, end_index)
confirm_stage_order(updated_order)
end
it 'persists the order when a group is selected' do
drag_from_index_to_index(start_index, end_index)
select_group(group)
confirm_stage_order(updated_order)
end
end
context 'dragging a stage to the top', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_top, 4, 0
end
context 'dragging a stage to the bottom', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_bottom, 4, 7
end
context 'dragging stages in the middle', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stages_near_middle_swapped, 2, 3
end
end
end
shared_examples 'submits custom stage form successfully' do |stage_name|
it 'custom stage is saved with confirmation message' do
fill_in name_field, with: stage_name
......@@ -291,217 +219,4 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
end
end
end
context 'With the path navigation feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
end
context 'with a group' do
context 'selected' do
before do
select_group(group, stage_table_selector)
end
it_behaves_like 'can create custom stages' do
let(:first_label) { group_label1 }
let(:second_label) { group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/273045' do
before do
create_custom_stage
select_group(group, stage_table_selector)
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'with a sub group' do
context 'selected' do
before do
select_group(sub_group, stage_table_selector)
end
it_behaves_like 'can create custom stages' do
let(:first_label) { sub_group_label1 }
let(:second_label) { sub_group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created' do
before do
create_custom_stage(sub_group)
select_group(sub_group, stage_table_selector)
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'Add a stage button' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
end
it 'displays the custom stage form when clicked' do
expect(page).not_to have_text(s_('CustomCycleAnalytics|New stage'))
expect(page).to have_selector(add_stage_button, visible: true)
expect(page).to have_text(s_('CustomCycleAnalytics|Add a stage'))
expect(page).not_to have_selector("#{add_stage_button}.active")
page.find(add_stage_button).click
expect(page).to have_selector("#{add_stage_button}.active")
expect(page).to have_text(s_('CustomCycleAnalytics|New stage'))
end
end
context 'default stages' do
def open_recover_stage_dropdown
find(add_stage_button).click
click_button(_('Recover hidden stage'))
end
def active_stages
page.all('.stage-nav .stage-name').collect(&:text)
end
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
toggle_more_options(first_default_stage)
end
it "can be hidden, can't be edited or removed" do
expect(find_stage_actions_btn(first_default_stage)).to have_text(_('Hide stage'))
expect(find_stage_actions_btn(first_default_stage)).not_to have_text(_('Edit stage'))
expect(find_stage_actions_btn(first_default_stage)).not_to have_text(_('Remove stage'))
end
context 'Hide stage' do
before do
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'disappears from the stage table & can be recovered' do
expect(active_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
open_recover_stage_dropdown
expect(page.find("[data-testid='recover-hidden-stage-dropdown']")).to have_text(s_('CycleAnalyticsStage|Issue'))
end
end
context 'Recover stage' do
before do
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'recovers the stage back to the stage table' do
open_recover_stage_dropdown
click_button(s_('CycleAnalyticsStage|Issue'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
expect(page.find('.flash-notice')).to have_content(_('Stage data updated'))
expect(active_stages).to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
context 'custom stages' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
create_custom_stage
select_group(group, stage_table_selector)
expect(page).to have_text custom_stage_name
toggle_more_options(first_custom_stage)
end
it 'can not be hidden, can be edited or removed' do
expect(find_stage_actions_btn(first_custom_stage)).not_to have_text(_('Hide stage'))
expect(find_stage_actions_btn(first_custom_stage)).to have_text(_('Edit stage'))
expect(find_stage_actions_btn(first_custom_stage)).to have_text(_('Remove stage'))
end
it 'disappears from the stage table after being removed' do
nav = page.find(stage_nav_selector)
expect(nav).to have_text(custom_stage_name)
click_button(_('Remove stage'))
expect(page.find('.flash-notice')).to have_text(_('Stage removed'))
expect(nav).not_to have_text(custom_stage_name)
end
end
end
context 'Duration chart' do
let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
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
before do
select_group(group)
end
it 'has all the default stages' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).to eq(translated_default_stage_names)
end
context 'hidden stage' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, stage_table_selector)
toggle_more_options(first_default_stage)
click_button(_('Hide stage'))
# wait for the stage list to load
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'will not appear in the duration chart dropdown' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
end
......@@ -17,21 +17,6 @@ RSpec.describe 'Group value stream analytics' do
it 'pushes frontend feature flags' do
visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags(
cycleAnalyticsScatterplotEnabled: true,
valueStreamAnalyticsPathNavigation: true
)
end
context 'when `value_stream_analytics_path_navigation` is disabled for a group' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false, thing: group)
end
it 'pushes disabled feature flag to the frontend' do
visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
end
expect(page).to have_pushed_frontend_feature_flags(cycleAnalyticsScatterplotEnabled: true)
end
end
......@@ -172,44 +172,6 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end
end
context 'with path navigation feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
select_group(group, '.js-stage-table')
end
it 'does not show the path navigation' do
expect(page).not_to have_selector(path_nav_selector)
end
it 'shows the vertical stage navigation' do
expect(page).to have_selector(stage_nav_selector, visible: true)
end
it 'displays the default list of stages' do
stage_nav = page.find(stage_nav_selector)
%w[Issue Plan Code Test Review Staging].each do |item|
string_id = "CycleAnalytics|#{item}"
expect(stage_nav).to have_content(s_(string_id))
end
end
it 'each stage will have median values', :sidekiq_might_not_need_inline do
stage_medians = page.all('.stage-nav .stage-median').collect(&:text)
expect(stage_medians).to eq(["Not enough data"] * 6)
end
it 'displays the stage table headers' do
expect(page).to have_selector('.stage-header', visible: true)
expect(page).to have_selector('.median-header', visible: true)
expect(page).to have_selector('.event-header', visible: true)
expect(page).to have_selector('.total-time-header', visible: true)
end
end
context 'without valid query parameters set' do
context 'with created_after date > created_before date' do
before do
......
......@@ -3,16 +3,11 @@ import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue';
import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import CustomStageForm from 'ee/analytics/cycle_analytics/components/custom_stage_form.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue';
import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue';
import StageNavItem from 'ee/analytics/cycle_analytics/components/stage_nav_item.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import StageTableNav from 'ee/analytics/cycle_analytics/components/stage_table_nav.vue';
import StageTableNew from 'ee/analytics/cycle_analytics/components/stage_table_new.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
......@@ -55,7 +50,6 @@ const defaultStubs = {
const defaultFeatureFlags = {
hasDurationChart: true,
hasPathNavigation: false,
};
const [selectedValueStream] = mockData.valueStreams;
......@@ -122,6 +116,7 @@ describe('Value Stream Analytics component', () => {
featureFlags = {},
initialState = initialCycleAnalyticsState,
props = {},
selectedStage = null,
} = options;
store = createStore();
......@@ -152,15 +147,16 @@ describe('Value Stream Analytics component', () => {
'receiveGroupStagesSuccess',
mockData.customizableStagesAndEvents.stages,
);
if (selectedStage) {
await store.dispatch('setSelectedStage', selectedStage);
await store.dispatch('fetchStageData', selectedStage.slug);
} else {
await store.dispatch('setDefaultSelectedStage');
}
}
return comp;
}
const findStageNavItemAtIndex = (index) =>
wrapper.find(StageTableNav).findAll(StageNavItem).at(index);
const findAddStageButton = () => wrapper.find(AddStageButton);
const displaysProjectsDropdownFilter = (flag) => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(flag);
};
......@@ -173,8 +169,8 @@ describe('Value Stream Analytics component', () => {
expect(wrapper.find(Metrics).exists()).toBe(flag);
};
const displaysStageTable = (flag, component = StageTable) => {
expect(wrapper.find(component).exists()).toBe(flag);
const displaysStageTable = (flag) => {
expect(wrapper.find(StageTableNew).exists()).toBe(flag);
};
const displaysDurationChart = (flag) => {
......@@ -189,10 +185,6 @@ describe('Value Stream Analytics component', () => {
expect(wrapper.find(PathNavigation).exists()).toBe(flag);
};
const displaysAddStageButton = (flag) => {
expect(wrapper.find(AddStageButton).exists()).toBe(flag);
};
const displaysFilterBar = (flag) => {
expect(wrapper.find(FilterBar).exists()).toBe(flag);
};
......@@ -205,12 +197,7 @@ describe('Value Stream Analytics component', () => {
beforeEach(async () => {
const { group, ...stateWithoutGroup } = initialCycleAnalyticsState;
mock = new MockAdapter(axios);
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
initialState: stateWithoutGroup,
});
wrapper = await createComponent({ initialState: stateWithoutGroup });
});
afterEach(() => {
......@@ -240,17 +227,12 @@ describe('Value Stream Analytics component', () => {
it('does not display the stage table', () => {
displaysStageTable(false);
displaysStageTable(false, StageTableNew);
});
it('does not display the duration chart', () => {
displaysDurationChart(false);
});
it('does not display the add stage button', () => {
displaysAddStageButton(false);
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
......@@ -265,11 +247,7 @@ describe('Value Stream Analytics component', () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
wrapper = await createComponent();
await store.dispatch('receiveCycleAnalyticsDataError', {
response: { status: httpStatusCodes.FORBIDDEN },
......@@ -297,11 +275,6 @@ describe('Value Stream Analytics component', () => {
it('does not display the stage table', () => {
displaysStageTable(false);
displaysStageTable(false, StageTableNew);
});
it('does not display the add stage button', () => {
displaysAddStageButton(false);
});
it('does not display the tasks by type chart', () => {
......@@ -312,35 +285,8 @@ describe('Value Stream Analytics component', () => {
displaysDurationChart(false);
});
describe('path navigation', () => {
describe('disabled', () => {
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
});
describe('enabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
pathNavigationEnabled: true,
});
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
mock.onAny().reply(httpStatusCodes.FORBIDDEN);
await waitForPromises();
});
afterEach(() => {
mock.restore();
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
});
......@@ -348,12 +294,7 @@ describe('Value Stream Analytics component', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
},
});
wrapper = await createComponent({ withStageSelected: true });
});
afterEach(() => {
......@@ -403,11 +344,6 @@ describe('Value Stream Analytics component', () => {
it('hides the stage table', () => {
displaysStageTable(false);
displaysStageTable(false, StageTableNew);
});
it('hides the add stage button', () => {
displaysAddStageButton(false);
});
describe('Without the overview stage selected', () => {
......@@ -416,112 +352,16 @@ describe('Value Stream Analytics component', () => {
mockRequiredRoutes(mock);
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
},
selectedStage: mockData.issueStage,
});
await store.dispatch('setSelectedStage', mockData.issueStage);
await wrapper.vm.$nextTick();
});
it('displays the stage table', () => {
displaysStageTable(true, StageTableNew);
displaysStageTable(true);
});
it('does not display the add stage button', () => {
displaysAddStageButton(false);
});
});
describe('path navigation', () => {
describe('disabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: false,
},
});
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
describe('StageTable', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent({
opts: {
stubs: {
StageTable,
StageTableNav,
StageNavItem,
},
},
withStageSelected: true,
});
});
it('has the first stage selected by default', () => {
const first = findStageNavItemAtIndex(0);
const second = findStageNavItemAtIndex(1);
expect(first.props('isActive')).toBe(true);
expect(second.props('isActive')).toBe(false);
});
it('can navigate to different stages', async () => {
findStageNavItemAtIndex(2).trigger('click');
await wrapper.vm.$nextTick();
const first = findStageNavItemAtIndex(0);
const third = findStageNavItemAtIndex(2);
expect(third.props('isActive')).toBe(true);
expect(first.props('isActive')).toBe(false);
});
describe('Add stage button', () => {
beforeEach(async () => {
wrapper = await createComponent({
opts: {
stubs: {
StageTable,
StageTableNav,
AddStageButton,
},
},
withStageSelected: true,
});
});
it('can navigate to the custom stage form', async () => {
expect(wrapper.find(CustomStageForm).exists()).toBe(false);
findAddStageButton().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find(CustomStageForm).exists()).toBe(true);
});
});
});
});
describe('enabled', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
},
});
});
it('displays the path navigation', () => {
displaysPathNavigation(true);
});
it('displays the path navigation', () => {
displaysPathNavigation(true);
});
});
});
......@@ -532,11 +372,7 @@ describe('Value Stream Analytics component', () => {
mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
wrapper = await createComponent();
});
afterEach(() => {
......@@ -568,7 +404,8 @@ describe('Value Stream Analytics component', () => {
mock
.onGet(mockData.endpoints.stageData)
.reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
await createComponent({ withStageSelected: true, selectedStage: mockData.issueStage });
await findError('There was an error fetching data for the selected stage');
});
......@@ -615,8 +452,10 @@ describe('Value Stream Analytics component', () => {
value_stream_id: selectedValueStream.id,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
stage_id: 1,
stage_id: null,
project_ids: null,
sort: null,
direction: null,
};
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => getIdFromGraphQLId(id));
......@@ -681,7 +520,7 @@ describe('Value Stream Analytics component', () => {
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: selectedProjectIds,
stage_id: 1,
stage_id: null,
});
});
});
......@@ -693,54 +532,12 @@ describe('Value Stream Analytics component', () => {
await wrapper.vm.$nextTick();
});
it('sets the stage_id url parameter', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
stage_id: 2,
});
});
});
describe('with hasPathNavigation=true', () => {
it('does not set the sort and direction parameters', async () => {
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
await wrapper.vm.$nextTick();
it('sets the stage, sort and direction parameters', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
project_ids: null,
});
});
describe('with a stage selected', () => {
beforeEach(async () => {
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
await store.dispatch('setSelectedStage', selectedStage);
await wrapper.vm.$nextTick();
});
it('sets the stage, sort and direction parameters', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
stage_id: selectedStage.id,
direction: PAGINATION_SORT_DIRECTION_DESC,
sort: PAGINATION_SORT_FIELD_END_EVENT,
});
stage_id: selectedStage.id,
direction: PAGINATION_SORT_DIRECTION_DESC,
sort: PAGINATION_SORT_FIELD_END_EVENT,
});
});
});
......
......@@ -394,140 +394,31 @@ describe('Value Stream Analytics actions', () => {
});
describe('receiveGroupStagesSuccess', () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: true,
},
};
});
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[],
);
});
});
describe('when the `hasPathNavigation` feature flag is disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: false,
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
};
});
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation and dispatches 'setDefaultSelectedStage`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[{ type: 'setDefaultSelectedStage' }],
);
});
],
[],
);
});
});
describe('setDefaultSelectedStage', () => {
describe('when the `hasPathNavigation` feature flag is enabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: true,
},
};
});
afterEach(() => {
mock.restore();
});
it("dispatches the 'setSelectedStage' with the overview stage", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[{ type: 'setSelectedStage', payload: OVERVIEW_STAGE_CONFIG }],
);
});
});
describe('when the `hasPathNavigation` feature flag is disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasPathNavigation: false,
},
};
});
afterEach(() => {
mock.restore();
});
it("dispatches the 'fetchStageData' action", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[
{ type: 'setSelectedStage', payload: selectedStage },
{ type: 'fetchStageData', payload: selectedStageSlug },
],
);
});
it.each`
data
${[]}
${null}
`('with $data will flash an error', ({ data }) => {
actions.setDefaultSelectedStage(
{ state, getters: { activeStages: data }, dispatch: () => {} },
{},
);
expect(createFlash).toHaveBeenCalledWith({ message: flashErrorMessage });
});
it('will select the first active stage', () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[
{ type: 'setSelectedStage', payload: stages[1] },
{ type: 'fetchStageData', payload: stages[1].slug },
],
);
});
it("dispatches the 'setSelectedStage' with the overview stage", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[{ type: 'setSelectedStage', payload: OVERVIEW_STAGE_CONFIG }],
);
});
});
......
......@@ -177,42 +177,23 @@ describe('Value Stream Analytics mutations', () => {
});
describe(`${types.RECEIVE_STAGE_MEDIANS_SUCCESS}`, () => {
it('sets each id as a key in the median object with the corresponding value and error', () => {
const stateWithData = {
beforeEach(() => {
state = {
medians: {},
};
mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](stateWithData, [
{ id: 1, value: 20 },
{ id: 2, value: 10 },
mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, [
{ id: 1, value: 7580 },
{ id: 2, value: 434340 },
]);
expect(stateWithData.medians).toEqual({
1: { value: 20, error: null },
2: { value: 10, error: null },
});
});
describe('with hasPathNavigation set to true', () => {
beforeEach(() => {
state = {
featureFlags: { hasPathNavigation: true },
medians: {},
};
mutations[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, [
{ id: 1, value: 7580 },
{ id: 2, value: 434340 },
]);
});
it('formats each stage median for display in the path navigation', () => {
expect(state.medians).toMatchObject({ 1: '2h', 2: '5d' });
});
it('formats each stage median for display in the path navigation', () => {
expect(state.medians).toMatchObject({ 1: '2h', 2: '5d' });
});
it('calculates the overview median', () => {
expect(state.medians).toMatchObject({ overview: '5d' });
});
it('calculates the overview median', () => {
expect(state.medians).toMatchObject({ overview: '5d' });
});
});
......
......@@ -9748,9 +9748,6 @@ msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|New stage"
msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
......@@ -26710,9 +26707,6 @@ msgstr ""
msgid "Reconfigure"
msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovering projects"
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