Commit 95f53a35 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Replace legacy date selector with range date

Updates the vuex store to remove the legacy
'daysInPast' state key that was used to track
the number of days to search across.

With the newer project VSA endpoints we can safely
filter only using `createdBefore` and `createdAfter`
parameters.

Minor refactor utils and fix store specs

Updates the vuex store action and mutation
specs to remove the daysInPast state key.
parent 89c07866
import dateFormat from 'dateformat';
import { dateFormats } from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data;
return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
};
export const toYmd = (date) => dateFormat(date, dateFormats.isoDate);
<script>
import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
......@@ -14,9 +14,7 @@ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
export default {
name: 'CycleAnalytics',
components: {
GlIcon,
GlLoadingIcon,
GlSprintf,
PathNavigation,
StageTable,
ValueStreamFilters,
......@@ -47,7 +45,6 @@ export default {
'selectedStageError',
'stages',
'summary',
'daysInPast',
'permissions',
'stageCounts',
'endpoints',
......@@ -103,8 +100,11 @@ export default {
},
methods: {
...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
handleDateSelect(daysInPast) {
this.setDateRange(daysInPast);
onSetDateRange({ startDate, endDate }) {
this.setDateRange({
createdAfter: new Date(startDate),
createdBefore: new Date(endDate),
});
},
onSelectStage(stage) {
this.setSelectedStage(stage);
......@@ -138,35 +138,14 @@ export default {
:selected-stage="selectedStage"
@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>
<value-stream-filters
class="gl-w-full"
:group-id="endpoints.groupId"
:group-path="endpoints.groupPath"
:has-project-filter="false"
:has-date-range-filter="false"
:start-date="createdAfter"
:end-date="createdBefore"
@setDateRange="onSetDateRange"
/>
<value-stream-metrics
:request-path="endpoints.fullPath"
......
......@@ -68,26 +68,30 @@ export default {
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>
<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)"
/>
</div>
<div>
<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>
</div>
</template>
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
import { calculateFormattedDayInPast } from './utils';
Vue.use(Translate);
......@@ -20,6 +22,9 @@ export default () => {
milestonesPath,
} = el.dataset;
// TODO: should we pass these as params from the ~backend, like group level
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10),
endpoints: {
......@@ -33,6 +38,8 @@ export default () => {
features: {
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
},
createdBefore: new Date(now),
createdAfter: new Date(past),
});
// eslint-disable-next-line no-new
......
......@@ -171,8 +171,8 @@ const refetchStageData = (dispatch) => {
export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
export const setDateRange = ({ dispatch, commit }, daysInPast) => {
commit(types.SET_DATE_RANGE, daysInPast);
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
return refetchStageData(dispatch);
};
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
import { formatMedianValues, calculateFormattedDayInPast } from '../utils';
import { formatMedianValues } from '../utils';
import * as types from './mutation_types';
export default {
[types.INITIALIZE_VSA](state, { endpoints, features }) {
[types.INITIALIZE_VSA](state, { endpoints, features, createdBefore, createdAfter }) {
state.endpoints = endpoints;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
state.createdBefore = now;
state.createdAfter = past;
state.createdBefore = createdBefore;
state.createdAfter = createdAfter;
state.features = features;
},
[types.SET_LOADING](state, loadingState) {
......@@ -20,11 +18,9 @@ export default {
[types.SET_SELECTED_STAGE](state, stage) {
state.selectedStage = stage;
},
[types.SET_DATE_RANGE](state, daysInPast) {
state.daysInPast = daysInPast;
const { now, past } = calculateFormattedDayInPast(daysInPast);
state.createdBefore = now;
state.createdAfter = past;
[types.SET_DATE_RANGE](state, { createdAfter, createdBefore }) {
state.createdBefore = createdBefore;
state.createdAfter = createdAfter;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
......
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
id: null,
features: {},
endpoints: {},
daysInPast: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null,
createdBefore: null,
stages: [],
......
......@@ -150,5 +150,4 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
};
});
// TODO: maybe use paths.join
export const appendExtension = (path) => (path.indexOf('.') > -1 ? path : `${path}.json`);
<script>
import { GlEmptyState } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.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 { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { toYmd } from '../../shared/utils';
import { METRICS_REQUESTS } from '../constants';
import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue';
......
import Api from 'ee/api';
import { removeFlash, appendExtension } from '~/cycle_analytics/utils';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
......
import dateFormat from 'dateformat';
import { isNumber, uniqBy } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
import createFlash from '~/flash';
......@@ -8,7 +9,6 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, dayAfter, secondsToDays, getDatesInRange } from '~/lib/utils/datetime_utility';
import httpStatus from '~/lib/utils/http_status';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { toYmd } from '../shared/utils';
const EVENT_TYPE_LABEL = 'label';
......
......@@ -3,12 +3,6 @@ import { dateFormats } from '~/analytics/shared/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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);
/**
......
......@@ -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 ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import createStore from 'ee/analytics/cycle_analytics/store';
import { toYmd } from 'ee/analytics/shared/utils';
import waitForPromises from 'helpers/wait_for_promises';
import {
currentGroup,
......@@ -16,6 +15,7 @@ import {
createdAfter,
selectedProjects,
} from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
......
......@@ -9,7 +9,6 @@ import {
getTasksByTypeData,
transformRawTasksByTypeData,
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { getJSONFixture } from 'helpers/fixtures';
import {
getStageByTitle,
......@@ -19,6 +18,7 @@ import {
createdAfter,
deepCamelCase,
} from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils';
import {
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
......
......@@ -17,8 +17,8 @@ import {
prepareStageErrors,
formatMedianValuesWithOverview,
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
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 { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
......
......@@ -130,10 +130,10 @@ describe('Value stream analytics component', () => {
expect(findFilterBar().exists()).toBe(true);
});
it('hides the project selector and range selector', () => {
it('displays the date range selector and hides the project selector', () => {
expect(findFilterBar().props()).toMatchObject({
hasProjectFilter: false,
hasDateRangeFilter: false,
hasDateRangeFilter: true,
});
});
......
......@@ -4,14 +4,20 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import * as getters from '~/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status';
import { allowedStages, selectedStage, selectedValueStream, currentGroup } 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 mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockStartDate = 30;
const mockEndpoints = {
fullPath: mockFullPath,
requestPath: mockRequestPath,
......@@ -20,15 +26,19 @@ const mockEndpoints = {
groupId,
groupPath,
};
const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
const mockSetDateActionCommit = {
payload: { createdAfter, createdBefore },
type: 'SET_DATE_RANGE',
};
const defaultState = { ...getters, selectedValueStream };
const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore };
describe('Project Value Stream Analytics actions', () => {
let state;
let mock;
beforeEach(() => {
state = { ...defaultState };
mock = new MockAdapter(axios);
});
......@@ -48,12 +58,12 @@ describe('Project Value Stream Analytics actions', () => {
];
describe.each`
action | payload | expectedActions | expectedMutations
${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
action | payload | expectedActions | expectedMutations
${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
......@@ -101,7 +111,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('fetchCycleAnalyticsData', () => {
beforeEach(() => {
state = { endpoints: mockEndpoints };
state = { ...defaultState, endpoints: mockEndpoints };
mock = new MockAdapter(axios);
mock.onGet(mockRequestPath).reply(httpStatusCodes.OK);
});
......@@ -146,7 +156,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
......@@ -169,7 +178,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
......@@ -194,7 +202,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
......
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 mutations from '~/cycle_analytics/store/mutations';
import {
......@@ -65,15 +64,16 @@ describe('Project Value Stream Analytics mutations', () => {
expect(state).toMatchObject({ [stateKey]: value });
});
const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore };
const mockInitialPayload = {
endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' },
id: 1337,
...mockSetDatePayload,
};
const mockInitializedObj = {
endpoints: { requestPath: mockRequestPath },
createdAfter: mockCreatedAfter,
createdBefore: mockCreatedBefore,
...mockSetDatePayload,
};
it.each`
......@@ -89,9 +89,8 @@ describe('Project Value Stream Analytics mutations', () => {
it.each`
mutation | payload | stateKey | value
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${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