Commit 135d32dc authored by charlie ablett's avatar charlie ablett

Merge branch '217943-vsa-filter-bar-feature-flag' into 'master'

VSA filter bar feature flag

See merge request gitlab-org/gitlab!34169
parents e46ff054 884c8e7f
...@@ -19,6 +19,7 @@ import StageTableNav from './stage_table_nav.vue'; ...@@ -19,6 +19,7 @@ import StageTableNav from './stage_table_nav.vue';
import CustomStageForm from './custom_stage_form.vue'; import CustomStageForm from './custom_stage_form.vue';
import PathNavigation from './path_navigation.vue'; import PathNavigation from './path_navigation.vue';
import MetricCard from '../../shared/components/metric_card.vue'; import MetricCard from '../../shared/components/metric_card.vue';
import FilterBar from './filter_bar.vue';
export default { export default {
name: 'CycleAnalytics', name: 'CycleAnalytics',
...@@ -37,6 +38,7 @@ export default { ...@@ -37,6 +38,7 @@ export default {
StageTableNav, StageTableNav,
PathNavigation, PathNavigation,
MetricCard, MetricCard,
FilterBar,
}, },
mixins: [glFeatureFlagsMixin(), UrlSyncMixin], mixins: [glFeatureFlagsMixin(), UrlSyncMixin],
props: { props: {
...@@ -56,6 +58,14 @@ export default { ...@@ -56,6 +58,14 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
milestonesPath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -119,12 +129,25 @@ export default { ...@@ -119,12 +129,25 @@ export default {
stageCount() { stageCount() {
return this.activeStages.length; return this.activeStages.length;
}, },
hasProject() {
return this.selectedProjectIds.length;
},
}, },
mounted() { mounted() {
const {
glFeatures: {
cycleAnalyticsScatterplotEnabled: hasDurationChart,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian,
valueStreamAnalyticsPathNavigation: hasPathNavigation,
valueStreamAnalyticsFilterBar: hasFilterBar,
},
} = this;
this.setFeatureFlags({ this.setFeatureFlags({
hasDurationChart: this.glFeatures.cycleAnalyticsScatterplotEnabled, hasDurationChart,
hasDurationChartMedian: this.glFeatures.cycleAnalyticsScatterplotMedianEnabled, hasDurationChartMedian,
hasPathNavigation: this.glFeatures.valueStreamAnalyticsPathNavigation, hasPathNavigation,
hasFilterBar,
}); });
}, },
methods: { methods: {
...@@ -232,6 +255,11 @@ export default { ...@@ -232,6 +255,11 @@ export default {
:default-projects="selectedProjects" :default-projects="selectedProjects"
@selected="onProjectsSelect" @selected="onProjectsSelect"
/> />
<filter-bar
v-if="featureFlags.hasFilterBar"
class="js-filter-bar filtered-search-box mx-2 gl-display-none"
:disabled="!hasProject"
/>
<div <div
v-if="shouldDisplayFilters" v-if="shouldDisplayFilters"
class="ml-0 ml-md-auto mt-2 mt-md-0 d-flex flex-column flex-md-row align-items-md-center justify-content-md-end" class="ml-0 ml-md-auto mt-2 mt-md-0 d-flex flex-column flex-md-row align-items-md-center justify-content-md-end"
......
<script>
export default {
name: 'FilterBar',
components: {},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div></div>
</template>
...@@ -6,9 +6,17 @@ import { parseBoolean } from '~/lib/utils/common_utils'; ...@@ -6,9 +6,17 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default () => { export default () => {
const el = document.querySelector('#js-cycle-analytics-app'); const el = document.querySelector('#js-cycle-analytics-app');
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath, hideGroupDropDown } = el.dataset; const {
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
hideGroupDropDown,
milestonesPath = '',
labelsPath = '',
} = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset); const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore(); const store = createStore();
store.dispatch('initializeCycleAnalytics', initialData); store.dispatch('initializeCycleAnalytics', initialData);
...@@ -23,6 +31,8 @@ export default () => { ...@@ -23,6 +31,8 @@ export default () => {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
hideGroupDropDown: parseBoolean(hideGroupDropDown), hideGroupDropDown: parseBoolean(hideGroupDropDown),
milestonesPath,
labelsPath,
}, },
}), }),
}); });
......
...@@ -15,6 +15,7 @@ class Analytics::CycleAnalyticsController < Analytics::ApplicationController ...@@ -15,6 +15,7 @@ class Analytics::CycleAnalyticsController < Analytics::ApplicationController
push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true) push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true)
push_frontend_feature_flag(:cycle_analytics_scatterplot_median_enabled, default_enabled: true) push_frontend_feature_flag(:cycle_analytics_scatterplot_median_enabled, default_enabled: true)
push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group) push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group)
push_frontend_feature_flag(:value_stream_analytics_filter_bar, @group)
end end
private private
......
- page_title _("Value Stream Analytics") - page_title _("Value Stream Analytics")
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {} - data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group) } : {}
- data_attributes.merge!({ empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg"), hide_group_drop_down: 'true' }) - image_paths = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg")}
- settings = { hide_group_drop_down: 'true' }
- data_attributes.merge!(api_paths, image_paths, settings)
#js-cycle-analytics-app{ data: data_attributes } #js-cycle-analytics-app{ data: data_attributes }
...@@ -21,6 +21,7 @@ RSpec.describe 'Group Value Stream Analytics', :js do ...@@ -21,6 +21,7 @@ RSpec.describe 'Group Value Stream Analytics', :js do
stage_nav_selector = '.stage-nav' stage_nav_selector = '.stage-nav'
path_nav_selector = '.js-path-navigation' path_nav_selector = '.js-path-navigation'
filter_bar_selector = '.js-filter-bar'
3.times do |i| 3.times do |i|
let_it_be("issue_#{i}".to_sym) { create(:issue, title: "New Issue #{i}", project: project, created_at: 2.days.ago) } let_it_be("issue_#{i}".to_sym) { create(:issue, title: "New Issue #{i}", project: project, created_at: 2.days.ago) }
...@@ -153,19 +154,38 @@ RSpec.describe 'Group Value Stream Analytics', :js do ...@@ -153,19 +154,38 @@ RSpec.describe 'Group Value Stream Analytics', :js do
expect(page).to have_selector('.js-daterange-picker', visible: true) expect(page).to have_selector('.js-daterange-picker', visible: true)
end end
it 'does not show the path navigation' do it 'shows the path navigation' do
expect(page).to have_selector(path_nav_selector, visible: false) expect(page).to have_selector(path_nav_selector)
end end
context 'with path navigation feature flag enabled' do it 'shows the filter bar' do
before do expect(page).to have_selector(filter_bar_selector, visible: false)
stub_feature_flags(value_stream_analytics_path_navigation: true) end
select_group end
end
it 'shows the path navigation' do context 'with path navigation feature flag disabled' do
expect(page).to have_selector(path_nav_selector, visible: true) before do
end stub_feature_flags(value_stream_analytics_path_navigation: false)
visit analytics_cycle_analytics_path
select_group
end
it 'shows the path navigation' do
expect(page).not_to have_selector(path_nav_selector)
end
end
context 'with filter bar feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_filter_bar: false)
visit analytics_cycle_analytics_path
select_group
end
it 'does not show the filter bar' do
expect(page).not_to have_selector(filter_bar_selector)
end end
end end
......
...@@ -20,7 +20,8 @@ RSpec.describe 'Group value stream analytics' do ...@@ -20,7 +20,8 @@ RSpec.describe 'Group value stream analytics' do
expect(page).to have_pushed_frontend_feature_flags( expect(page).to have_pushed_frontend_feature_flags(
cycleAnalyticsScatterplotEnabled: true, cycleAnalyticsScatterplotEnabled: true,
cycleAnalyticsScatterplotMedianEnabled: true, cycleAnalyticsScatterplotMedianEnabled: true,
valueStreamAnalyticsPathNavigation: true valueStreamAnalyticsPathNavigation: true,
valueStreamAnalyticsFilterBar: true
) )
end end
...@@ -35,4 +36,16 @@ RSpec.describe 'Group value stream analytics' do ...@@ -35,4 +36,16 @@ RSpec.describe 'Group value stream analytics' do
expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false) expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
end end
end end
context 'when `value_stream_analytics_filter_bar` is disabled for a group' do
before do
stub_feature_flags(value_stream_analytics_filter_bar: 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(valueStreamAnalyticsFilterBar: false)
end
end
end end
...@@ -14,6 +14,7 @@ import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue' ...@@ -14,6 +14,7 @@ import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue'
import StageTableNav from 'ee/analytics/cycle_analytics/components/stage_table_nav.vue'; import StageTableNav from 'ee/analytics/cycle_analytics/components/stage_table_nav.vue';
import StageNavItem from 'ee/analytics/cycle_analytics/components/stage_nav_item.vue'; import StageNavItem from 'ee/analytics/cycle_analytics/components/stage_nav_item.vue';
import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue'; import AddStageButton from 'ee/analytics/cycle_analytics/components/add_stage_button.vue';
import FilterBar from 'ee/analytics/cycle_analytics/components/filter_bar.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue'; import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import Daterange from 'ee/analytics/shared/components/daterange.vue'; import Daterange from 'ee/analytics/shared/components/daterange.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue'; import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
...@@ -29,6 +30,8 @@ import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin'; ...@@ -29,6 +30,8 @@ import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access'; const noAccessSvgPath = 'path/to/no/access';
const emptyStateSvgPath = 'path/to/empty/state'; const emptyStateSvgPath = 'path/to/empty/state';
const milestonesPath = '/some/milestones/endpoint';
const labelsPath = '/some/labels/endpoint';
const hideGroupDropDown = false; const hideGroupDropDown = false;
const selectedGroup = convertObjectPropsToCamelCase(mockData.group); const selectedGroup = convertObjectPropsToCamelCase(mockData.group);
...@@ -53,6 +56,7 @@ function createComponent({ ...@@ -53,6 +56,7 @@ function createComponent({
withStageSelected = false, withStageSelected = false,
scatterplotEnabled = true, scatterplotEnabled = true,
pathNavigationEnabled = false, pathNavigationEnabled = false,
filterBarEnabled = false,
props = {}, props = {},
} = {}) { } = {}) {
const func = shallow ? shallowMount : mount; const func = shallow ? shallowMount : mount;
...@@ -64,6 +68,8 @@ function createComponent({ ...@@ -64,6 +68,8 @@ function createComponent({
emptyStateSvgPath, emptyStateSvgPath,
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
milestonesPath,
labelsPath,
baseStagesEndpoint: mockData.endpoints.baseStagesEndpoint, baseStagesEndpoint: mockData.endpoints.baseStagesEndpoint,
hideGroupDropDown, hideGroupDropDown,
...props, ...props,
...@@ -72,6 +78,7 @@ function createComponent({ ...@@ -72,6 +78,7 @@ function createComponent({
glFeatures: { glFeatures: {
cycleAnalyticsScatterplotEnabled: scatterplotEnabled, cycleAnalyticsScatterplotEnabled: scatterplotEnabled,
valueStreamAnalyticsPathNavigation: pathNavigationEnabled, valueStreamAnalyticsPathNavigation: pathNavigationEnabled,
valueStreamAnalyticsFilterBar: filterBarEnabled,
}, },
}, },
...opts, ...opts,
...@@ -150,6 +157,10 @@ describe('Cycle Analytics component', () => { ...@@ -150,6 +157,10 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(AddStageButton).exists()).toBe(flag); expect(wrapper.find(AddStageButton).exists()).toBe(flag);
}; };
const displaysFilterBar = flag => {
expect(wrapper.find(FilterBar).exists()).toBe(flag);
};
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent(); wrapper = createComponent();
...@@ -294,6 +305,10 @@ describe('Cycle Analytics component', () => { ...@@ -294,6 +305,10 @@ describe('Cycle Analytics component', () => {
}); });
}); });
it('displays the duration chart', () => {
displaysDurationChart(true);
});
describe('path navigation', () => { describe('path navigation', () => {
describe('disabled', () => { describe('disabled', () => {
it('does not display the path navigation', () => { it('does not display the path navigation', () => {
...@@ -315,8 +330,25 @@ describe('Cycle Analytics component', () => { ...@@ -315,8 +330,25 @@ describe('Cycle Analytics component', () => {
}); });
}); });
it('displays the duration chart', () => { describe('filter bar', () => {
displaysDurationChart(true); describe('disabled', () => {
it('does not display the filter bar', () => {
displaysFilterBar(false);
});
});
describe('enabled', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
filterBarEnabled: true,
});
});
it('displays the filter bar', () => {
displaysFilterBar(true);
});
});
}); });
describe('StageTable', () => { describe('StageTable', () => {
......
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