Commit 87aabe9f authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch '327457-vsa-fe-deep-link-the-url-query-parameters' into 'master'

VSA - Deep link the url query parameters

See merge request gitlab-org/gitlab!72777
parents b343b0ef cd4c047c
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { toYmd } from '~/analytics/shared/utils';
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 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 UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants'; import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
StageTable, StageTable,
ValueStreamFilters, ValueStreamFilters,
ValueStreamMetrics, ValueStreamMetrics,
UrlSync,
}, },
props: { props: {
noDataSvgPath: { noDataSvgPath: {
...@@ -54,6 +57,9 @@ export default { ...@@ -54,6 +57,9 @@ export default {
'pagination', 'pagination',
]), ]),
...mapGetters(['pathNavigationData', 'filterParams']), ...mapGetters(['pathNavigationData', 'filterParams']),
isLoaded() {
return !this.isLoading && !this.isLoadingStage;
},
displayStageEvents() { displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this; const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage; return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
...@@ -98,6 +104,16 @@ export default { ...@@ -98,6 +104,16 @@ export default {
metricsRequests() { metricsRequests() {
return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST; return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
}, },
query() {
return {
created_after: toYmd(this.createdAfter),
created_before: toYmd(this.createdBefore),
stage_id: this.selectedStage?.id || null,
sort: this.pagination?.sort || null,
direction: this.pagination?.direction || null,
page: this.pagination?.page || null,
};
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -176,5 +192,6 @@ export default { ...@@ -176,5 +192,6 @@ export default {
:pagination="pagination" :pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination" @handleUpdatePagination="onHandleUpdatePagination"
/> />
<url-sync v-if="isLoaded" :query="query" />
</div> </div>
</template> </template>
...@@ -5,8 +5,6 @@ import { ...@@ -5,8 +5,6 @@ import {
} from '~/api/analytics_api'; } from '~/api/analytics_api';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview'; export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = { export const DEFAULT_VALUE_STREAM = {
......
import Vue from 'vue'; import Vue from 'vue';
import {
extractFilterQueryParameters,
extractPaginationQueryParameters,
} from '~/analytics/shared/utils';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue'; import CycleAnalytics from './components/base.vue';
import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store'; import createStore from './store';
import { calculateFormattedDayInPast } from './utils'; import { buildCycleAnalyticsInitialData } from './utils';
Vue.use(Translate); Vue.use(Translate);
export default () => { export default () => {
const store = createStore(); const store = createStore();
const el = document.querySelector('#js-cycle-analytics'); const el = document.querySelector('#js-cycle-analytics');
const { const { noAccessSvgPath, noDataSvgPath } = el.dataset;
noAccessSvgPath, const initialData = buildCycleAnalyticsInitialData({ ...el.dataset, gon });
noDataSvgPath,
requestPath,
fullPath,
projectId,
groupId,
groupPath,
labelsPath,
milestonesPath,
} = el.dataset;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); const pagination = extractPaginationQueryParameters(window.location.search);
const {
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
} = extractFilterQueryParameters(window.location.search);
store.dispatch('initializeVsa', { store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10), ...initialData,
endpoints: { selectedAuthor,
requestPath, selectedMilestone,
fullPath, selectedAssigneeList,
labelsPath, selectedLabelList,
milestonesPath, pagination,
groupId: parseInt(groupId, 10),
groupPath,
},
features: {
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
},
createdBefore: new Date(now),
createdAfter: new Date(past),
}); });
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -52,7 +44,6 @@ export default () => { ...@@ -52,7 +44,6 @@ export default () => {
props: { props: {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
fullPath,
}, },
}), }),
}); });
......
...@@ -14,7 +14,7 @@ import * as types from './mutation_types'; ...@@ -14,7 +14,7 @@ import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream); commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]); return dispatch('fetchValueStreamStages');
}; };
export const fetchValueStreamStages = ({ commit, state }) => { export const fetchValueStreamStages = ({ commit, state }) => {
...@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => { ...@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state; } = state;
commit(types.REQUEST_VALUE_STREAMS); commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath) return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data)) .then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => { .catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status); commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
}); });
...@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({ ...@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({
}); });
}; };
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => { export const fetchValueStreamStageData = ({ dispatch }) =>
const stage = selectedStage || stages[0]; Promise.all([
commit(types.SET_SELECTED_STAGE, stage); dispatch('fetchCycleAnalyticsData'),
return dispatch('fetchStageData'); dispatch('fetchStageData'),
dispatch('fetchStageMedians'),
dispatch('fetchStageCountValues'),
]);
export const refetchStageData = async ({ dispatch, commit }) => {
commit(types.SET_LOADING, true);
await dispatch('fetchValueStreamStageData');
commit(types.SET_LOADING, false);
}; };
export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value); export const setSelectedStage = ({ dispatch, commit }, selectedStage = null) => {
commit(types.SET_SELECTED_STAGE, selectedStage);
const refetchStageData = (dispatch) => { return dispatch('refetchStageData');
return Promise.resolve()
.then(() => dispatch('setLoading', true))
.then(() =>
Promise.all([
dispatch('fetchCycleAnalyticsData'),
dispatch('fetchStageData'),
dispatch('fetchStageMedians'),
dispatch('fetchStageCountValues'),
]),
)
.finally(() => dispatch('setLoading', false));
}; };
export const setFilters = ({ dispatch }) => refetchStageData(dispatch); export const setFilters = ({ dispatch }) => dispatch('refetchStageData');
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => { export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore }); commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
return refetchStageData(dispatch); return dispatch('refetchStageData');
};
export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
const selectedStage = stage || stages[0];
commit(types.SET_SELECTED_STAGE, selectedStage);
return dispatch('fetchValueStreamStageData');
}; };
export const updateStageTablePagination = ( export const updateStageTablePagination = (
...@@ -190,12 +191,18 @@ export const updateStageTablePagination = ( ...@@ -190,12 +191,18 @@ export const updateStageTablePagination = (
return dispatch('fetchStageData', selectedStage.id); return dispatch('fetchStageData', selectedStage.id);
}; };
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
const { const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' }, endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
selectedStage = null,
} = initialData; } = initialData;
dispatch('filters/setEndpoints', { dispatch('filters/setEndpoints', {
labelsEndpoint: labelsPath, labelsEndpoint: labelsPath,
milestonesEndpoint: milestonesPath, milestonesEndpoint: milestonesPath,
...@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { ...@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
projectEndpoint: fullPath, projectEndpoint: fullPath,
}); });
return dispatch('setLoading', true) dispatch('filters/initialize', {
.then(() => dispatch('fetchValueStreams')) selectedAuthor,
.finally(() => dispatch('setLoading', false)); selectedMilestone,
selectedAssigneeList,
selectedLabelList,
});
commit(types.SET_LOADING, true);
await dispatch('fetchValueStreams');
await dispatch('setInitialStage', selectedStage);
commit(types.SET_LOADING, false);
}; };
import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash'; import { hideFlash } from '~/flash';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility'; import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility'; import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility'; import { slugify } from '~/lib/utils/text_utility';
...@@ -74,23 +71,6 @@ export const formatMedianValues = (medians = []) => ...@@ -74,23 +71,6 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) => export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden); stages.filter(({ hidden = false }) => hidden === isHidden);
const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
/**
* Takes an integer specifying the number of days to subtract
* from the date specified will return the 2 dates, formatted as ISO dates
*
* @param {Number} daysInPast - Number of days in the past to subtract
* @param {Date} [today=new Date] - Date to subtract days from, defaults to today
* @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
*/
export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
return {
now: toIsoFormat(today),
past: toIsoFormat(getDateInPast(today, daysInPast)),
};
};
/** /**
* @typedef {Object} MetricData * @typedef {Object} MetricData
* @property {String} title - Title of the metric measured * @property {String} title - Title of the metric measured
...@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) => ...@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '', description: popoverContent[key]?.description || '',
}; };
}); });
const extractFeatures = (gon) => ({
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
});
/**
* Builds the initial data object for Value Stream Analytics with data loaded from the backend
*
* @param {Object} dataset - dataset object paseed to the frontend via data-* properties
* @returns {Object} - The initial data to load the app with
*/
export const buildCycleAnalyticsInitialData = ({
fullPath,
requestPath,
projectId,
groupId,
groupPath,
labelsPath,
milestonesPath,
stage,
createdAfter,
createdBefore,
gon,
} = {}) => {
return {
projectId: parseInt(projectId, 10),
endpoints: {
requestPath,
fullPath,
labelsPath,
milestonesPath,
groupId: parseInt(groupId, 10),
groupPath,
},
createdAfter: new Date(createdAfter),
createdBefore: new Date(createdBefore),
selectedStage: stage ? JSON.parse(stage) : null,
features: extractFeatures(gon),
};
};
...@@ -6,8 +6,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -6,8 +6,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include CycleAnalyticsParams include CycleAnalyticsParams
include GracefulTimeoutHandling include GracefulTimeoutHandling
include RedisTracking include RedisTracking
extend ::Gitlab::Utils::Override
before_action :authorize_read_cycle_analytics! before_action :authorize_read_cycle_analytics!
before_action :load_value_stream, only: :show
track_redis_hll_event :show, name: 'p_analytics_valuestream' track_redis_hll_event :show, name: 'p_analytics_valuestream'
...@@ -19,6 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -19,6 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show def show
@cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params)) @cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params))
@request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -34,6 +37,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -34,6 +37,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private private
override :all_cycle_analytics_params
def all_cycle_analytics_params
super.merge({ project: @project, value_stream: @value_stream })
end
def load_value_stream
@value_stream = Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
end
def cycle_analytics_json def cycle_analytics_json
{ {
summary: @cycle_analytics.summary, summary: @cycle_analytics.summary,
......
- page_title _("Value Stream Analytics") - page_title _("Value Stream Analytics")
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- data_attributes.merge!(cycle_analytics_initial_data(@project, @group))
- add_page_specific_style 'page_bundles/cycle_analytics' - add_page_specific_style 'page_bundles/cycle_analytics'
#js-cycle-analytics{ data: cycle_analytics_initial_data(@project, @group) } #js-cycle-analytics{ data: data_attributes }
...@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' } let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
let_it_be(:stage_filter_bar) { '[data-testid="vsa-filter-bar"]' }
let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' } let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' }
let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' } let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' }
let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' } let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' }
...@@ -27,6 +28,9 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -27,6 +28,9 @@ RSpec.describe 'Value Stream Analytics', :js do
def set_daterange(from_date, to_date) def set_daterange(from_date, to_date)
page.find(".js-daterange-picker-from input").set(from_date) page.find(".js-daterange-picker-from input").set(from_date)
page.find(".js-daterange-picker-to input").set(to_date) page.find(".js-daterange-picker-to input").set(to_date)
# simulate a blur event
page.find(".js-daterange-picker-to input").send_keys(:tab)
wait_for_all_requests wait_for_all_requests
end end
...@@ -158,6 +162,18 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -158,6 +162,18 @@ RSpec.describe 'Value Stream Analytics', :js do
expect(page).not_to have_text(original_first_title, exact: true) expect(page).not_to have_text(original_first_title, exact: true)
end end
it 'can navigate directly to a value stream stream stage with filters applied' do
visit project_cycle_analytics_path(project, created_before: '2019-12-31', created_after: '2019-11-01', stage_id: 'code', milestone_title: milestone.title)
wait_for_requests
expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Code')
expect(page.find(".js-daterange-picker-from input").value).to eq("2019-11-01")
expect(page.find(".js-daterange-picker-to input").value).to eq("2019-12-31")
filter_bar = page.find(stage_filter_bar)
expect(filter_bar.find(".gl-filtered-search-token-data-content").text).to eq("%#{milestone.title}")
end
def stage_time_column def stage_time_column
stage_table.find(stage_table_duration_column_header_selector).ancestor("th") stage_table.find(stage_table_duration_column_header_selector).ancestor("th")
end end
......
...@@ -9,7 +9,6 @@ import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_ ...@@ -9,7 +9,6 @@ import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { import {
DEFAULT_VALUE_STREAM, DEFAULT_VALUE_STREAM,
DEFAULT_DAYS_IN_PAST,
PAGINATION_TYPE, PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_FIELD_END_EVENT,
...@@ -17,6 +16,7 @@ import { ...@@ -17,6 +16,7 @@ import {
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility'; import { getDateInPast } from '~/lib/utils/datetime_utility';
const DEFAULT_DAYS_IN_PAST = 30;
export const createdBefore = new Date(2019, 0, 14); export const createdBefore = new Date(2019, 0, 14);
export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST); export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST);
......
...@@ -57,22 +57,12 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -57,22 +57,12 @@ describe('Project Value Stream Analytics actions', () => {
const mutationTypes = (arr) => arr.map(({ type }) => type); const mutationTypes = (arr) => arr.map(({ type }) => type);
const mockFetchStageDataActions = [
{ type: 'setLoading', payload: true },
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
{ type: 'setLoading', payload: false },
];
describe.each` describe.each`
action | payload | expectedActions | expectedMutations action | payload | expectedActions | expectedMutations
${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]} ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]} ${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]} ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]} ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => { `('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations); const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () => it(`will dispatch ${expectedActions} and commit ${types}`, () =>
...@@ -86,9 +76,18 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -86,9 +76,18 @@ describe('Project Value Stream Analytics actions', () => {
}); });
describe('initializeVsa', () => { describe('initializeVsa', () => {
let mockDispatch; const selectedAuthor = 'Author';
let mockCommit; const selectedMilestone = 'Milestone 1';
const payload = { endpoints: mockEndpoints }; const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
const selectedLabelList = ['Label 1', 'Label 2'];
const payload = {
endpoints: mockEndpoints,
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
selectedStage,
};
const mockFilterEndpoints = { const mockFilterEndpoints = {
groupEndpoint: 'foo', groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath, labelsEndpoint: mockLabelsPath,
...@@ -96,27 +95,63 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -96,27 +95,63 @@ describe('Project Value Stream Analytics actions', () => {
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams', projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
}; };
it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
return testAction({
action: actions.initializeVsa,
state: {},
payload,
expectedMutations: [
{ type: 'INITIALIZE_VSA', payload },
{ type: 'SET_LOADING', payload: true },
{ type: 'SET_LOADING', payload: false },
],
expectedActions: [
{ type: 'filters/setEndpoints', payload: mockFilterEndpoints },
{
type: 'filters/initialize',
payload: { selectedAuthor, selectedMilestone, selectedAssigneeList, selectedLabelList },
},
{ type: 'fetchValueStreams' },
{ type: 'setInitialStage', payload: selectedStage },
],
});
});
});
describe('setInitialStage', () => {
beforeEach(() => { beforeEach(() => {
mockDispatch = jest.fn(() => Promise.resolve()); state = { ...state, stages: allowedStages };
mockCommit = jest.fn();
}); });
it('will dispatch the setLoading and fetchValueStreams actions and commit INITIALIZE_VSA', async () => { describe('with a selected stage', () => {
await actions.initializeVsa( it('will commit `SET_SELECTED_STAGE` and fetchValueStreamStageData actions', () => {
{ const fakeStage = { ...selectedStage, id: 'fake', name: 'fake-stae' };
...state, return testAction({
dispatch: mockDispatch, action: actions.setInitialStage,
commit: mockCommit, state,
}, payload: fakeStage,
payload, expectedMutations: [
); {
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints }); type: 'SET_SELECTED_STAGE',
payload: fakeStage,
expect(mockDispatch).toHaveBeenCalledTimes(4); },
expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints); ],
expect(mockDispatch).toHaveBeenCalledWith('setLoading', true); expectedActions: [{ type: 'fetchValueStreamStageData' }],
expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams'); });
expect(mockDispatch).toHaveBeenCalledWith('setLoading', false); });
});
describe('without a selected stage', () => {
it('will select the first stage from the value stream', () => {
const [firstStage] = allowedStages;
testAction({
action: actions.setInitialStage,
state,
payload: null,
expectedMutations: [{ type: 'SET_SELECTED_STAGE', payload: firstStage }],
expectedActions: [{ type: 'fetchValueStreamStageData' }],
});
});
}); });
}); });
...@@ -270,12 +305,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -270,12 +305,7 @@ describe('Project Value Stream Analytics actions', () => {
state, state,
payload: {}, payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }], expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [ expectedActions: [{ type: 'receiveValueStreamsSuccess' }],
{ type: 'receiveValueStreamsSuccess' },
{ type: 'setSelectedStage' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
],
})); }));
describe('with a failing request', () => { describe('with a failing request', () => {
...@@ -483,4 +513,34 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -483,4 +513,34 @@ describe('Project Value Stream Analytics actions', () => {
})); }));
}); });
}); });
describe('refetchStageData', () => {
it('will commit SET_LOADING and dispatch fetchValueStreamStageData actions', () =>
testAction({
action: actions.refetchStageData,
state,
payload: {},
expectedMutations: [
{ type: 'SET_LOADING', payload: true },
{ type: 'SET_LOADING', payload: false },
],
expectedActions: [{ type: 'fetchValueStreamStageData' }],
}));
});
describe('fetchValueStreamStageData', () => {
it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
testAction({
action: actions.fetchValueStreamStageData,
state,
payload: {},
expectedMutations: [],
expectedActions: [
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
],
}));
});
}); });
...@@ -101,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -101,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }} ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} ${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]} ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages} ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians} ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
......
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json'; import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import { useFakeDate } from 'helpers/fake_date';
import { import {
transformStagesForPathNavigation, transformStagesForPathNavigation,
medianTimeToParsedSeconds, medianTimeToParsedSeconds,
formatMedianValues, formatMedianValues,
filterStagesByHiddenStatus, filterStagesByHiddenStatus,
calculateFormattedDayInPast,
prepareTimeMetricsData, prepareTimeMetricsData,
buildCycleAnalyticsInitialData,
} from '~/cycle_analytics/utils'; } from '~/cycle_analytics/utils';
import { slugify } from '~/lib/utils/text_utility'; import { slugify } from '~/lib/utils/text_utility';
import { import {
...@@ -90,14 +89,6 @@ describe('Value stream analytics utils', () => { ...@@ -90,14 +89,6 @@ describe('Value stream analytics utils', () => {
}); });
}); });
describe('calculateFormattedDayInPast', () => {
useFakeDate(1815, 11, 10);
it('will return 2 dates, now and past', () => {
expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
});
});
describe('prepareTimeMetricsData', () => { describe('prepareTimeMetricsData', () => {
let prepared; let prepared;
const [first, second] = metricsData; const [first, second] = metricsData;
...@@ -125,4 +116,87 @@ describe('Value stream analytics utils', () => { ...@@ -125,4 +116,87 @@ describe('Value stream analytics utils', () => {
]); ]);
}); });
}); });
describe('buildCycleAnalyticsInitialData', () => {
let res = null;
const projectId = '5';
const createdAfter = '2021-09-01';
const createdBefore = '2021-11-06';
const groupId = '146';
const groupPath = 'fake-group';
const fullPath = 'fake-group/fake-project';
const labelsPath = '/fake-group/fake-project/-/labels.json';
const milestonesPath = '/fake-group/fake-project/-/milestones.json';
const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
const rawData = {
projectId,
createdBefore,
createdAfter,
fullPath,
requestPath,
labelsPath,
milestonesPath,
groupId,
groupPath,
};
describe('with minimal data', () => {
beforeEach(() => {
res = buildCycleAnalyticsInitialData(rawData);
});
it('sets the projectId', () => {
expect(res.projectId).toBe(parseInt(projectId, 10));
});
it('sets the date range', () => {
expect(res.createdBefore).toEqual(new Date(createdBefore));
expect(res.createdAfter).toEqual(new Date(createdAfter));
});
it('sets the endpoints', () => {
const { endpoints } = res;
expect(endpoints.fullPath).toBe(fullPath);
expect(endpoints.requestPath).toBe(requestPath);
expect(endpoints.labelsPath).toBe(labelsPath);
expect(endpoints.milestonesPath).toBe(milestonesPath);
expect(endpoints.groupId).toBe(parseInt(groupId, 10));
expect(endpoints.groupPath).toBe(groupPath);
});
it('returns null when there is no stage', () => {
expect(res.selectedStage).toBeNull();
});
it('returns false for missing features', () => {
expect(res.features.cycleAnalyticsForGroups).toBe(false);
});
});
describe('with a stage set', () => {
const jsonStage = '{"id":"fakeStage","title":"fakeStage"}';
it('parses the selectedStage data', () => {
res = buildCycleAnalyticsInitialData({ ...rawData, stage: jsonStage });
const { selectedStage: stage } = res;
expect(stage.id).toBe('fakeStage');
expect(stage.title).toBe('fakeStage');
});
});
describe('with features set', () => {
const fakeFeatures = { cycleAnalyticsForGroups: true };
it('sets the feature flags', () => {
res = buildCycleAnalyticsInitialData({
...rawData,
gon: { licensed_features: fakeFeatures },
});
expect(res.features).toEqual(fakeFeatures);
});
});
});
}); });
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