Commit 5ff67e3a authored by Brandon Labuschagne's avatar Brandon Labuschagne

Replace CA date range with date picker

This commit removes the temporary date range selector
and implements the new date range picker component
parent a403cea8
<script>
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlDaterangePicker } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PROJECTS_PER_PAGE } from '../constants';
import { PROJECTS_PER_PAGE, DEFAULT_DAYS_IN_PAST } from '../constants';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
import DateRangeDropdown from '../../shared/components/date_range_dropdown.vue';
import SummaryTable from './summary_table.vue';
import StageTable from './stage_table.vue';
......@@ -16,9 +16,9 @@ export default {
GlEmptyState,
GroupsDropdownFilter,
ProjectsDropdownFilter,
DateRangeDropdown,
SummaryTable,
StageTable,
GlDaterangePicker,
},
mixins: [glFeatureFlagsMixin()],
props: {
......@@ -61,11 +61,12 @@ export default {
'selectedStageName',
'stages',
'summary',
'dataTimeframe',
'labels',
'currentStageEvents',
'customStageFormEvents',
'errorCode',
'startDate',
'endDate',
]),
...mapGetters(['currentStage', 'defaultStage', 'hasNoAccessError', 'currentGroupPath']),
shouldRenderEmptyState() {
......@@ -77,6 +78,17 @@ export default {
shouldDisplayFilters() {
return this.selectedGroup && !this.errorCode;
},
dateRange: {
get() {
return { startDate: this.startDate, endDate: this.endDate };
},
set({ startDate, endDate }) {
this.setDateRange({ startDate, endDate });
},
},
},
mounted() {
this.initDateRange();
},
methods: {
...mapActions([
......@@ -88,8 +100,10 @@ export default {
'setSelectedGroup',
'setSelectedProjects',
'setSelectedTimeframe',
'fetchStageData',
'setSelectedStageName',
'hideCustomStageForm',
'setDateRange',
]),
onGroupSelect(group) {
this.setCycleAnalyticsDataEndpoint(group.path);
......@@ -101,10 +115,6 @@ export default {
this.setSelectedProjects(projectIds);
this.fetchCycleAnalyticsData();
},
onTimeframeSelect(days) {
this.setSelectedTimeframe(days);
this.fetchCycleAnalyticsData();
},
onStageSelect(stage) {
this.hideCustomStageForm();
this.setSelectedStageName(stage.name);
......@@ -114,6 +124,11 @@ export default {
onShowAddStageForm() {
this.fetchCustomStageFormData(this.currentGroupPath);
},
initDateRange() {
const endDate = new Date(Date.now());
const startDate = new Date(getDateInPast(endDate, DEFAULT_DAYS_IN_PAST));
this.setDateRange({ skipFetch: true, startDate, endDate });
},
},
};
</script>
......@@ -145,12 +160,14 @@ export default {
v-if="shouldDisplayFilters"
class="ml-0 ml-md-auto mt-2 mt-md-0 d-flex flex-column flex-md-row align-items-md-center justify-content-md-end"
>
<label class="text-bold mb-0 mr-1">{{ __('Timeframe') }}</label>
<date-range-dropdown
class="js-timeframe-filter"
:available-days-in-past="dateOptions"
:default-selected="dataTimeframe"
@selected="onTimeframeSelect"
<gl-daterange-picker
v-model="dateRange"
class="d-flex flex-column flex-lg-row js-daterange-picker"
:default-start-date="startDate"
:default-end-date="endDate"
start-picker-class="d-flex flex-column flex-lg-row align-items-lg-center mr-lg-2"
end-picker-class="d-flex flex-column flex-lg-row align-items-lg-center"
theme="animate-picker"
/>
</div>
</div>
......
......@@ -2,7 +2,7 @@ import { __ } from '~/locale';
export const PROJECTS_PER_PAGE = 50;
export const DEFAULT_DATA_TIME_FRAME = 30;
export const DEFAULT_DAYS_IN_PAST = 30;
export const EVENTS_LIST_ITEM_LIMIT = 50;
......
import dateFormat from 'dateformat';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import Api from '~/api';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants';
export const setCycleAnalyticsDataEndpoint = ({ commit }, groupPath) =>
commit(types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT, groupPath);
......@@ -13,11 +15,20 @@ export const setStageDataEndpoint = ({ commit }, stageSlug) =>
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
export const setSelectedProjects = ({ commit }, projectIds) =>
commit(types.SET_SELECTED_PROJECTS, projectIds);
export const setSelectedTimeframe = ({ commit }, timeframe) =>
commit(types.SET_SELECTED_TIMEFRAME, timeframe);
export const setSelectedStageName = ({ commit }, stageName) =>
commit(types.SET_SELECTED_STAGE_NAME, stageName);
export const setDateRange = (
{ commit, dispatch, state },
{ skipFetch = false, startDate, endDate },
) => {
commit(types.SET_DATE_RANGE, { startDate, endDate });
if (skipFetch) return false;
return dispatch('fetchCycleAnalyticsData', { state, dispatch });
};
export const requestStageData = ({ commit }) => commit(types.REQUEST_STAGE_DATA);
export const receiveStageDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_STAGE_DATA_SUCCESS, data);
......@@ -33,7 +44,8 @@ export const fetchStageData = ({ state, dispatch }) => {
axios
.get(state.endpoints.stageData, {
params: {
'cycle_analytics[start_date]': state.dataTimeframe,
'cycle_analytics[created_after]': dateFormat(state.startDate, dateFormats.isoDate),
'cycle_analytics[created_before]': dateFormat(state.endDate, dateFormats.isoDate),
'cycle_analytics[project_ids]': state.selectedProjectIds,
},
})
......@@ -68,7 +80,8 @@ export const fetchCycleAnalyticsData = ({ state, dispatch }) => {
axios
.get(state.endpoints.cycleAnalyticsData, {
params: {
'cycle_analytics[start_date]': state.dataTimeframe,
'cycle_analytics[created_after]': dateFormat(state.startDate, dateFormats.isoDate),
'cycle_analytics[created_before]': dateFormat(state.endDate, dateFormats.isoDate),
'cycle_analytics[project_ids]': state.selectedProjectIds,
},
})
......
......@@ -3,9 +3,10 @@ export const SET_STAGE_DATA_ENDPOINT = 'SET_STAGE_DATA_ENDPOINT';
export const SET_SELECTED_GROUP = 'SET_SELECTED_GROUP';
export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_TIMEFRAME = 'SET_SELECTED_TIMEFRAME';
export const SET_SELECTED_STAGE_NAME = 'SET_SELECTED_STAGE_NAME';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
export const RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR';
......
......@@ -17,12 +17,13 @@ export default {
[types.SET_SELECTED_PROJECTS](state, projectIds) {
state.selectedProjectIds = projectIds;
},
[types.SET_SELECTED_TIMEFRAME](state, timeframe) {
state.dataTimeframe = timeframe;
},
[types.SET_SELECTED_STAGE_NAME](state, stageName) {
state.selectedStageName = stageName;
},
[types.SET_DATE_RANGE](state, { startDate, endDate }) {
state.startDate = startDate;
state.endDate = endDate;
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.isAddingCustomStage = false;
......
import { DEFAULT_DATA_TIME_FRAME } from '../constants';
export default () => ({
endpoints: {
cycleAnalyticsData: '',
stageData: '',
cycleAnalyticsData: null,
stageData: null,
},
dataTimeframe: DEFAULT_DATA_TIME_FRAME,
startDate: null,
endDate: null,
isLoading: false,
isLoadingStage: false,
......
.gl-daterange-picker {
.gl-datepicker-input {
width: 140px;
@include media-breakpoint-down(md) {
width: 100%;
}
}
label {
@include media-breakpoint-up(lg) {
margin-bottom: 0;
margin-right: $gl-padding-4;
}
}
}
......@@ -47,7 +47,7 @@ describe 'Group Cycle Analytics', :js do
end
it 'shows the date filter' do
expect(page).to have_selector('.js-timeframe-filter', visible: true)
expect(page).to have_selector('.js-daterange-picker', visible: true)
end
end
......
......@@ -3,12 +3,11 @@ import Vuex from 'vuex';
import Vue from 'vue';
import store from 'ee/analytics/cycle_analytics/store';
import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlDaterangePicker } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import GroupsDropdownFilter from 'ee/analytics/shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue';
import DateRangeDropdown from 'ee/analytics/shared/components/date_range_dropdown.vue';
import SummaryTable from 'ee/analytics/cycle_analytics/components/summary_table.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import 'bootstrap';
......@@ -66,8 +65,8 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(flag);
};
const displaysDateRangeDropdown = flag => {
expect(wrapper.find(DateRangeDropdown).exists()).toBe(flag);
const displaysDateRangePicker = flag => {
expect(wrapper.find(GlDaterangePicker).exists()).toBe(flag);
};
const displaysSummaryTable = flag => {
......@@ -88,6 +87,27 @@ describe('Cycle Analytics component', () => {
mock.restore();
});
describe('mounted', () => {
const actionSpies = {
setDateRange: jest.fn(),
};
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(mockData.endDate));
wrapper = createComponent({ opts: { methods: actionSpies } });
});
describe('initDateRange', () => {
it('dispatches setDateRange with skipFetch=true', () => {
expect(actionSpies.setDateRange).toHaveBeenCalledWith({
skipFetch: true,
startDate: mockData.startDate,
endDate: mockData.endDate,
});
});
});
});
describe('displays the components as required', () => {
describe('before a filter has been selected', () => {
it('displays an empty state', () => {
......@@ -108,8 +128,8 @@ describe('Cycle Analytics component', () => {
displaysProjectsDropdownFilter(false);
});
it('does not display the date range dropdown', () => {
displaysDateRangeDropdown(false);
it('does not display the date range picker', () => {
displaysDateRangePicker(false);
});
it('does not display the summary table', () => {
......@@ -143,8 +163,8 @@ describe('Cycle Analytics component', () => {
);
});
it('displays the date range dropdown', () => {
displaysDateRangeDropdown(true);
it('displays the date range picker', () => {
displaysDateRangePicker(true);
});
it('displays the summary table', () => {
......@@ -224,8 +244,8 @@ describe('Cycle Analytics component', () => {
displaysProjectsDropdownFilter(false);
});
it('does not display the date range dropdown', () => {
displaysDateRangeDropdown(false);
it('does not display the date range picker', () => {
displaysDateRangePicker(false);
});
it('does not display the summary table', () => {
......
......@@ -3,6 +3,8 @@ import { getJSONFixture } from 'helpers/fixtures';
import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { DEFAULT_DAYS_IN_PAST } from 'ee/analytics/cycle_analytics/constants';
import { mockLabels } from '../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data';
export const groupLabels = mockLabels.map(({ title, ...rest }) => ({ ...rest, name: title }));
......@@ -47,6 +49,9 @@ const stageFixtures = defaultStages.reduce((acc, stage) => {
};
}, {});
export const endDate = new Date(Date.now());
export const startDate = new Date(getDateInPast(endDate, DEFAULT_DAYS_IN_PAST));
export const issueEvents = stageFixtures.issue;
export const planEvents = stageFixtures.plan;
export const reviewEvents = stageFixtures.review;
......
......@@ -4,7 +4,14 @@ import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { group, cycleAnalyticsData, allowedStages as stages, groupLabels } from '../mock_data';
import {
group,
cycleAnalyticsData,
allowedStages as stages,
groupLabels,
startDate,
endDate,
} from '../mock_data';
const stageData = { events: [] };
const error = new Error('Request failed with status code 404');
......@@ -41,7 +48,6 @@ describe('Cycle analytics actions', () => {
${'setSelectedGroup'} | ${'SET_SELECTED_GROUP'} | ${'selectedGroup'} | ${'someNewGroup'}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStageName'} | ${'SET_SELECTED_STAGE_NAME'} | ${'selectedStageName'} | ${'someNewGroup'}
${'setSelectedTimeframe'} | ${'SET_SELECTED_TIMEFRAME'} | ${'dataTimeframe'} | ${20}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
testAction(
actions[action],
......@@ -57,6 +63,21 @@ describe('Cycle analytics actions', () => {
);
});
describe('setDateRange', () => {
it('sets the dates as expected and dispatches fetchCycleAnalyticsData', done => {
const dispatch = expect.any(Function);
testAction(
actions.setDateRange,
{ startDate, endDate },
state,
[{ type: types.SET_DATE_RANGE, payload: { startDate, endDate } }],
[{ type: 'fetchCycleAnalyticsData', payload: { dispatch, state } }],
done,
);
});
});
describe('fetchStageData', () => {
beforeEach(() => {
mock.onGet(state.endpoints.stageData).replyOnce(200, { events: [] });
......
......@@ -13,6 +13,8 @@ import {
reviewStage,
productionStage,
groupLabels,
startDate,
endDate,
} from '../mock_data';
let state = null;
......@@ -45,13 +47,13 @@ describe('Cycle analytics mutations', () => {
});
it.each`
mutation | payload | expectedState
${types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT} | ${'cool-beans'} | ${{ endpoints: { cycleAnalyticsData: '/groups/cool-beans/-/cycle_analytics' } }}
${types.SET_STAGE_DATA_ENDPOINT} | ${'rad-stage'} | ${{ endpoints: { stageData: '/fake/api/events/rad-stage.json' } }}
${types.SET_SELECTED_GROUP} | ${'cool-beans'} | ${{ selectedGroup: 'cool-beans', selectedProjectIds: [] }}
${types.SET_SELECTED_PROJECTS} | ${[606, 707, 808, 909]} | ${{ selectedProjectIds: [606, 707, 808, 909] }}
${types.SET_SELECTED_TIMEFRAME} | ${60} | ${{ dataTimeframe: 60 }}
${types.SET_SELECTED_STAGE_NAME} | ${'first-stage'} | ${{ selectedStageName: 'first-stage' }}
mutation | payload | expectedState
${types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT} | ${'cool-beans'} | ${{ endpoints: { cycleAnalyticsData: '/groups/cool-beans/-/cycle_analytics' } }}
${types.SET_STAGE_DATA_ENDPOINT} | ${'rad-stage'} | ${{ endpoints: { stageData: '/fake/api/events/rad-stage.json' } }}
${types.SET_SELECTED_GROUP} | ${'cool-beans'} | ${{ selectedGroup: 'cool-beans', selectedProjectIds: [] }}
${types.SET_SELECTED_PROJECTS} | ${[606, 707, 808, 909]} | ${{ selectedProjectIds: [606, 707, 808, 909] }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE_NAME} | ${'first-stage'} | ${{ selectedStageName: 'first-stage' }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......
......@@ -16623,9 +16623,6 @@ msgstr ""
msgid "Timeago|right now"
msgstr ""
msgid "Timeframe"
msgstr ""
msgid "Timeout"
msgstr ""
......
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