diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/cycle_analytics | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) | |
download | gitlab-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')
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)), + }; +}; |