Commit a417766f authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Add requests for project level value streams

Adds the api/analytics_api module to group analytics
related api requests.

Adds the value stream API requests to project
level VSA and updates the actions and mutations
parent 9e735c0b
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
if (valueStreamId) {
return buildApiUrl(PROJECT_VSA_STAGES_PATH)
.replace(':project_path', projectPath)
.replace(':value_stream_id', valueStreamId);
}
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
};
export const getProjectValueStreams = (projectPath) => {
const url = buildProjectValueStreamPath(projectPath);
return axios.get(url);
};
// TODO: handle filter params?
export const getProjectValueStreamStages = (projectPath, valueStreamId) => {
const url = buildProjectValueStreamPath(projectPath, valueStreamId);
return axios.get(url);
};
...@@ -67,9 +67,10 @@ export default { ...@@ -67,9 +67,10 @@ export default {
displayNotEnoughData() { displayNotEnoughData() {
return this.selectedStageReady && this.isEmptyStage; return this.selectedStageReady && this.isEmptyStage;
}, },
displayNoAccess() { // TODO: double check if we need to still check this ??
return this.selectedStageReady && !this.selectedStage.isUserAllowed; // displayNoAccess() {
}, // return this.selectedStageReady && !this.selectedStage.isUserAllowed;
// },
selectedStageReady() { selectedStageReady() {
return !this.isLoadingStage && this.selectedStage; return !this.isLoadingStage && this.selectedStage;
}, },
...@@ -91,20 +92,14 @@ export default { ...@@ -91,20 +92,14 @@ export default {
]), ]),
handleDateSelect(startDate) { handleDateSelect(startDate) {
this.setDateRange({ startDate }); this.setDateRange({ startDate });
this.fetchStageData();
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
}, },
isActiveStage(stage) { isActiveStage(stage) {
return stage.slug === this.selectedStage.slug; return stage.slug === this.selectedStage.slug;
}, },
onSelectStage(stage) { onSelectStage(stage) {
if (this.isLoadingStage || this.selectedStage?.slug === stage?.slug) return;
this.setSelectedStage(stage); this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
return;
}
this.fetchStageData();
}, },
dismissOverviewDialog() { dismissOverviewDialog() {
this.isOverviewDialogDismissed = true; this.isOverviewDialogDismissed = true;
...@@ -204,29 +199,29 @@ export default { ...@@ -204,29 +199,29 @@ export default {
<section class="stage-events gl-overflow-auto gl-w-full"> <section class="stage-events gl-overflow-auto gl-w-full">
<gl-loading-icon v-if="isLoadingStage" size="lg" /> <gl-loading-icon v-if="isLoadingStage" size="lg" />
<template v-else> <template v-else>
<gl-empty-state <!--<gl-empty-state
v-if="displayNoAccess" v-if="displayNoAccess"
class="js-empty-state" class="js-empty-state"
:title="__('You need permission.')" :title="__('You need permission.')"
:svg-path="noAccessSvgPath" :svg-path="noAccessSvgPath"
:description="__('Want to see the data? Please ask an administrator for access.')" :description="__('Want to see the data? Please ask an administrator for access.')"
/> />
<template v-else> <template v-else>-->
<gl-empty-state <gl-empty-state
v-if="displayNotEnoughData" v-if="displayNotEnoughData"
class="js-empty-state" class="js-empty-state"
:description="emptyStageText" :description="emptyStageText"
:svg-path="noDataSvgPath" :svg-path="noDataSvgPath"
:title="emptyStageTitle" :title="emptyStageTitle"
/> />
<component <component
:is="selectedStage.component" :is="selectedStage.component"
v-if="displayStageEvents" v-if="displayStageEvents"
:stage="selectedStage" :stage="selectedStage"
:items="selectedStageEvents" :items="selectedStageEvents"
data-testid="stage-table-events" data-testid="stage-table-events"
/> />
</template> <!--</template>-->
</template> </template>
</section> </section>
</div> </div>
......
export const DEFAULT_DAYS_TO_DISPLAY = 30; export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview'; export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
id: 'default',
slug: 'default',
name: 'default',
};
...@@ -8,10 +8,12 @@ Vue.use(Translate); ...@@ -8,10 +8,12 @@ Vue.use(Translate);
export default () => { export default () => {
const store = createStore(); const store = createStore();
const el = document.querySelector('#js-cycle-analytics'); const el = document.querySelector('#js-cycle-analytics');
const { noAccessSvgPath, noDataSvgPath, requestPath } = el.dataset; console.log('el.dataset', el.dataset);
const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
store.dispatch('initializeVsa', { store.dispatch('initializeVsa', {
requestPath, requestPath,
fullPath,
}); });
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -24,6 +26,7 @@ export default () => { ...@@ -24,6 +26,7 @@ export default () => {
props: { props: {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
fullPath,
}, },
}), }),
}); });
......
import { getProjectValueStreamStages, getProjectValueStreams } from '~/api/analytics_api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const fetchCycleAnalyticsData = ({ export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
state: { requestPath, startDate }, commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
dispatch, return dispatch('fetchValueStreamStages');
commit, };
}) => {
export const fetchValueStreamStages = ({ commit, state }) => {
const { fullPath, selectedValueStream } = state;
commit(types.REQUEST_VALUE_STREAMS);
return getProjectValueStreamStages(fullPath, selectedValueStream.id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
.catch((error) => {
const {
response: { status },
} = error;
commit(types.RECEIVE_VALUE_STREAM_STAGES_ERROR, status);
throw error;
});
};
export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
if (data.length) {
const [firstStream] = data;
return dispatch('setSelectedValueStream', firstStream);
}
return dispatch('setSelectedValueStream', DEFAULT_VALUE_STREAM);
};
// TODO: add getters for common request params
// TODO: calculate date range from that
export const fetchValueStreams = ({ commit, dispatch, state }) => {
const { fullPath } = state;
commit(types.REQUEST_VALUE_STREAMS);
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => dispatch('setSelectedStage'))
.catch((error) => {
const {
response: { status },
} = error;
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
throw error;
});
};
export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA); commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
return axios return axios
...@@ -16,12 +60,10 @@ export const fetchCycleAnalyticsData = ({ ...@@ -16,12 +60,10 @@ export const fetchCycleAnalyticsData = ({
params: { 'cycle_analytics[start_date]': startDate }, params: { 'cycle_analytics[start_date]': startDate },
}) })
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data)) .then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.then(() => dispatch('setSelectedStage'))
.then(() => dispatch('fetchStageData'))
.catch(() => { .catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR); commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
createFlash({ createFlash({
message: __('There was an error while fetching value stream analytics data.'), message: __('There was an error while fetching value stream summary data.'),
}); });
}); });
}; };
...@@ -29,8 +71,9 @@ export const fetchCycleAnalyticsData = ({ ...@@ -29,8 +71,9 @@ export const fetchCycleAnalyticsData = ({
export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => { export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
commit(types.REQUEST_STAGE_DATA); commit(types.REQUEST_STAGE_DATA);
// TODO: move to api
return axios return axios
.get(`${requestPath}/events/${selectedStage.name}.json`, { .get(`${requestPath}/events/${selectedStage.id}`, {
params: { 'cycle_analytics[start_date]': startDate }, params: { 'cycle_analytics[start_date]': startDate },
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -44,9 +87,10 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate ...@@ -44,9 +87,10 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR)); .catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
}; };
export const setSelectedStage = ({ commit, state: { stages } }, selectedStage = null) => { export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0]; const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage); commit(types.SET_SELECTED_STAGE, stage);
return dispatch('fetchStageData');
}; };
export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) =>
...@@ -54,5 +98,5 @@ export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY } ...@@ -54,5 +98,5 @@ export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
return dispatch('fetchCycleAnalyticsData'); return Promise.all([dispatch('fetchCycleAnalyticsData'), dispatch('fetchValueStreams')]);
}; };
export const INITIALIZE_VSA = 'INITIALIZE_VSA'; export const INITIALIZE_VSA = 'INITIALIZE_VSA';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE'; export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const REQUEST_VALUE_STREAMS = 'REQUEST_VALUE_STREAMS';
export const RECEIVE_VALUE_STREAMS_SUCCESS = 'RECEIVE_VALUE_STREAMS_SUCCESS';
export const RECEIVE_VALUE_STREAMS_ERROR = 'RECEIVE_VALUE_STREAMS_ERROR';
export const REQUEST_VALUE_STREAM_STAGES = 'REQUEST_VALUE_STREAM_STAGES';
export const RECEIVE_VALUE_STREAM_STAGES_SUCCESS = 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS';
export const RECEIVE_VALUE_STREAM_STAGES_ERROR = 'RECEIVE_VALUE_STREAM_STAGES_ERROR';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA'; 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_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR'; export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR';
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { decorateData, decorateEvents, formatMedianValues } from '../utils'; import { decorateData, decorateEvents, formatMedianValues } from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.INITIALIZE_VSA](state, { requestPath }) { [types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
state.requestPath = requestPath; state.requestPath = requestPath;
state.fullPath = fullPath;
},
[types.SET_SELECTED_VALUE_STREAM](state, selectedValueStream = {}) {
state.selectedValueStream = convertObjectPropsToCamelCase(selectedValueStream, { deep: true });
}, },
[types.SET_SELECTED_STAGE](state, stage) { [types.SET_SELECTED_STAGE](state, stage) {
state.isLoadingStage = true; state.isLoadingStage = true;
...@@ -13,22 +18,43 @@ export default { ...@@ -13,22 +18,43 @@ export default {
[types.SET_DATE_RANGE](state, { startDate }) { [types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate; state.startDate = startDate;
}, },
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
},
[types.RECEIVE_VALUE_STREAMS_SUCCESS](state, valueStreams = []) {
state.valueStreams = valueStreams;
},
[types.RECEIVE_VALUE_STREAMS_ERROR](state) {
state.valueStreams = [];
},
[types.REQUEST_VALUE_STREAM_STAGES](state) {
state.stages = [];
},
[types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) {
state.stages = stages.map((s) => ({
...convertObjectPropsToCamelCase(s, { deep: true }),
// NOTE: we set the component type here to match the current behaviour
// this can be removed when we migrate to the update stage table
// https://gitlab.com/gitlab-org/gitlab/-/issues/326704
component: `stage-${s.id}-component`,
}));
},
[types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) {
state.stages = [];
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) { [types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true; state.isLoading = true;
state.stages = [];
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) { [types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false; state.isLoading = false;
const { stages, summary, medians } = decorateData(data); const { summary, medians } = decorateData(data);
state.stages = stages;
state.summary = summary; state.summary = summary;
state.medians = formatMedianValues(medians); state.medians = formatMedianValues(medians);
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) { [types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false; state.isLoading = false;
state.stages = [];
state.hasError = true; state.hasError = true;
}, },
[types.REQUEST_STAGE_DATA](state) { [types.REQUEST_STAGE_DATA](state) {
......
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({ export default () => ({
requestPath: '', requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY, startDate: DEFAULT_DAYS_TO_DISPLAY,
stages: [], stages: [],
summary: [], summary: [],
analytics: [], analytics: [],
stats: [], stats: [],
valueStreams: [],
selectedValueStream: {},
selectedStage: {}, selectedStage: {},
selectedStageEvents: [], selectedStageEvents: [],
selectedStageError: '', selectedStageError: '',
......
- 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 = { request_path: project_cycle_analytics_path(@project) }.merge!(svgs) - initial_data = { request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
#js-cycle-analytics{ data: initial_data } #js-cycle-analytics{ data: initial_data }
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