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