Commit 89c07866 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added filter bar to project VSA

Adds a filter bar to project VSA to support
filtering at the project level

Update related jest specs

Adds updated specs for the cycle analytics filter
bar in project level VSA.

Changelog: added
parent 5a614293
...@@ -4,6 +4,7 @@ import Cookies from 'js-cookie'; ...@@ -4,6 +4,7 @@ import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue'; import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants'; import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
GlSprintf, GlSprintf,
PathNavigation, PathNavigation,
StageTable, StageTable,
ValueStreamFilters,
ValueStreamMetrics, ValueStreamMetrics,
}, },
props: { props: {
...@@ -50,6 +52,8 @@ export default { ...@@ -50,6 +52,8 @@ export default {
'stageCounts', 'stageCounts',
'endpoints', 'endpoints',
'features', 'features',
'createdBefore',
'createdAfter',
]), ]),
...mapGetters(['pathNavigationData', 'filterParams']), ...mapGetters(['pathNavigationData', 'filterParams']),
displayStageEvents() { displayStageEvents() {
...@@ -157,6 +161,13 @@ export default { ...@@ -157,6 +161,13 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<value-stream-filters
class="gl-w-full"
:group-id="endpoints.groupId"
:group-path="endpoints.groupPath"
:has-project-filter="false"
:has-date-range-filter="false"
/>
<value-stream-metrics <value-stream-metrics
:request-path="endpoints.fullPath" :request-path="endpoints.fullPath"
:request-params="filterParams" :request-params="filterParams"
......
...@@ -14,15 +14,21 @@ export default () => { ...@@ -14,15 +14,21 @@ export default () => {
requestPath, requestPath,
fullPath, fullPath,
projectId, projectId,
groupId,
groupPath, groupPath,
labelsPath,
milestonesPath,
} = el.dataset; } = el.dataset;
store.dispatch('initializeVsa', { store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10), projectId: parseInt(projectId, 10),
groupPath,
endpoints: { endpoints: {
requestPath, requestPath,
fullPath, fullPath,
labelsPath,
milestonesPath,
groupId: parseInt(groupId, 10),
groupPath,
}, },
features: { features: {
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups), cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants'; import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
import { appendExtension } from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
...@@ -178,6 +179,16 @@ export const setDateRange = ({ dispatch, commit }, daysInPast) => { ...@@ -178,6 +179,16 @@ export const setDateRange = ({ dispatch, commit }, daysInPast) => {
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
} = initialData;
dispatch('filters/setEndpoints', {
labelsEndpoint: appendExtension(labelsPath),
milestonesEndpoint: appendExtension(milestonesPath),
groupEndpoint: groupPath,
projectEndpoint: fullPath,
});
return dispatch('setLoading', true) return dispatch('setLoading', true)
.then(() => dispatch('fetchValueStreams')) .then(() => dispatch('fetchValueStreams'))
.finally(() => dispatch('setLoading', false)); .finally(() => dispatch('setLoading', false));
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils'; import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => { export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
...@@ -20,6 +21,21 @@ export const requestParams = (state) => { ...@@ -20,6 +21,21 @@ export const requestParams = (state) => {
return { requestPath: fullPath, valueStreamId, stageId }; return { requestPath: fullPath, valueStreamId, stageId };
}; };
const filterBarParams = ({ filters }) => {
const {
authors: { selected: selectedAuthor },
milestones: { selected: selectedMilestone },
assignees: { selectedList: selectedAssigneeList },
labels: { selectedList: selectedLabelList },
} = filters;
return filterToQueryObject({
milestone_title: selectedMilestone,
author_username: selectedAuthor,
label_name: selectedLabelList,
assignee_username: selectedAssigneeList,
});
};
const dateRangeParams = ({ createdAfter, createdBefore }) => ({ const dateRangeParams = ({ createdAfter, createdBefore }) => ({
created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null, created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null, created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
...@@ -33,6 +49,7 @@ export const legacyFilterParams = ({ daysInPast }) => { ...@@ -33,6 +49,7 @@ export const legacyFilterParams = ({ daysInPast }) => {
export const filterParams = (state) => { export const filterParams = (state) => {
return { return {
...filterBarParams(state),
...dateRangeParams(state), ...dateRangeParams(state),
}; };
}; };
...@@ -149,3 +149,6 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) => ...@@ -149,3 +149,6 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '', description: popoverContent[key]?.description || '',
}; };
}); });
// TODO: maybe use paths.join
export const appendExtension = (path) => (path.indexOf('.') > -1 ? path : `${path}.json`);
- page_title _("Value Stream Analytics") - page_title _("Value Stream Analytics")
- add_page_specific_style 'page_bundles/cycle_analytics' - add_page_specific_style 'page_bundles/cycle_analytics'
- svgs = { 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") } - svgs = { 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") }
- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs) - api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group), group_path: group_path(@group), group_id: @group&.id } : { milestones_path: project_milestones_path(@project), labels_path: project_labels_path(@project), group_path: @project.parent&.path, group_id: @project.parent&.id }
- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs, api_paths)
#js-cycle-analytics{ data: initial_data } #js-cycle-analytics{ data: initial_data }
import Api from 'ee/api'; import { removeFlash, appendExtension } from '~/cycle_analytics/utils';
import { removeFlash } from '~/cycle_analytics/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -9,8 +8,6 @@ export * from './actions/filters'; ...@@ -9,8 +8,6 @@ export * from './actions/filters';
export * from './actions/stages'; export * from './actions/stages';
export * from './actions/value_streams'; export * from './actions/value_streams';
const appendExtension = (path) => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ dispatch }, options) => { export const setPaths = ({ dispatch }, options) => {
const { groupPath, milestonesPath = '', labelsPath = '' } = options; const { groupPath, milestonesPath = '', labelsPath = '' } = options;
......
...@@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; ...@@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BaseComponent from '~/cycle_analytics/components/base.vue'; import BaseComponent from '~/cycle_analytics/components/base.vue';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue'; import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state'; import initState from '~/cycle_analytics/store/state';
...@@ -30,13 +31,14 @@ Vue.use(Vuex); ...@@ -30,13 +31,14 @@ Vue.use(Vuex);
let wrapper; let wrapper;
const { id: groupId, path: groupPath } = currentGroup;
const defaultState = { const defaultState = {
permissions, permissions,
currentGroup, currentGroup,
createdBefore, createdBefore,
createdAfter, createdAfter,
stageCounts, stageCounts,
endpoints: { fullPath }, endpoints: { fullPath, groupId, groupPath },
}; };
function createStore({ initialState = {}, initialGetters = {} }) { function createStore({ initialState = {}, initialGetters = {} }) {
...@@ -74,6 +76,7 @@ function createComponent({ initialState, initialGetters } = {}) { ...@@ -74,6 +76,7 @@ function createComponent({ initialState, initialGetters } = {}) {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPathNavigation = () => wrapper.findComponent(PathNavigation); const findPathNavigation = () => wrapper.findComponent(PathNavigation);
const findFilterBar = () => wrapper.findComponent(ValueStreamFilters);
const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics); const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable); const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents'); const findStageEvents = () => findStageTable().props('stageEvents');
...@@ -123,6 +126,17 @@ describe('Value stream analytics component', () => { ...@@ -123,6 +126,17 @@ describe('Value stream analytics component', () => {
expect(findStageEvents()).toEqual(selectedStageEvents); expect(findStageEvents()).toEqual(selectedStageEvents);
}); });
it('renders the filter bar', () => {
expect(findFilterBar().exists()).toBe(true);
});
it('hides the project selector and range selector', () => {
expect(findFilterBar().props()).toMatchObject({
hasProjectFilter: false,
hasDateRangeFilter: false,
});
});
it('does not render the loading icon', () => { it('does not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
}); });
......
...@@ -4,12 +4,22 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -4,12 +4,22 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions'; import * as actions from '~/cycle_analytics/store/actions';
import * as getters from '~/cycle_analytics/store/getters'; import * as getters from '~/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { allowedStages, selectedStage, selectedValueStream } from '../mock_data'; import { allowedStages, selectedStage, selectedValueStream, currentGroup } from '../mock_data';
const { id: groupId, path: groupPath } = currentGroup;
const mockMilestonesPath = 'mock-milestones';
const mockLabelsPath = 'mock-labels';
const mockRequestPath = 'some/cool/path'; const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams'; const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockStartDate = 30; const mockStartDate = 30;
const mockEndpoints = { fullPath: mockFullPath, requestPath: mockRequestPath }; const mockEndpoints = {
fullPath: mockFullPath,
requestPath: mockRequestPath,
labelsPath: mockLabelsPath,
milestonesPath: mockMilestonesPath,
groupId,
groupPath,
};
const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' }; const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
const defaultState = { ...getters, selectedValueStream }; const defaultState = { ...getters, selectedValueStream };
...@@ -60,6 +70,12 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -60,6 +70,12 @@ describe('Project Value Stream Analytics actions', () => {
let mockDispatch; let mockDispatch;
let mockCommit; let mockCommit;
const payload = { endpoints: mockEndpoints }; const payload = { endpoints: mockEndpoints };
const mockFilterEndpoints = {
groupEndpoint: 'foo',
labelsEndpoint: 'mock-labels.json',
milestonesEndpoint: 'mock-milestones.json',
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
};
beforeEach(() => { beforeEach(() => {
mockDispatch = jest.fn(() => Promise.resolve()); mockDispatch = jest.fn(() => Promise.resolve());
...@@ -76,6 +92,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -76,6 +92,7 @@ describe('Project Value Stream Analytics actions', () => {
payload, payload,
); );
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints }); expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints);
expect(mockDispatch).toHaveBeenCalledWith('setLoading', true); expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams'); expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
expect(mockDispatch).toHaveBeenCalledWith('setLoading', false); expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
......
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