Commit d7143c36 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Fix - intercept 403 errors for cycle analytics

Checks errors from the new `/-/analytics` endpoints for errors
ensuring that the "no access" message is displayed if
the user does not have sufficient permissions.

Also adds additional specs to ensure we do not
render the charts if the permissions check fails.
parent a1b4efcc
...@@ -85,9 +85,15 @@ export default { ...@@ -85,9 +85,15 @@ export default {
return this.selectedGroup && !this.errorCode; return this.selectedGroup && !this.errorCode;
}, },
shouldDisplayDurationChart() { shouldDisplayDurationChart() {
return !this.isLoadingDurationChart && !this.isLoading; return this.featureFlags.hasDurationChart && !this.hasNoAccessError;
}, },
shouldDisplayTasksByTypeChart() { shouldDisplayTasksByTypeChart() {
return this.featureFlags.hasTasksByTypeChart && !this.hasNoAccessError;
},
isDurationChartLoaded() {
return !this.isLoadingDurationChart && !this.isLoading;
},
isTasksByTypeChartLoaded() {
return !this.isLoading && !this.isLoadingTasksByTypeChart; return !this.isLoading && !this.isLoadingTasksByTypeChart;
}, },
hasDateRangeSet() { hasDateRangeSet() {
...@@ -280,8 +286,8 @@ export default { ...@@ -280,8 +286,8 @@ export default {
/> />
</div> </div>
</div> </div>
<template v-if="featureFlags.hasDurationChart"> <template v-if="shouldDisplayDurationChart">
<template v-if="shouldDisplayDurationChart"> <template v-if="isDurationChartLoaded">
<div class="mt-3 d-flex"> <div class="mt-3 d-flex">
<h4 class="mt-0">{{ s__('CycleAnalytics|Days to completion') }}</h4> <h4 class="mt-0">{{ s__('CycleAnalytics|Days to completion') }}</h4>
<stage-dropdown-filter <stage-dropdown-filter
...@@ -304,9 +310,9 @@ export default { ...@@ -304,9 +310,9 @@ export default {
</template> </template>
<gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" /> <gl-loading-icon v-else-if="!isLoading" size="md" class="my-4 py-4" />
</template> </template>
<template v-if="featureFlags.hasTasksByTypeChart"> <template v-if="shouldDisplayTasksByTypeChart">
<div class="js-tasks-by-type-chart"> <div class="js-tasks-by-type-chart">
<div v-if="shouldDisplayTasksByTypeChart"> <div v-if="isTasksByTypeChartLoaded">
<tasks-by-type-chart <tasks-by-type-chart
:chart-data="tasksByTypeChartData" :chart-data="tasksByTypeChartData"
:filters="selectedTasksByTypeFilters" :filters="selectedTasksByTypeFilters"
......
...@@ -11,6 +11,14 @@ const removeError = () => { ...@@ -11,6 +11,14 @@ const removeError = () => {
hideFlash(flashEl); hideFlash(flashEl);
} }
}; };
const handleErrorOrRethrow = ({ action, error }) => {
if (error?.response?.status === httpStatus.FORBIDDEN) {
throw error;
}
action();
};
export const setFeatureFlags = ({ commit }, featureFlags) => export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags); commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group); export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
...@@ -85,7 +93,12 @@ export const fetchStageMedianValues = ({ state, dispatch, getters }) => { ...@@ -85,7 +93,12 @@ export const fetchStageMedianValues = ({ state, dispatch, getters }) => {
return Promise.all(stageIds.map(stageId => fetchStageMedian(currentGroupPath, stageId, params))) return Promise.all(stageIds.map(stageId => fetchStageMedian(currentGroupPath, stageId, params)))
.then(data => dispatch('receiveStageMedianValuesSuccess', data)) .then(data => dispatch('receiveStageMedianValuesSuccess', data))
.catch(err => dispatch('receiveStageMedianValuesError', err)); .catch(error =>
handleErrorOrRethrow({
error,
action: () => dispatch('receiveStageMedianValuesError', error),
}),
);
}; };
export const requestCycleAnalyticsData = ({ commit }) => commit(types.REQUEST_CYCLE_ANALYTICS_DATA); export const requestCycleAnalyticsData = ({ commit }) => commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
...@@ -150,7 +163,9 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => { ...@@ -150,7 +163,9 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
return Api.cycleAnalyticsSummaryData({ group_id: fullPath, created_after, created_before }) return Api.cycleAnalyticsSummaryData({ group_id: fullPath, created_after, created_before })
.then(({ data }) => dispatch('receiveSummaryDataSuccess', data)) .then(({ data }) => dispatch('receiveSummaryDataSuccess', data))
.catch(error => dispatch('receiveSummaryDataError', error)); .catch(error =>
handleErrorOrRethrow({ error, action: () => dispatch('receiveSummaryDataError', error) }),
);
}; };
export const requestGroupStagesAndEvents = ({ commit }) => export const requestGroupStagesAndEvents = ({ commit }) =>
...@@ -174,7 +189,9 @@ export const fetchGroupLabels = ({ dispatch, state }) => { ...@@ -174,7 +189,9 @@ export const fetchGroupLabels = ({ dispatch, state }) => {
return Api.groupLabels(fullPath) return Api.groupLabels(fullPath)
.then(data => dispatch('receiveGroupLabelsSuccess', data)) .then(data => dispatch('receiveGroupLabelsSuccess', data))
.catch(error => dispatch('receiveGroupLabelsError', error)); .catch(error =>
handleErrorOrRethrow({ error, action: () => dispatch('receiveGroupLabelsError', error) }),
);
}; };
export const receiveGroupStagesAndEventsError = ({ commit }, error) => { export const receiveGroupStagesAndEventsError = ({ commit }, error) => {
...@@ -209,7 +226,12 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { ...@@ -209,7 +226,12 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'), nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'),
) )
.then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data)) .then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data))
.catch(error => dispatch('receiveGroupStagesAndEventsError', error)); .catch(error =>
handleErrorOrRethrow({
error,
action: () => dispatch('receiveGroupStagesAndEventsError', error),
}),
);
}; };
export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE); export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE);
......
...@@ -14,6 +14,7 @@ import 'bootstrap'; ...@@ -14,6 +14,7 @@ import 'bootstrap';
import '~/gl_dropdown'; import '~/gl_dropdown';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue'; import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import Daterange from 'ee/analytics/shared/components/daterange.vue'; import Daterange from 'ee/analytics/shared/components/daterange.vue';
import TasksByTypeChart from 'ee/analytics/cycle_analytics/components/tasks_by_type_chart.vue';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import * as mockData from '../mock_data'; import * as mockData from '../mock_data';
...@@ -105,6 +106,10 @@ describe('Cycle Analytics component', () => { ...@@ -105,6 +106,10 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(Scatterplot).exists()).toBe(flag); expect(wrapper.find(Scatterplot).exists()).toBe(flag);
}; };
const displaysTasksByType = flag => {
expect(wrapper.find(TasksByTypeChart).exists()).toBe(flag);
};
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent(); wrapper = createComponent();
...@@ -289,11 +294,10 @@ describe('Cycle Analytics component', () => { ...@@ -289,11 +294,10 @@ describe('Cycle Analytics component', () => {
describe('the user does not have access to the group', () => { describe('the user does not have access to the group', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper.vm.$store.dispatch('setSelectedGroup', { mock.onAny().reply(403);
...mockData.group,
});
wrapper.vm.$store.state.errorCode = 403; wrapper.vm.onGroupSelect(mockData.group);
return waitForPromises();
}); });
it('renders the no access information', () => { it('renders the no access information', () => {
...@@ -322,6 +326,14 @@ describe('Cycle Analytics component', () => { ...@@ -322,6 +326,14 @@ describe('Cycle Analytics component', () => {
it('does not display the add stage button', () => { it('does not display the add stage button', () => {
expect(wrapper.find('.js-add-stage-button').exists()).toBe(false); expect(wrapper.find('.js-add-stage-button').exists()).toBe(false);
}); });
it('does not display the tasks by type chart', () => {
displaysTasksByType(false);
});
it('does not display the duration chart', () => {
displaysDurationScatterPlot(false);
});
}); });
describe('with customizableCycleAnalytics=true', () => { describe('with customizableCycleAnalytics=true', () => {
...@@ -366,6 +378,7 @@ describe('Cycle Analytics component', () => { ...@@ -366,6 +378,7 @@ describe('Cycle Analytics component', () => {
wrapper.destroy(); wrapper.destroy();
mock.restore(); mock.restore();
}); });
it('displays the tasks by type chart', () => { it('displays the tasks by type chart', () => {
expect(wrapper.find('.js-tasks-by-type-chart').exists()).toBe(true); expect(wrapper.find('.js-tasks-by-type-chart').exists()).toBe(true);
}); });
......
...@@ -60,6 +60,7 @@ describe('TasksByTypeChart', () => { ...@@ -60,6 +60,7 @@ describe('TasksByTypeChart', () => {
}, },
}); });
}); });
it('should render the no data available message', () => { it('should render the no data available message', () => {
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
......
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