summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/cycle_analytics
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/cycle_analytics
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
downloadgitlab-ce-e8d2c2579383897a1dd7f9debd359abe8ae8373d.tar.gz
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/filter_bar.vue142
-rw-r--r--app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue32
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue10
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue93
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js62
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js29
-rw-r--r--app/assets/javascripts/cycle_analytics/store/index.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js38
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js20
13 files changed, 436 insertions, 17 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
new file mode 100644
index 00000000000..5140b05e189
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
@@ -0,0 +1,142 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { __ } from '~/locale';
+import {
+ OPERATOR_IS_ONLY,
+ DEFAULT_NONE_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import {
+ prepareTokens,
+ processFilters,
+ filterToQueryObject,
+} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+
+export default {
+ name: 'FilterBar',
+ components: {
+ FilteredSearchBar,
+ UrlSync,
+ },
+ props: {
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState('filters', {
+ selectedMilestone: (state) => state.milestones.selected,
+ selectedAuthor: (state) => state.authors.selected,
+ selectedLabelList: (state) => state.labels.selectedList,
+ selectedAssigneeList: (state) => state.assignees.selectedList,
+ milestonesData: (state) => state.milestones.data,
+ labelsData: (state) => state.labels.data,
+ authorsData: (state) => state.authors.data,
+ assigneesData: (state) => state.assignees.data,
+ }),
+ tokens() {
+ return [
+ {
+ icon: 'clock',
+ title: __('Milestone'),
+ type: 'milestone',
+ token: MilestoneToken,
+ initialMilestones: this.milestonesData,
+ unique: true,
+ symbol: '%',
+ operators: OPERATOR_IS_ONLY,
+ fetchMilestones: this.fetchMilestones,
+ },
+ {
+ icon: 'labels',
+ title: __('Label'),
+ type: 'labels',
+ token: LabelToken,
+ defaultLabels: DEFAULT_NONE_ANY,
+ initialLabels: this.labelsData,
+ unique: false,
+ symbol: '~',
+ operators: OPERATOR_IS_ONLY,
+ fetchLabels: this.fetchLabels,
+ },
+ {
+ icon: 'pencil',
+ title: __('Author'),
+ type: 'author',
+ token: AuthorToken,
+ initialAuthors: this.authorsData,
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ fetchAuthors: this.fetchAuthors,
+ },
+ {
+ icon: 'user',
+ title: __('Assignees'),
+ type: 'assignees',
+ token: AuthorToken,
+ defaultAuthors: [],
+ initialAuthors: this.assigneesData,
+ unique: false,
+ operators: OPERATOR_IS_ONLY,
+ fetchAuthors: this.fetchAssignees,
+ },
+ ];
+ },
+ query() {
+ return filterToQueryObject({
+ milestone_title: this.selectedMilestone,
+ author_username: this.selectedAuthor,
+ label_name: this.selectedLabelList,
+ assignee_username: this.selectedAssigneeList,
+ });
+ },
+ },
+ methods: {
+ ...mapActions('filters', [
+ 'setFilters',
+ 'fetchMilestones',
+ 'fetchLabels',
+ 'fetchAuthors',
+ 'fetchAssignees',
+ ]),
+ initialFilterValue() {
+ return prepareTokens({
+ milestone: this.selectedMilestone,
+ author: this.selectedAuthor,
+ assignees: this.selectedAssigneeList,
+ labels: this.selectedLabelList,
+ });
+ },
+ handleFilter(filters) {
+ const { labels, milestone, author, assignees } = processFilters(filters);
+
+ this.setFilters({
+ selectedAuthor: author ? author[0] : null,
+ selectedMilestone: milestone ? milestone[0] : null,
+ selectedAssigneeList: assignees || [],
+ selectedLabelList: labels || [],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <filtered-search-bar
+ class="gl-flex-grow-1"
+ :namespace="groupPath"
+ recent-searches-storage-key="value-stream-analytics"
+ :search-input-placeholder="__('Filter results')"
+ :tokens="tokens"
+ :initial-filter-value="initialFilterValue()"
+ @onFilter="handleFilter"
+ />
+ <url-sync :query="query" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
new file mode 100644
index 00000000000..b622b0441e2
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
@@ -0,0 +1,32 @@
+<script>
+import { s__, n__, sprintf, formatNumber } from '~/locale';
+
+export default {
+ props: {
+ stageCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ formattedStageCount() {
+ if (!this.stageCount) {
+ return '-';
+ } else if (this.stageCount > 1000) {
+ return sprintf(s__('ValueStreamAnalytics|%{stageCount}+ items'), {
+ stageCount: formatNumber(1000),
+ });
+ }
+
+ return sprintf(n__('%{count} item', '%{count} items', this.stageCount), {
+ count: formatNumber(this.stageCount),
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <span>{{ formattedStageCount }}</span>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
index c1e33f73b13..47fafc3b90c 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -7,6 +7,7 @@ import {
} from '@gitlab/ui';
import Tracking from '~/tracking';
import { OVERVIEW_STAGE_ID } from '../constants';
+import FormattedStageCount from './formatted_stage_count.vue';
export default {
name: 'PathNavigation',
@@ -14,6 +15,7 @@ export default {
GlPath,
GlSkeletonLoading,
GlPopover,
+ FormattedStageCount,
},
directives: {
SafeHtml,
@@ -44,9 +46,6 @@ export default {
showPopover({ id }) {
return id && id !== OVERVIEW_STAGE_ID;
},
- hasStageCount({ stageCount = null }) {
- return stageCount !== null;
- },
onSelectStage($event) {
this.$emit('selected', $event);
this.track('click_path_navigation', {
@@ -88,10 +87,7 @@ export default {
{{ s__('ValueStreamEvent|Items in stage') }}
</div>
<div class="gl-pb-4 gl-font-weight-bold">
- <template v-if="hasStageCount(pathItem)">{{
- n__('%d item', '%d items', pathItem.stageCount)
- }}</template>
- <template v-else>-</template>
+ <formatted-stage-count :stage-count="pathItem.stageCount" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
new file mode 100644
index 00000000000..6b1e537dc77
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
@@ -0,0 +1,93 @@
+<script>
+import DateRange from '~/analytics/shared/components/daterange.vue';
+import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
+import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
+import FilterBar from './filter_bar.vue';
+
+export default {
+ name: 'ValueStreamFilters',
+ components: {
+ DateRange,
+ ProjectsDropdownFilter,
+ FilterBar,
+ },
+ props: {
+ selectedProjects: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasProjectFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ hasDateRangeFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ startDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ endDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ projectsQueryParams() {
+ return {
+ first: PROJECTS_PER_PAGE,
+ includeSubgroups: true,
+ };
+ },
+ },
+ multiProjectSelect: true,
+ maxDateRange: DATE_RANGE_LIMIT,
+};
+</script>
+<template>
+ <div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom">
+ <filter-bar
+ class="js-filter-bar filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
+ :group-path="groupPath"
+ />
+ <div
+ v-if="hasDateRangeFilter || hasProjectFilter"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
+ >
+ <projects-dropdown-filter
+ v-if="hasProjectFilter"
+ :key="groupId"
+ class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
+ :group-id="groupId"
+ :group-namespace="groupPath"
+ :query-params="projectsQueryParams"
+ :multi-select="$options.multiProjectSelect"
+ :default-projects="selectedProjects"
+ @selected="$emit('selectProject', $event)"
+ />
+ <date-range
+ v-if="hasDateRangeFilter"
+ :start-date="startDate"
+ :end-date="endDate"
+ :max-date-range="$options.maxDateRange"
+ :include-selected-date="true"
+ class="js-daterange-picker"
+ @change="$emit('setDateRange', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index 96c89049e90..97f502326e5 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -1,3 +1,4 @@
+export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 57cb220d9c9..615f96c3860 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -8,11 +8,24 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
+ const {
+ noAccessSvgPath,
+ noDataSvgPath,
+ requestPath,
+ fullPath,
+ projectId,
+ groupPath,
+ } = el.dataset;
store.dispatch('initializeVsa', {
+ projectId: parseInt(projectId, 10),
+ groupPath,
requestPath,
fullPath,
+ features: {
+ cycleAnalyticsForGroups:
+ (groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
+ },
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index faf1c37d86a..955f0c7271e 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -3,6 +3,7 @@ import {
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
+ getValueStreamStageMedian,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
};
export const fetchValueStreams = ({ commit, dispatch, state }) => {
- const { fullPath } = state;
+ const {
+ fullPath,
+ features: { cycleAnalyticsForGroups },
+ } = state;
commit(types.REQUEST_VALUE_STREAMS);
+ const stageRequests = ['setSelectedStage'];
+ if (cycleAnalyticsForGroups) {
+ stageRequests.push('fetchStageMedians');
+ }
+
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => dispatch('setSelectedStage'))
+ .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
-export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
+export const fetchCycleAnalyticsData = ({
+ state: { requestPath },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
- return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
+ return getProjectValueStreamMetrics(requestPath, legacyFilterParams)
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
@@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
});
};
-export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
+export const fetchStageData = ({
+ state: { requestPath, selectedStage },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_STAGE_DATA);
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
- params: { 'cycle_analytics[start_date]': startDate },
+ params: legacyFilterParams,
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
@@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};
+const getStageMedians = ({ stageId, vsaParams, filterParams = {} }) => {
+ return getValueStreamStageMedian({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
+ id: stageId,
+ value: data?.value || null,
+ }));
+};
+
+export const fetchStageMedians = ({
+ state: { stages },
+ getters: { requestParams: vsaParams, filterParams },
+ commit,
+}) => {
+ commit(types.REQUEST_STAGE_MEDIANS);
+ return Promise.all(
+ stages.map(({ id: stageId }) =>
+ getStageMedians({
+ vsaParams,
+ stageId,
+ filterParams,
+ }),
+ ),
+ )
+ .then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
+ .catch((error) => {
+ commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
+ createFlash({
+ message: __('There was an error fetching median data for stages'),
+ });
+ });
+};
+
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
@@ -92,6 +140,8 @@ const refetchData = (dispatch, commit) => {
.finally(() => commit(types.SET_LOADING, false));
};
+export const setFilters = ({ dispatch, commit }) => refetchData(dispatch, commit);
+
export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => {
commit(types.SET_DATE_RANGE, { startDate });
return refetchData(dispatch, commit);
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
index c60a70ef147..66971ea8a2e 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -1,3 +1,5 @@
+import dateFormat from 'dateformat';
+import { dateFormats } from '~/analytics/shared/constants';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
@@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
selectedStage,
});
};
+
+export const requestParams = (state) => {
+ const {
+ selectedStage: { id: stageId = null },
+ groupPath: groupId,
+ selectedValueStream: { id: valueStreamId },
+ } = state;
+ return { valueStreamId, groupId, stageId };
+};
+
+const dateRangeParams = ({ createdAfter, createdBefore }) => ({
+ created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
+ created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
+});
+
+export const legacyFilterParams = ({ startDate }) => {
+ return {
+ 'cycle_analytics[start_date]': startDate,
+ };
+};
+
+export const filterParams = ({ id, ...rest }) => {
+ return {
+ project_ids: [id],
+ ...dateRangeParams(rest),
+ };
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/cycle_analytics/store/index.js
index c6ca88ea492..76e3e835016 100644
--- a/app/assets/javascripts/cycle_analytics/store/index.js
+++ b/app/assets/javascripts/cycle_analytics/store/index.js
@@ -7,6 +7,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import filters from '~/vue_shared/components/filtered_search_bar/store/modules/filters';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
@@ -20,4 +21,5 @@ export default () =>
getters,
mutations,
state,
+ modules: { filters },
});
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
index 4f3d430ec9f..11ed62a4081 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
@@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_
export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
+
+export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
+export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
+export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index 0ae80116cd2..a8b7a607b66 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,11 +1,23 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { decorateData, decorateEvents, formatMedianValues } from '../utils';
+import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
+import {
+ decorateData,
+ decorateEvents,
+ formatMedianValues,
+ calculateFormattedDayInPast,
+} from '../utils';
import * as types from './mutation_types';
export default {
- [types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
+ [types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) {
state.requestPath = requestPath;
state.fullPath = fullPath;
+ state.groupPath = groupPath;
+ state.id = projectId;
+ const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ state.createdBefore = now;
+ state.createdAfter = past;
+ state.features = features;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
@@ -18,6 +30,9 @@ export default {
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
+ const { now, past } = calculateFormattedDayInPast(startDate);
+ state.createdBefore = now;
+ state.createdAfter = past;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
@@ -46,17 +61,25 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.hasError = false;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
const { summary, medians } = decorateData(data);
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = formatMedianValues(medians);
+ }
state.permissions = data.permissions;
state.summary = summary;
- state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.hasError = true;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true;
@@ -78,4 +101,13 @@ export default {
state.hasError = true;
state.selectedStageError = error;
},
+ [types.REQUEST_STAGE_MEDIANS](state) {
+ state.medians = {};
+ },
+ [types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians) {
+ state.medians = formatMedianValues(medians);
+ },
+ [types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
+ state.medians = {};
+ },
};
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
index 02f953d9517..4d61077fb99 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -1,9 +1,13 @@
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
+ features: {},
+ id: null,
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
+ createdAfter: null,
+ createdBefore: null,
stages: [],
summary: [],
analytics: [],
@@ -19,4 +23,5 @@ export default () => ({
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
+ parentPath: null,
});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 40ad7d8b2fc..a1690dd1513 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,6 +1,9 @@
+import dateFormat from 'dateformat';
import { unescape } from 'lodash';
+import { dateFormats } from '~/analytics/shared/constants';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
@@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
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)),
+ };
+};