Commit 9ed55b6d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '326701-move-vsa-filters-to-ce' into 'master'

Migrate group VSA filters to project level

See merge request gitlab-org/gitlab!67340
parents 409f9aa6 0bbcd7f0
import dateFormat from 'dateformat';
import { dateFormats } from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => { export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data; if (!searchTerm?.length) return data;
return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase())); return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
}; };
export const toYmd = (date) => dateFormat(date, dateFormats.isoDate);
<script> <script>
import { GlIcon, GlLoadingIcon, GlSprintf } 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 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';
...@@ -13,11 +14,10 @@ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; ...@@ -13,11 +14,10 @@ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
export default { export default {
name: 'CycleAnalytics', name: 'CycleAnalytics',
components: { components: {
GlIcon,
GlLoadingIcon, GlLoadingIcon,
GlSprintf,
PathNavigation, PathNavigation,
StageTable, StageTable,
ValueStreamFilters,
ValueStreamMetrics, ValueStreamMetrics,
}, },
props: { props: {
...@@ -45,11 +45,12 @@ export default { ...@@ -45,11 +45,12 @@ export default {
'selectedStageError', 'selectedStageError',
'stages', 'stages',
'summary', 'summary',
'daysInPast',
'permissions', 'permissions',
'stageCounts', 'stageCounts',
'endpoints', 'endpoints',
'features', 'features',
'createdBefore',
'createdAfter',
]), ]),
...mapGetters(['pathNavigationData', 'filterParams']), ...mapGetters(['pathNavigationData', 'filterParams']),
displayStageEvents() { displayStageEvents() {
...@@ -99,8 +100,11 @@ export default { ...@@ -99,8 +100,11 @@ export default {
}, },
methods: { methods: {
...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']), ...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
handleDateSelect(daysInPast) { onSetDateRange({ startDate, endDate }) {
this.setDateRange(daysInPast); this.setDateRange({
createdAfter: new Date(startDate),
createdBefore: new Date(endDate),
});
}, },
onSelectStage(stage) { onSelectStage(stage) {
this.setSelectedStage(stage); this.setSelectedStage(stage);
...@@ -134,29 +138,15 @@ export default { ...@@ -134,29 +138,15 @@ export default {
:selected-stage="selectedStage" :selected-stage="selectedStage"
@selected="onSelectStage" @selected="onSelectStage"
/> />
<div class="gl-flex-grow gl-align-self-end">
<div class="js-ca-dropdown dropdown inline">
<!-- eslint-disable-next-line @gitlab/vue-no-data-toggle -->
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
<span class="dropdown-label">
<gl-sprintf :message="$options.i18n.dropdownText">
<template #days>{{ daysInPast }}</template>
</gl-sprintf>
<gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
</span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
<a href="#" @click.prevent="handleDateSelect(days)">
<gl-sprintf :message="$options.i18n.dropdownText">
<template #days>{{ days }}</template>
</gl-sprintf>
</a>
</li>
</ul>
</div>
</div>
</div> </div>
<value-stream-filters
:group-id="endpoints.groupId"
:group-path="endpoints.groupPath"
:has-project-filter="false"
:start-date="createdAfter"
:end-date="createdBefore"
@setDateRange="onSetDateRange"
/>
<value-stream-metrics <value-stream-metrics
:request-path="endpoints.fullPath" :request-path="endpoints.fullPath"
:request-params="filterParams" :request-params="filterParams"
......
...@@ -68,26 +68,30 @@ export default { ...@@ -68,26 +68,30 @@ export default {
v-if="hasDateRangeFilter || hasProjectFilter" v-if="hasDateRangeFilter || hasProjectFilter"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between" class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
> >
<projects-dropdown-filter <div>
v-if="hasProjectFilter" <projects-dropdown-filter
:key="groupId" v-if="hasProjectFilter"
class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0" :key="groupId"
:group-id="groupId" class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
:group-namespace="groupPath" :group-id="groupId"
:query-params="projectsQueryParams" :group-namespace="groupPath"
:multi-select="$options.multiProjectSelect" :query-params="projectsQueryParams"
:default-projects="selectedProjects" :multi-select="$options.multiProjectSelect"
@selected="$emit('selectProject', $event)" :default-projects="selectedProjects"
/> @selected="$emit('selectProject', $event)"
<date-range />
v-if="hasDateRangeFilter" </div>
:start-date="startDate" <div>
:end-date="endDate" <date-range
:max-date-range="$options.maxDateRange" v-if="hasDateRangeFilter"
:include-selected-date="true" :start-date="startDate"
class="js-daterange-picker" :end-date="endDate"
@change="$emit('setDateRange', $event)" :max-date-range="$options.maxDateRange"
/> :include-selected-date="true"
class="js-daterange-picker"
@change="$emit('setDateRange', $event)"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
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';
Vue.use(Translate); Vue.use(Translate);
...@@ -14,19 +16,29 @@ export default () => { ...@@ -14,19 +16,29 @@ export default () => {
requestPath, requestPath,
fullPath, fullPath,
projectId, projectId,
groupId,
groupPath, groupPath,
labelsPath,
milestonesPath,
} = el.dataset; } = el.dataset;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
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),
}, },
createdBefore: new Date(now),
createdAfter: new Date(past),
}); });
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
......
...@@ -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) => {
...@@ -163,6 +164,7 @@ const refetchStageData = (dispatch) => { ...@@ -163,6 +164,7 @@ const refetchStageData = (dispatch) => {
dispatch('fetchCycleAnalyticsData'), dispatch('fetchCycleAnalyticsData'),
dispatch('fetchStageData'), dispatch('fetchStageData'),
dispatch('fetchStageMedians'), dispatch('fetchStageMedians'),
dispatch('fetchStageCountValues'),
]), ]),
) )
.finally(() => dispatch('setLoading', false)); .finally(() => dispatch('setLoading', false));
...@@ -170,14 +172,24 @@ const refetchStageData = (dispatch) => { ...@@ -170,14 +172,24 @@ const refetchStageData = (dispatch) => {
export const setFilters = ({ dispatch }) => refetchStageData(dispatch); export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
export const setDateRange = ({ dispatch, commit }, daysInPast) => { export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, daysInPast); commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
return refetchStageData(dispatch); return refetchStageData(dispatch);
}; };
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),
}; };
}; };
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { formatMedianValues } from '../utils';
import { formatMedianValues, calculateFormattedDayInPast } from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.INITIALIZE_VSA](state, { endpoints, features }) { [types.INITIALIZE_VSA](state, { endpoints, features, createdBefore, createdAfter }) {
state.endpoints = endpoints; state.endpoints = endpoints;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); state.createdBefore = createdBefore;
state.createdBefore = now; state.createdAfter = createdAfter;
state.createdAfter = past;
state.features = features; state.features = features;
}, },
[types.SET_LOADING](state, loadingState) { [types.SET_LOADING](state, loadingState) {
...@@ -20,11 +18,9 @@ export default { ...@@ -20,11 +18,9 @@ export default {
[types.SET_SELECTED_STAGE](state, stage) { [types.SET_SELECTED_STAGE](state, stage) {
state.selectedStage = stage; state.selectedStage = stage;
}, },
[types.SET_DATE_RANGE](state, daysInPast) { [types.SET_DATE_RANGE](state, { createdAfter, createdBefore }) {
state.daysInPast = daysInPast; state.createdBefore = createdBefore;
const { now, past } = calculateFormattedDayInPast(daysInPast); state.createdAfter = createdAfter;
state.createdBefore = now;
state.createdAfter = past;
}, },
[types.REQUEST_VALUE_STREAMS](state) { [types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = []; state.valueStreams = [];
......
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({ export default () => ({
id: null, id: null,
features: {}, features: {},
endpoints: {}, endpoints: {},
daysInPast: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null, createdAfter: null,
createdBefore: null, createdBefore: null,
stages: [], stages: [],
......
...@@ -149,3 +149,5 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) => ...@@ -149,3 +149,5 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '', description: popoverContent[key]?.description || '',
}; };
}); });
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 }
<script> <script>
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
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 { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import { toYmd } from '../../shared/utils';
import { METRICS_REQUESTS } from '../constants'; import { METRICS_REQUESTS } from '../constants';
import DurationChart from './duration_chart.vue'; import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue'; import TypeOfWorkCharts from './type_of_work_charts.vue';
......
import Api from 'ee/api'; import Api from 'ee/api';
import { removeFlash } from '~/cycle_analytics/utils'; import { removeFlash, appendExtension } 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 +9,6 @@ export * from './actions/filters'; ...@@ -9,8 +9,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;
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { isNumber, uniqBy } from 'lodash'; import { isNumber, uniqBy } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils'; import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -8,7 +9,6 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; ...@@ -8,7 +9,6 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, dayAfter, secondsToDays, getDatesInRange } from '~/lib/utils/datetime_utility'; import { newDate, dayAfter, secondsToDays, getDatesInRange } from '~/lib/utils/datetime_utility';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { toYmd } from '../shared/utils';
const EVENT_TYPE_LABEL = 'label'; const EVENT_TYPE_LABEL = 'label';
......
...@@ -3,12 +3,6 @@ import { dateFormats } from '~/analytics/shared/constants'; ...@@ -3,12 +3,6 @@ import { dateFormats } from '~/analytics/shared/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const toYmd = (date) => dateFormat(date, dateFormats.isoDate);
export default {
toYmd,
};
export const formattedDate = (d) => dateFormat(d, dateFormats.defaultDate); export const formattedDate = (d) => dateFormat(d, dateFormats.defaultDate);
/** /**
......
...@@ -8,7 +8,6 @@ import DurationChart from 'ee/analytics/cycle_analytics/components/duration_char ...@@ -8,7 +8,6 @@ import DurationChart from 'ee/analytics/cycle_analytics/components/duration_char
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';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue'; import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import createStore from 'ee/analytics/cycle_analytics/store'; import createStore from 'ee/analytics/cycle_analytics/store';
import { toYmd } from 'ee/analytics/shared/utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import {
currentGroup, currentGroup,
...@@ -16,6 +15,7 @@ import { ...@@ -16,6 +15,7 @@ import {
createdAfter, createdAfter,
selectedProjects, selectedProjects,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
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';
......
...@@ -9,7 +9,6 @@ import { ...@@ -9,7 +9,6 @@ import {
getTasksByTypeData, getTasksByTypeData,
transformRawTasksByTypeData, transformRawTasksByTypeData,
} from 'ee/analytics/cycle_analytics/utils'; } from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { import {
getStageByTitle, getStageByTitle,
...@@ -19,6 +18,7 @@ import { ...@@ -19,6 +18,7 @@ import {
createdAfter, createdAfter,
deepCamelCase, deepCamelCase,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils';
import { import {
PAGINATION_TYPE, PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_DIRECTION_DESC,
......
...@@ -17,8 +17,8 @@ import { ...@@ -17,8 +17,8 @@ import {
prepareStageErrors, prepareStageErrors,
formatMedianValuesWithOverview, formatMedianValuesWithOverview,
} from 'ee/analytics/cycle_analytics/utils'; } from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { createdAfter, createdBefore, rawStageMedians } from 'jest/cycle_analytics/mock_data'; import { createdAfter, createdBefore, rawStageMedians } from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils'; import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDatesInRange } from '~/lib/utils/datetime_utility';
......
...@@ -5,35 +5,40 @@ require 'spec_helper' ...@@ -5,35 +5,40 @@ require 'spec_helper'
RSpec.describe 'Value Stream Analytics', :js do 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(:project) { create(:project, :repository) }
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_table_event_selector) { '[data-testid="vsa-stage-event"]' }
let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" } let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" }
let_it_be(:metric_value_selector) { "[data-testid='displayValue']" }
let(:stage_table) { page.find(stage_table_selector) }
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
def metrics_values
page.find(metrics_selector).all(metric_value_selector).collect(&:text)
end
def set_daterange(from_date, to_date)
page.find(".js-daterange-picker-from input").set(from_date)
page.find(".js-daterange-picker-to input").set(to_date)
wait_for_all_requests
end
context 'as an allowed user' do context 'as an allowed user' do
context 'when project is new' do context 'when project is new' do
before(:all) do
project.add_maintainer(user)
end
before do before do
project.add_maintainer(user)
sign_in(user) sign_in(user)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
wait_for_requests wait_for_requests
end end
it 'displays metrics' do it 'displays metrics with relevant values' do
aggregate_failures 'with relevant values' do expect(metrics_values).to eq(['-'] * 4)
expect(new_issues_counter).to have_content('-')
expect(commits_counter).to have_content('-')
expect(deploys_counter).to have_content('-')
expect(deployment_frequency_counter).to have_content('-')
end
end end
it 'shows active stage with empty message' do it 'shows active stage with empty message' do
...@@ -43,14 +48,21 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -43,14 +48,21 @@ RSpec.describe 'Value Stream Analytics', :js do
end end
context "when there's value stream analytics data" do context "when there's value stream analytics data" do
# NOTE: in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68595 travel back
# 5 days in time before we create data for these specs, to mitigate some flakiness
# So setting the date range to be the last 2 days should skip past the existing data
from = 2.days.ago.strftime("%Y-%m-%d")
to = 1.day.ago.strftime("%Y-%m-%d")
around do |example| around do |example|
travel_to(5.days.ago) { example.run } travel_to(5.days.ago) { example.run }
end end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
create_list(:issue, 2, project: project, created_at: 2.weeks.ago, milestone: milestone)
@build = create_cycle(user, project, issue, mr, milestone, pipeline) create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project) deploy_master(user, project)
issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour) issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour)
...@@ -65,6 +77,8 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -65,6 +77,8 @@ RSpec.describe 'Value Stream Analytics', :js do
sign_in(user) sign_in(user)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
wait_for_requests
end end
it 'displays metrics' do it 'displays metrics' do
...@@ -97,18 +111,20 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -97,18 +111,20 @@ RSpec.describe 'Value Stream Analytics', :js do
expect_merge_request_to_be_present expect_merge_request_to_be_present
end end
context "when I change the time period observed" do it 'can filter the issues by date' do
before do expect(stage_table.all(stage_table_event_selector).length).to eq(3)
_two_weeks_old_issue = create(:issue, project: project, created_at: 2.weeks.ago)
click_button('Last 30 days') set_daterange(from, to)
click_link('Last 7 days')
wait_for_requests
end
it 'shows only relevant data' do expect(stage_table.all(stage_table_event_selector).length).to eq(0)
expect(new_issue_counter).to have_content('1') end
end
it 'can filter the metrics by date' do
expect(metrics_values).to eq(["3.0", "2.0", "1.0", "0.0"])
set_daterange(from, to)
expect(metrics_values).to eq(['-'] * 4)
end end
end end
end end
...@@ -141,31 +157,6 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -141,31 +157,6 @@ RSpec.describe 'Value Stream Analytics', :js do
end end
end end
def find_metric_tile(sel)
page.find("#{metrics_selector} #{sel}")
end
# When now use proper pluralization for the metric names, which affects the id
def new_issue_counter
find_metric_tile("#new-issue")
end
def new_issues_counter
find_metric_tile("#new-issues")
end
def commits_counter
find_metric_tile("#commits")
end
def deploys_counter
find_metric_tile("#deploys")
end
def deployment_frequency_counter
find_metric_tile("#deployment-frequency")
end
def expect_issue_to_be_present def expect_issue_to_be_present
expect(find(stage_table_selector)).to have_content(issue.title) expect(find(stage_table_selector)).to have_content(issue.title)
expect(find(stage_table_selector)).to have_content(issue.author.name) expect(find(stage_table_selector)).to have_content(issue.author.name)
......
...@@ -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 findFilters = () => 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,29 @@ describe('Value stream analytics component', () => { ...@@ -123,6 +126,29 @@ describe('Value stream analytics component', () => {
expect(findStageEvents()).toEqual(selectedStageEvents); expect(findStageEvents()).toEqual(selectedStageEvents);
}); });
it('renders the filters', () => {
expect(findFilters().exists()).toBe(true);
});
it('displays the date range selector and hides the project selector', () => {
expect(findFilters().props()).toMatchObject({
hasProjectFilter: false,
hasDateRangeFilter: true,
});
});
it('passes the paths to the filter bar', () => {
expect(findFilters().props()).toEqual({
groupId,
groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
hasProjectFilter: false,
selectedProjects: [],
startDate: createdAfter,
});
});
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,21 +4,41 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -4,21 +4,41 @@ 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,
createdAfter,
createdBefore,
} 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 mockEndpoints = {
const mockEndpoints = { fullPath: mockFullPath, requestPath: mockRequestPath }; fullPath: mockFullPath,
const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' }; requestPath: mockRequestPath,
labelsPath: mockLabelsPath,
const defaultState = { ...getters, selectedValueStream }; milestonesPath: mockMilestonesPath,
groupId,
groupPath,
};
const mockSetDateActionCommit = {
payload: { createdAfter, createdBefore },
type: 'SET_DATE_RANGE',
};
const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore };
describe('Project Value Stream Analytics actions', () => { describe('Project Value Stream Analytics actions', () => {
let state; let state;
let mock; let mock;
beforeEach(() => { beforeEach(() => {
state = { ...defaultState };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -34,16 +54,17 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -34,16 +54,17 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'fetchCycleAnalyticsData' }, { type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' }, { type: 'fetchStageData' },
{ type: 'fetchStageMedians' }, { type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
{ type: 'setLoading', payload: false }, { type: 'setLoading', payload: false },
]; ];
describe.each` describe.each`
action | payload | expectedActions | expectedMutations action | payload | expectedActions | expectedMutations
${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]} ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]} ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]} ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]} ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ 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}`, () =>
...@@ -60,6 +81,12 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -60,6 +81,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 +103,9 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -76,6 +103,9 @@ describe('Project Value Stream Analytics actions', () => {
payload, payload,
); );
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints }); expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
expect(mockDispatch).toHaveBeenCalledTimes(4);
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);
...@@ -84,7 +114,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -84,7 +114,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('fetchCycleAnalyticsData', () => { describe('fetchCycleAnalyticsData', () => {
beforeEach(() => { beforeEach(() => {
state = { endpoints: mockEndpoints }; state = { ...defaultState, endpoints: mockEndpoints };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockRequestPath).reply(httpStatusCodes.OK); mock.onGet(mockRequestPath).reply(httpStatusCodes.OK);
}); });
...@@ -129,7 +159,6 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -129,7 +159,6 @@ describe('Project Value Stream Analytics actions', () => {
state = { state = {
...defaultState, ...defaultState,
endpoints: mockEndpoints, endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage, selectedStage,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -152,7 +181,6 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -152,7 +181,6 @@ describe('Project Value Stream Analytics actions', () => {
state = { state = {
...defaultState, ...defaultState,
endpoints: mockEndpoints, endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage, selectedStage,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -177,7 +205,6 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -177,7 +205,6 @@ describe('Project Value Stream Analytics actions', () => {
state = { state = {
...defaultState, ...defaultState,
endpoints: mockEndpoints, endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage, selectedStage,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
......
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants';
import * as types from '~/cycle_analytics/store/mutation_types'; import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations'; import mutations from '~/cycle_analytics/store/mutations';
import { import {
...@@ -65,15 +64,16 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -65,15 +64,16 @@ describe('Project Value Stream Analytics mutations', () => {
expect(state).toMatchObject({ [stateKey]: value }); expect(state).toMatchObject({ [stateKey]: value });
}); });
const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore };
const mockInitialPayload = { const mockInitialPayload = {
endpoints: { requestPath: mockRequestPath }, endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' }, currentGroup: { title: 'cool-group' },
id: 1337, id: 1337,
...mockSetDatePayload,
}; };
const mockInitializedObj = { const mockInitializedObj = {
endpoints: { requestPath: mockRequestPath }, endpoints: { requestPath: mockRequestPath },
createdAfter: mockCreatedAfter, ...mockSetDatePayload,
createdBefore: mockCreatedBefore,
}; };
it.each` it.each`
...@@ -89,9 +89,8 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -89,9 +89,8 @@ describe('Project Value Stream Analytics mutations', () => {
it.each` it.each`
mutation | payload | stateKey | value mutation | payload | stateKey | value
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY} ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter} ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false} ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
......
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