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 {
displayNotEnoughData() {
return this.selectedStageReady && this.isEmptyStage;
},
displayNoAccess() {
return this.selectedStageReady && !this.selectedStage.isUserAllowed;
},
// TODO: double check if we need to still check this ??
// displayNoAccess() {
// return this.selectedStageReady && !this.selectedStage.isUserAllowed;
// },
selectedStageReady() {
return !this.isLoadingStage && this.selectedStage;
},
......@@ -91,20 +92,14 @@ export default {
]),
handleDateSelect(startDate) {
this.setDateRange({ startDate });
this.fetchStageData();
this.fetchCycleAnalyticsData();
},
isActiveStage(stage) {
return stage.slug === this.selectedStage.slug;
},
onSelectStage(stage) {
if (this.isLoadingStage || this.selectedStage?.slug === stage?.slug) return;
this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
return;
}
this.fetchStageData();
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
......@@ -204,29 +199,29 @@ export default {
<section class="stage-events gl-overflow-auto gl-w-full">
<gl-loading-icon v-if="isLoadingStage" size="lg" />
<template v-else>
<gl-empty-state
<!--<gl-empty-state
v-if="displayNoAccess"
class="js-empty-state"
:title="__('You need permission.')"
:svg-path="noAccessSvgPath"
:description="__('Want to see the data? Please ask an administrator for access.')"
/>
<template v-else>
<gl-empty-state
v-if="displayNotEnoughData"
class="js-empty-state"
:description="emptyStageText"
:svg-path="noDataSvgPath"
:title="emptyStageTitle"
/>
<component
:is="selectedStage.component"
v-if="displayStageEvents"
:stage="selectedStage"
:items="selectedStageEvents"
data-testid="stage-table-events"
/>
</template>
<template v-else>-->
<gl-empty-state
v-if="displayNotEnoughData"
class="js-empty-state"
:description="emptyStageText"
:svg-path="noDataSvgPath"
:title="emptyStageTitle"
/>
<component
:is="selectedStage.component"
v-if="displayStageEvents"
:stage="selectedStage"
:items="selectedStageEvents"
data-testid="stage-table-events"
/>
<!--</template>-->
</template>
</section>
</div>
......
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
id: 'default',
slug: 'default',
name: 'default',
};
......@@ -8,10 +8,12 @@ Vue.use(Translate);
export default () => {
const store = createStore();
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', {
requestPath,
fullPath,
});
// eslint-disable-next-line no-new
......@@ -24,6 +26,7 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
fullPath,
},
}),
});
......
import { getProjectValueStreamStages, getProjectValueStreams } from '~/api/analytics_api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
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';
export const fetchCycleAnalyticsData = ({
state: { requestPath, startDate },
dispatch,
commit,
}) => {
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return dispatch('fetchValueStreamStages');
};
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);
return axios
......@@ -16,12 +60,10 @@ export const fetchCycleAnalyticsData = ({
params: { 'cycle_analytics[start_date]': startDate },
})
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.then(() => dispatch('setSelectedStage'))
.then(() => dispatch('fetchStageData'))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
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 = ({
export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
commit(types.REQUEST_STAGE_DATA);
// TODO: move to api
return axios
.get(`${requestPath}/events/${selectedStage.name}.json`, {
.get(`${requestPath}/events/${selectedStage.id}`, {
params: { 'cycle_analytics[start_date]': startDate },
})
.then(({ data }) => {
......@@ -44,9 +87,10 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.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];
commit(types.SET_SELECTED_STAGE, stage);
return dispatch('fetchStageData');
};
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 = {}) => {
commit(types.INITIALIZE_VSA, initialData);
return dispatch('fetchCycleAnalyticsData');
return Promise.all([dispatch('fetchCycleAnalyticsData'), dispatch('fetchValueStreams')]);
};
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_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 RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
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 * as types from './mutation_types';
export default {
[types.INITIALIZE_VSA](state, { requestPath }) {
[types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
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) {
state.isLoadingStage = true;
......@@ -13,22 +18,43 @@ export default {
[types.SET_DATE_RANGE](state, { 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) {
state.isLoading = true;
state.stages = [];
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false;
const { stages, summary, medians } = decorateData(data);
state.stages = stages;
const { summary, medians } = decorateData(data);
state.summary = summary;
state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.stages = [];
state.hasError = true;
},
[types.REQUEST_STAGE_DATA](state) {
......
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
stages: [],
summary: [],
analytics: [],
stats: [],
valueStreams: [],
selectedValueStream: {},
selectedStage: {},
selectedStageEvents: [],
selectedStageError: '',
......
- page_title _("Value Stream 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") }
- 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 }
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