Commit 903a6d80 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Kushal Pandya

Add error handling for access restrictions

Only groups on a silver / premium tier or above, where the user has
reporter access or greater are able to have access to cycle analytics.

This change presents the user with a relevant error message.
parent af35e9ff
......@@ -55,7 +55,7 @@ export default {
'summary',
'dataTimeframe',
]),
...mapGetters(['currentStage', 'defaultStage']),
...mapGetters(['currentStage', 'defaultStage', 'hasNoAccessError']),
shouldRenderEmptyState() {
return !this.selectedGroup;
},
......@@ -150,22 +150,35 @@ export default {
:svg-path="emptyStateSvgPath"
/>
<div v-else class="cycle-analytics mt-0">
<summary-table class="js-summary-table" :items="summary" />
<stage-table
v-if="currentStage"
class="js-stage-table"
:current-stage="currentStage"
:stages="stages"
:is-loading-stage="isLoadingStage"
:is-empty-stage="isEmptyStage"
:is-adding-custom-stage="isAddingCustomStage"
:events="events"
:no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics"
@selectStage="onStageSelect"
@showAddStageForm="onShowAddStageForm"
<gl-empty-state
v-if="hasNoAccessError"
class="js-empty-state"
:title="__('You don’t have access to Cycle Analytics for this group')"
:svg-path="noAccessSvgPath"
:description="
__(
'Only \'Reporter\' roles and above on tiers Premium / Silver and above can see Cycle Analytics.',
)
"
/>
<div v-else class="cycle-analytics mt-0">
<summary-table class="js-summary-table" :items="summary" />
<stage-table
v-if="currentStage"
class="js-stage-table"
:current-stage="currentStage"
:stages="stages"
:is-loading-stage="isLoadingStage"
:is-empty-stage="isEmptyStage"
:is-adding-custom-stage="isAddingCustomStage"
:events="events"
:no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics"
@selectStage="onStageSelect"
@showAddStageForm="onShowAddStageForm"
/>
</div>
</div>
</div>
</template>
......@@ -50,8 +50,9 @@ export const receiveCycleAnalyticsDataSuccess = ({ state, commit, dispatch }, da
createFlash(__('There was an error while fetching cycle analytics data.'));
}
};
export const receiveCycleAnalyticsDataError = ({ commit }) => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => {
const { status } = response;
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR, status);
createFlash(__('There was an error while fetching cycle analytics data.'));
};
......
import httpStatus from '~/lib/utils/http_status';
export const currentStage = ({ stages, selectedStageName }) =>
stages.length && selectedStageName
? stages.find(stage => stage.name === selectedStageName)
: null;
export const defaultStage = state => (state.stages.length ? state.stages[0] : null);
export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN;
......@@ -47,9 +47,12 @@ export default {
const { name } = state.stages[0];
state.selectedStageName = name;
}
state.errorCode = null;
state.isLoading = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state, errCode) {
state.errorCode = errCode;
state.isLoading = false;
},
[types.REQUEST_STAGE_DATA](state) {
......
......@@ -12,6 +12,7 @@ export default () => ({
isLoadingStage: false,
isEmptyStage: false,
errorCode: null,
isAddingCustomStage: false,
......
......@@ -10,18 +10,18 @@ import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_drop
import DateRangeDropdown from 'ee/analytics/shared/components/date_range_dropdown.vue';
import SummaryTable from 'ee/analytics/cycle_analytics/components/summary_table.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import { TEST_HOST } from 'helpers/test_constants';
import 'bootstrap';
import '~/gl_dropdown';
import * as mockData from '../mock_data';
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
const emptyStateSvgPath = 'path/to/empty/state';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Cycle Analytics component', () => {
const emptyStateSvgPath = `${TEST_HOST}/images/home/nasa.svg`;
let wrapper;
let mock;
......@@ -47,7 +47,10 @@ describe('Cycle Analytics component', () => {
describe('displays the components as required', () => {
describe('before a filter has been selected', () => {
it('displays an empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
const emptyState = wrapper.find(GlEmptyState);
expect(emptyState.exists()).toBe(true);
expect(emptyState.props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays the groups filter', () => {
......@@ -61,35 +64,54 @@ describe('Cycle Analytics component', () => {
});
describe('after a filter has been selected', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...mockData.group,
describe('the user has access to the group', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...mockData.group,
});
wrapper.vm.$store.dispatch('receiveCycleAnalyticsDataSuccess', {
...mockData.cycleAnalyticsData,
});
wrapper.vm.$store.dispatch('receiveStageDataSuccess', {
events: mockData.issueEvents,
});
});
wrapper.vm.$store.dispatch('receiveCycleAnalyticsDataSuccess', {
...mockData.cycleAnalyticsData,
it('hides the empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
wrapper.vm.$store.dispatch('receiveStageDataSuccess', {
events: mockData.issueEvents,
it('displays the projects and timeframe filters', () => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(true);
expect(wrapper.find(DateRangeDropdown).exists()).toBe(true);
});
});
it('hides the empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
it('displays summary table', () => {
expect(wrapper.find(SummaryTable).exists()).toBe(true);
});
it('displays the projects and timeframe filters', () => {
expect(wrapper.find(ProjectsDropdownFilter).exists()).toBe(true);
expect(wrapper.find(DateRangeDropdown).exists()).toBe(true);
it('displays the stage table', () => {
expect(wrapper.find(StageTable).exists()).toBe(true);
});
});
it('displays summary table', () => {
expect(wrapper.find(SummaryTable).exists()).toBe(true);
});
describe('the user does not have access to the group', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...mockData.group,
});
wrapper.vm.$store.state.errorCode = 403;
});
it('renders the no access information', () => {
const emptyState = wrapper.find(GlEmptyState);
it('displays the stage table', () => {
expect(wrapper.find(StageTable).exists()).toBe(true);
expect(emptyState.exists()).toBe(true);
expect(emptyState.props('svgPath')).toBe(noAccessSvgPath);
});
});
});
});
......
......@@ -271,7 +271,7 @@ describe('Cycle analytics actions', () => {
it(`commits the ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} mutation`, done => {
testAction(
actions.receiveCycleAnalyticsDataError,
null,
{ response: 403 },
state,
[
{
......@@ -288,7 +288,7 @@ describe('Cycle analytics actions', () => {
{
commit: () => {},
},
{},
{ response: 403 },
);
shouldFlashAnError();
......
......@@ -72,4 +72,21 @@ describe('Cycle analytics getters', () => {
});
});
});
describe('hasNoAccessError', () => {
beforeEach(() => {
state = {
errorCode: null,
};
});
it('returns true if "hasError" is set to 403', () => {
state.errorCode = 403;
expect(getters.hasNoAccessError(state)).toEqual(true);
});
it('returns false if "hasError" is not set to 403', () => {
expect(getters.hasNoAccessError(state)).toEqual(false);
});
});
});
......@@ -14,14 +14,13 @@ import {
describe('Cycle analytics mutations', () => {
it.each`
mutation | stateKey | value
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'isLoading'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${true}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${false}
mutation | stateKey | value
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${true}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${false}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
const state = {};
mutations[mutation](state);
......@@ -60,7 +59,7 @@ describe('Cycle analytics mutations', () => {
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false', () => {
it('will set isLoading=false and errorCode=null', () => {
const state = {};
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......@@ -69,6 +68,7 @@ describe('Cycle analytics mutations', () => {
stages: [],
});
expect(state.errorCode).toBe(null);
expect(state.isLoading).toBe(false);
});
......@@ -110,4 +110,16 @@ describe('Cycle analytics mutations', () => {
});
});
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR}`, () => {
it('sets errorCode correctly', () => {
const state = {};
const errorCode = 403;
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state, errorCode);
expect(state.isLoading).toBe(false);
expect(state.errorCode).toBe(errorCode);
});
});
});
......@@ -10627,6 +10627,9 @@ msgstr ""
msgid "One or more of your dependency files are not supported, and the dependency list may be incomplete. Below is a list of supported file types."
msgstr ""
msgid "Only 'Reporter' roles and above on tiers Premium / Silver and above can see Cycle Analytics."
msgstr ""
msgid "Only Project Members"
msgstr ""
......@@ -17984,6 +17987,9 @@ msgstr ""
msgid "You don't have any recent searches"
msgstr ""
msgid "You don’t have access to Cycle Analytics for this group"
msgstr ""
msgid "You don’t have access to Productivity Analytics in this group"
msgstr ""
......
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