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) => {
return axios.get(url);
};
// TODO: handle filter params?
export const getProjectValueStreamStages = (projectPath, valueStreamId) => {
const url = buildProjectValueStreamPath(projectPath, valueStreamId);
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 {
'stages',
'summary',
'startDate',
'permissions',
]),
...mapGetters(['pathNavigationData']),
displayStageEvents() {
......@@ -67,10 +68,9 @@ export default {
displayNotEnoughData() {
return this.selectedStageReady && this.isEmptyStage;
},
// TODO: double check if we need to still check this ??
// displayNoAccess() {
// return this.selectedStageReady && !this.selectedStage.isUserAllowed;
// },
displayNoAccess() {
return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id);
},
selectedStageReady() {
return !this.isLoadingStage && this.selectedStage;
},
......@@ -92,8 +92,6 @@ export default {
]),
handleDateSelect(startDate) {
this.setDateRange({ startDate });
this.fetchStageData();
this.fetchCycleAnalyticsData();
},
isActiveStage(stage) {
return stage.slug === this.selectedStage.slug;
......@@ -105,6 +103,10 @@ export default {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
},
isUserAllowed(id) {
const { permissions } = this;
return permissions?.[id];
},
},
dayRangeOptions: [7, 30, 90],
i18n: {
......@@ -199,29 +201,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>
......
......@@ -8,7 +8,6 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
console.log('el.dataset', el.dataset);
const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
store.dispatch('initializeVsa', {
......
import { getProjectValueStreamStages, getProjectValueStreams } from '~/api/analytics_api';
import {
getProjectValueStreamStages,
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants';
import * as types from './mutation_types';
......@@ -12,7 +16,7 @@ export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
export const fetchValueStreamStages = ({ commit, state }) => {
const { fullPath, selectedValueStream } = state;
commit(types.REQUEST_VALUE_STREAMS);
commit(types.REQUEST_VALUE_STREAM_STAGES);
return getProjectValueStreamStages(fullPath, selectedValueStream.id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
......@@ -34,8 +38,6 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
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);
......@@ -55,10 +57,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
return axios
.get(requestPath, {
params: { 'cycle_analytics[start_date]': startDate },
})
return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
......@@ -71,11 +70,11 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
commit(types.REQUEST_STAGE_DATA);
// TODO: move to api
return axios
.get(`${requestPath}/events/${selectedStage.id}`, {
params: { 'cycle_analytics[start_date]': startDate },
})
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
params: { 'cycle_analytics[start_date]': startDate },
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) {
......@@ -93,10 +92,19 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select
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 });
return refetchData(dispatch, commit);
};
export const initializeVsa = ({ commit, dispatch }, 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 SET_LOADING = 'SET_LOADING';
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';
......
......@@ -7,13 +7,14 @@ export default {
state.requestPath = requestPath;
state.fullPath = fullPath;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
},
[types.SET_SELECTED_VALUE_STREAM](state, selectedValueStream = {}) {
state.selectedValueStream = convertObjectPropsToCamelCase(selectedValueStream, { deep: true });
},
[types.SET_SELECTED_STAGE](state, stage) {
state.isLoadingStage = true;
state.selectedStage = stage;
state.isLoadingStage = false;
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
......@@ -47,8 +48,8 @@ export default {
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false;
const { summary, medians } = decorateData(data);
state.permissions = data.permissions;
state.summary = summary;
state.medians = formatMedianValues(medians);
state.hasError = false;
......
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
......@@ -19,4 +18,5 @@ export default () => ({
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
});
......@@ -2,31 +2,9 @@ import { unescape } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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';
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
* new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704
......@@ -43,33 +21,12 @@ const 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 mapToMedians = ({ id, value }) => ({ id, value });
const mapToMedians = ({ name: id, value }) => ({ id, value });
export const decorateData = (data = {}) => {
const { permissions, stats, summary } = data;
const stages = stats?.map((item) => mapToStage(permissions, item)) || [];
const { stats: stages, summary } = data;
return {
stages,
summary: summary?.map((item) => mapToSummary(item)) || [],
medians: stages?.map((item) => mapToMedians(item)) || [],
};
......
......@@ -32970,6 +32970,9 @@ msgstr ""
msgid "There was an error while fetching value stream analytics duration data."
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."
msgstr ""
......
......@@ -19,6 +19,9 @@ function createStore({ initialState = {} }) {
return new Vuex.Store({
state: {
...initState(),
permissions: {
[selectedStage.id]: true,
},
...initialState,
},
getters: {
......@@ -155,7 +158,11 @@ describe('Value stream analytics component', () => {
describe('without enough permissions', () => {
beforeEach(() => {
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