Commit 08a35743 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Fix displaying no permissions error

Ensures we correctly handle situations where
the user does not have permission for the page.

Binds the permissions object directly to the base
component from the vuex state

Fix failing base_spec permissions tests
parent a417766f
...@@ -18,8 +18,15 @@ export const getProjectValueStreams = (projectPath) => { ...@@ -18,8 +18,15 @@ export const getProjectValueStreams = (projectPath) => {
return axios.get(url); return axios.get(url);
}; };
// TODO: handle filter params?
export const getProjectValueStreamStages = (projectPath, valueStreamId) => { export const getProjectValueStreamStages = (projectPath, valueStreamId) => {
const url = buildProjectValueStreamPath(projectPath, valueStreamId); const url = buildProjectValueStreamPath(projectPath, valueStreamId);
return axios.get(url); return axios.get(url);
}; };
// NOTE: legacy VSA request use a different path
// the `requestPath` provides a full url for th request
export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) =>
axios.get(`${requestPath}/events/${stageId}`, { params });
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
...@@ -58,6 +58,7 @@ export default { ...@@ -58,6 +58,7 @@ export default {
'stages', 'stages',
'summary', 'summary',
'startDate', 'startDate',
'permissions',
]), ]),
...mapGetters(['pathNavigationData']), ...mapGetters(['pathNavigationData']),
displayStageEvents() { displayStageEvents() {
...@@ -67,10 +68,9 @@ export default { ...@@ -67,10 +68,9 @@ export default {
displayNotEnoughData() { displayNotEnoughData() {
return this.selectedStageReady && this.isEmptyStage; return this.selectedStageReady && this.isEmptyStage;
}, },
// TODO: double check if we need to still check this ?? displayNoAccess() {
// displayNoAccess() { return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id);
// return this.selectedStageReady && !this.selectedStage.isUserAllowed; },
// },
selectedStageReady() { selectedStageReady() {
return !this.isLoadingStage && this.selectedStage; return !this.isLoadingStage && this.selectedStage;
}, },
...@@ -92,8 +92,6 @@ export default { ...@@ -92,8 +92,6 @@ export default {
]), ]),
handleDateSelect(startDate) { handleDateSelect(startDate) {
this.setDateRange({ startDate }); this.setDateRange({ startDate });
this.fetchStageData();
this.fetchCycleAnalyticsData();
}, },
isActiveStage(stage) { isActiveStage(stage) {
return stage.slug === this.selectedStage.slug; return stage.slug === this.selectedStage.slug;
...@@ -105,6 +103,10 @@ export default { ...@@ -105,6 +103,10 @@ export default {
this.isOverviewDialogDismissed = true; this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 }); Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
}, },
isUserAllowed(id) {
const { permissions } = this;
return permissions?.[id];
},
}, },
dayRangeOptions: [7, 30, 90], dayRangeOptions: [7, 30, 90],
i18n: { i18n: {
...@@ -199,29 +201,29 @@ export default { ...@@ -199,29 +201,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>
......
...@@ -8,7 +8,6 @@ Vue.use(Translate); ...@@ -8,7 +8,6 @@ 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');
console.log('el.dataset', el.dataset);
const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset; const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
store.dispatch('initializeVsa', { store.dispatch('initializeVsa', {
......
import { getProjectValueStreamStages, getProjectValueStreams } from '~/api/analytics_api'; import {
getProjectValueStreamStages,
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
} from '~/api/analytics_api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -12,7 +16,7 @@ export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { ...@@ -12,7 +16,7 @@ export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
export const fetchValueStreamStages = ({ commit, state }) => { export const fetchValueStreamStages = ({ commit, state }) => {
const { fullPath, selectedValueStream } = state; const { fullPath, selectedValueStream } = state;
commit(types.REQUEST_VALUE_STREAMS); commit(types.REQUEST_VALUE_STREAM_STAGES);
return getProjectValueStreamStages(fullPath, selectedValueStream.id) return getProjectValueStreamStages(fullPath, selectedValueStream.id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data)) .then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
...@@ -34,8 +38,6 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { ...@@ -34,8 +38,6 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
return dispatch('setSelectedValueStream', DEFAULT_VALUE_STREAM); 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 }) => { export const fetchValueStreams = ({ commit, dispatch, state }) => {
const { fullPath } = state; const { fullPath } = state;
commit(types.REQUEST_VALUE_STREAMS); commit(types.REQUEST_VALUE_STREAMS);
...@@ -55,10 +57,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => { ...@@ -55,10 +57,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => { export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA); commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
return axios return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
.get(requestPath, {
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))
.catch(() => { .catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR); commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
...@@ -71,11 +70,11 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com ...@@ -71,11 +70,11 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
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 getProjectValueStreamStageData({
return axios requestPath,
.get(`${requestPath}/events/${selectedStage.id}`, { stageId: selectedStage.id,
params: { 'cycle_analytics[start_date]': startDate }, params: { 'cycle_analytics[start_date]': startDate },
}) })
.then(({ data }) => { .then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data // when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) { if (data?.error) {
...@@ -93,10 +92,19 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select ...@@ -93,10 +92,19 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select
return dispatch('fetchStageData'); return dispatch('fetchStageData');
}; };
export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => const refetchData = (dispatch, commit) => {
commit(types.SET_LOADING, true);
return dispatch('fetchValueStreams')
.then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => commit(types.SET_LOADING, false));
};
export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => {
commit(types.SET_DATE_RANGE, { startDate }); commit(types.SET_DATE_RANGE, { startDate });
return refetchData(dispatch, commit);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
return Promise.all([dispatch('fetchCycleAnalyticsData'), dispatch('fetchValueStreams')]); return refetchData(dispatch, commit);
}; };
export const INITIALIZE_VSA = 'INITIALIZE_VSA'; export const INITIALIZE_VSA = 'INITIALIZE_VSA';
export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM'; 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';
......
...@@ -7,13 +7,14 @@ export default { ...@@ -7,13 +7,14 @@ export default {
state.requestPath = requestPath; state.requestPath = requestPath;
state.fullPath = fullPath; state.fullPath = fullPath;
}, },
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
},
[types.SET_SELECTED_VALUE_STREAM](state, selectedValueStream = {}) { [types.SET_SELECTED_VALUE_STREAM](state, selectedValueStream = {}) {
state.selectedValueStream = convertObjectPropsToCamelCase(selectedValueStream, { deep: true }); state.selectedValueStream = convertObjectPropsToCamelCase(selectedValueStream, { deep: true });
}, },
[types.SET_SELECTED_STAGE](state, stage) { [types.SET_SELECTED_STAGE](state, stage) {
state.isLoadingStage = true;
state.selectedStage = stage; state.selectedStage = stage;
state.isLoadingStage = false;
}, },
[types.SET_DATE_RANGE](state, { startDate }) { [types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate; state.startDate = startDate;
...@@ -47,8 +48,8 @@ export default { ...@@ -47,8 +48,8 @@ export default {
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;
const { summary, medians } = decorateData(data); const { summary, medians } = decorateData(data);
state.permissions = data.permissions;
state.summary = summary; state.summary = summary;
state.medians = formatMedianValues(medians); state.medians = formatMedianValues(medians);
state.hasError = false; state.hasError = false;
......
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({ export default () => ({
...@@ -19,4 +18,5 @@ export default () => ({ ...@@ -19,4 +18,5 @@ export default () => ({
isLoading: false, isLoading: false,
isLoadingStage: false, isLoadingStage: false,
isEmptyStage: false, isEmptyStage: false,
permissions: {},
}); });
...@@ -2,31 +2,9 @@ import { unescape } from 'lodash'; ...@@ -2,31 +2,9 @@ import { unescape } from 'lodash';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parseSeconds } from '~/lib/utils/datetime_utility'; import { parseSeconds } from '~/lib/utils/datetime_utility';
import { dasherize } from '~/lib/utils/text_utility'; import { s__, sprintf } from '../locale';
import { __, s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
issue: __(
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
),
plan: __(
'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
),
code: __(
'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
),
test: __(
'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
),
review: __(
'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
),
staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
),
};
/** /**
* These `decorate` methods will be removed when me migrate to the * These `decorate` methods will be removed when me migrate to the
* new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704 * new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704
...@@ -43,33 +21,12 @@ const mapToEvent = (event, stage) => { ...@@ -43,33 +21,12 @@ const mapToEvent = (event, stage) => {
export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage)); export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage));
/*
* NOTE: We currently use the `name` field since the project level stages are in memory
* once we migrate to a default value stream https://gitlab.com/gitlab-org/gitlab/-/issues/326705
* we can use the `id` to identify which median we are using
*/
const mapToStage = (permissions, { name, ...rest }) => {
const slug = dasherize(name.toLowerCase());
return {
...rest,
name,
id: name,
slug,
active: false,
isUserAllowed: permissions[slug],
emptyStageText: EMPTY_STAGE_TEXTS[slug],
component: `stage-${slug}-component`,
};
};
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' }); const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
const mapToMedians = ({ id, value }) => ({ id, value }); const mapToMedians = ({ name: id, value }) => ({ id, value });
export const decorateData = (data = {}) => { export const decorateData = (data = {}) => {
const { permissions, stats, summary } = data; const { stats: stages, summary } = data;
const stages = stats?.map((item) => mapToStage(permissions, item)) || [];
return { return {
stages,
summary: summary?.map((item) => mapToSummary(item)) || [], summary: summary?.map((item) => mapToSummary(item)) || [],
medians: stages?.map((item) => mapToMedians(item)) || [], medians: stages?.map((item) => mapToMedians(item)) || [],
}; };
......
...@@ -32970,6 +32970,9 @@ msgstr "" ...@@ -32970,6 +32970,9 @@ msgstr ""
msgid "There was an error while fetching value stream analytics duration data." msgid "There was an error while fetching value stream analytics duration data."
msgstr "" msgstr ""
msgid "There was an error while fetching value stream summary data."
msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr "" msgstr ""
......
...@@ -19,6 +19,9 @@ function createStore({ initialState = {} }) { ...@@ -19,6 +19,9 @@ function createStore({ initialState = {} }) {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
...initState(), ...initState(),
permissions: {
[selectedStage.id]: true,
},
...initialState, ...initialState,
}, },
getters: { getters: {
...@@ -155,7 +158,11 @@ describe('Value stream analytics component', () => { ...@@ -155,7 +158,11 @@ describe('Value stream analytics component', () => {
describe('without enough permissions', () => { describe('without enough permissions', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialState: { selectedStage: { ...selectedStage, isUserAllowed: false } }, initialState: {
permissions: {
[selectedStage.id]: false,
},
},
}); });
}); });
......
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