Commit e6143067 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'mw-productivity-analytics-no-access-error-handling' into 'master'

Productivity Analytics: Add error handling for reporting on groups which have no plan

Closes #13571

See merge request gitlab-org/gitlab-ee!15291
parents 579bdd50 44c893b8
...@@ -36,6 +36,10 @@ export default { ...@@ -36,6 +36,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
noAccessSvgPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -65,7 +69,11 @@ export default { ...@@ -65,7 +69,11 @@ export default {
'getColumnOptions', 'getColumnOptions',
'columnMetricLabel', 'columnMetricLabel',
'isSelectedSortField', 'isSelectedSortField',
'hasNoAccessError',
]), ]),
showAppContent() {
return this.groupNamespace && !this.hasNoAccessError;
},
}, },
mounted() { mounted() {
this.setEndpoint(this.endpoint); this.setEndpoint(this.endpoint);
...@@ -106,7 +114,18 @@ export default { ...@@ -106,7 +114,18 @@ export default {
) )
" "
/> />
<template v-else> <gl-empty-state
v-if="hasNoAccessError"
class="js-empty-state"
:title="__('You don’t have acces to Productivity Analaytics in this group')"
:svg-path="noAccessSvgPath"
:description="
__(
'Only ‘Reporter’ roles and above on tiers Premium / Silver and above can see Productivity Analytics.',
)
"
/>
<template v-if="showAppContent">
<h4>{{ __('Merge Requests') }}</h4> <h4>{{ __('Merge Requests') }}</h4>
<div class="qa-time-to-merge mb-4"> <div class="qa-time-to-merge mb-4">
<h5>{{ __('Time to merge') }}</h5> <h5>{{ __('Time to merge') }}</h5>
......
...@@ -16,7 +16,7 @@ export default () => { ...@@ -16,7 +16,7 @@ export default () => {
const timeframeContainer = container.querySelector('.js-timeframe-container'); const timeframeContainer = container.querySelector('.js-timeframe-container');
const appContainer = container.querySelector('.js-productivity-analytics-app-container'); const appContainer = container.querySelector('.js-productivity-analytics-app-container');
const { endpoint, emptyStateSvgPath } = appContainer.dataset; const { endpoint, emptyStateSvgPath, noAccessSvgPath } = appContainer.dataset;
let filterManager; let filterManager;
...@@ -100,6 +100,7 @@ export default () => { ...@@ -100,6 +100,7 @@ export default () => {
props: { props: {
endpoint, endpoint,
emptyStateSvgPath, emptyStateSvgPath,
noAccessSvgPath,
}, },
}); });
}, },
......
...@@ -26,9 +26,7 @@ export const fetchChartData = ({ dispatch, getters, rootState }, chartKey) => { ...@@ -26,9 +26,7 @@ export const fetchChartData = ({ dispatch, getters, rootState }, chartKey) => {
const { data } = response; const { data } = response;
dispatch('receiveChartDataSuccess', { chartKey, data }); dispatch('receiveChartDataSuccess', { chartKey, data });
}) })
.catch(() => { .catch(() => dispatch('receiveChartDataError', chartKey));
dispatch('receiveChartDataError', chartKey);
});
}; };
export const receiveChartDataSuccess = ({ commit }, { chartKey, data = {} }) => { export const receiveChartDataSuccess = ({ commit }, { chartKey, data = {} }) => {
......
...@@ -3,8 +3,11 @@ import * as types from './mutation_types'; ...@@ -3,8 +3,11 @@ import * as types from './mutation_types';
export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => { export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
commit(types.SET_GROUP_NAMESPACE, groupNamespace); commit(types.SET_GROUP_NAMESPACE, groupNamespace);
dispatch('charts/fetchAllChartData', null, { root: true }); // let's fetch the merge requests first to see if the user has access to the selected group
dispatch('table/fetchMergeRequests', null, { root: true }); // if there's no 403, then we fetch all chart data
return dispatch('table/fetchMergeRequests', null, { root: true }).then(() =>
dispatch('charts/fetchAllChartData', null, { root: true }),
);
}; };
export const setProjectPath = ({ commit, dispatch }, projectPath) => { export const setProjectPath = ({ commit, dispatch }, projectPath) => {
......
...@@ -21,9 +21,7 @@ export const fetchMergeRequests = ({ dispatch, state, rootState, rootGetters }) ...@@ -21,9 +21,7 @@ export const fetchMergeRequests = ({ dispatch, state, rootState, rootGetters })
const { headers, data } = response; const { headers, data } = response;
dispatch('receiveMergeRequestsSuccess', { headers, data }); dispatch('receiveMergeRequestsSuccess', { headers, data });
}) })
.catch(() => { .catch(err => dispatch('receiveMergeRequestsError', err));
dispatch('receiveMergeRequestsError');
});
}; };
export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
...@@ -35,7 +33,10 @@ export const receiveMergeRequestsSuccess = ({ commit }, { headers, data: mergeRe ...@@ -35,7 +33,10 @@ export const receiveMergeRequestsSuccess = ({ commit }, { headers, data: mergeRe
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { pageInfo, mergeRequests }); commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { pageInfo, mergeRequests });
}; };
export const receiveMergeRequestsError = ({ commit }) => commit(types.RECEIVE_MERGE_REQUESTS_ERROR); export const receiveMergeRequestsError = ({ commit }, { response }) => {
const { status } = response;
commit(types.RECEIVE_MERGE_REQUESTS_ERROR, status);
};
export const setSortField = ({ commit, dispatch }, data) => { export const setSortField = ({ commit, dispatch }, data) => {
commit(types.SET_SORT_FIELD, data); commit(types.SET_SORT_FIELD, data);
......
import httpStatus from '~/lib/utils/http_status';
import { tableSortOrder } from './../../../constants'; import { tableSortOrder } from './../../../constants';
export const sortIcon = state => tableSortOrder[state.sortOrder].icon; export const sortIcon = state => tableSortOrder[state.sortOrder].icon;
...@@ -18,5 +19,7 @@ export const columnMetricLabel = (state, getters) => getters.getColumnOptions[st ...@@ -18,5 +19,7 @@ export const columnMetricLabel = (state, getters) => getters.getColumnOptions[st
export const isSelectedSortField = state => sortField => state.sortField === sortField; export const isSelectedSortField = state => sortField => state.sortField === sortField;
export const hasNoAccessError = state => state.hasError === httpStatus.FORBIDDEN;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -11,9 +11,9 @@ export default { ...@@ -11,9 +11,9 @@ export default {
state.pageInfo = pageInfo; state.pageInfo = pageInfo;
state.mergeRequests = mergeRequests; state.mergeRequests = mergeRequests;
}, },
[types.RECEIVE_MERGE_REQUESTS_ERROR](state) { [types.RECEIVE_MERGE_REQUESTS_ERROR](state, errCode) {
state.isLoadingTable = false; state.isLoadingTable = false;
state.hasError = true; state.hasError = errCode;
state.pageInfo = {}; state.pageInfo = {};
state.mergeRequests = []; state.mergeRequests = [];
}, },
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
.js-search-bar.filter-container.hide .js-search-bar.filter-container.hide
= render 'shared/issuable/search_bar', type: :productivity_analytics = render 'shared/issuable/search_bar', type: :productivity_analytics
.js-timeframe-container .js-timeframe-container
.js-productivity-analytics-app-container{ data: { endpoint: analytics_productivity_analytics_path, empty_state_svg_path: image_path('illustrations/productivity-analytics-empty-state.svg') } } .js-productivity-analytics-app-container{ data: { endpoint: analytics_productivity_analytics_path, empty_state_svg_path: image_path('illustrations/productivity-analytics-empty-state.svg'), no_access_svg_path: image_path('illustrations/analytics/no-access.svg') } }
---
title: 'Productivity Analytics: Add error handling for reporting on groups which have
no plan'
merge_request: 15291
author:
type: other
...@@ -8,29 +8,32 @@ describe('Productivity analytics filter actions', () => { ...@@ -8,29 +8,32 @@ describe('Productivity analytics filter actions', () => {
const projectPath = 'gitlab-test'; const projectPath = 'gitlab-test';
describe('setGroupNamespace', () => { describe('setGroupNamespace', () => {
it('commits the SET_GROUP_NAMESPACE mutation', done => it('commits the SET_GROUP_NAMESPACE mutation', done => {
testAction( const store = {
actions.setGroupNamespace, commit: jest.fn(),
groupNamespace, dispatch: jest.fn(() => Promise.resolve()),
getInitialState(), };
[
{ actions
type: types.SET_GROUP_NAMESPACE, .setGroupNamespace(store, groupNamespace)
payload: groupNamespace, .then(() => {
}, expect(store.commit).toHaveBeenCalledWith(types.SET_GROUP_NAMESPACE, groupNamespace);
],
[ expect(store.dispatch.mock.calls[0]).toEqual([
{ 'table/fetchMergeRequests',
type: 'charts/fetchAllChartData', jasmine.any(Object),
payload: null, { root: true },
}, ]);
{
type: 'table/fetchMergeRequests', expect(store.dispatch.mock.calls[1]).toEqual([
payload: null, 'charts/fetchAllChartData',
}, jasmine.any(Object),
], { root: true },
done, ]);
)); })
.then(done)
.catch(done.fail);
});
}); });
describe('setProjectPath', () => { describe('setProjectPath', () => {
......
...@@ -123,15 +123,22 @@ describe('Productivity analytics table actions', () => { ...@@ -123,15 +123,22 @@ describe('Productivity analytics table actions', () => {
mock.onGet(mockedState.endpoint).replyOnce(500); mock.onGet(mockedState.endpoint).replyOnce(500);
}); });
it('dispatches error', done => it('dispatches error', done => {
testAction( testAction(
actions.fetchMergeRequests, actions.fetchMergeRequests,
null, null,
mockedState, mockedState,
[], [],
[{ type: 'requestMergeRequests' }, { type: 'receiveMergeRequestsError' }], [
{ type: 'requestMergeRequests' },
{
type: 'receiveMergeRequestsError',
payload: new Error('Request failed with status code 500'),
},
],
done, done,
)); );
});
}); });
}); });
...@@ -168,9 +175,9 @@ describe('Productivity analytics table actions', () => { ...@@ -168,9 +175,9 @@ describe('Productivity analytics table actions', () => {
it('should commit error', done => it('should commit error', done =>
testAction( testAction(
actions.receiveMergeRequestsError, actions.receiveMergeRequestsError,
null, { response: { status: 500 } },
mockedContext.state, mockedContext.state,
[{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 500 }],
[], [],
done, done,
)); ));
......
...@@ -69,4 +69,16 @@ describe('Productivity analytics table getters', () => { ...@@ -69,4 +69,16 @@ describe('Productivity analytics table getters', () => {
}); });
}); });
}); });
describe('hasNoAccessError', () => {
it('returns true if "hasError" is set to 403', () => {
state.hasError = 403;
expect(getters.hasNoAccessError(state)).toEqual(true);
});
it('returns false if "hasError" is not set to 403', () => {
state.hasError = false;
expect(getters.hasNoAccessError(state)).toEqual(false);
});
});
}); });
...@@ -40,11 +40,12 @@ describe('Productivity analytics table mutations', () => { ...@@ -40,11 +40,12 @@ describe('Productivity analytics table mutations', () => {
}); });
describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => { describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
it('sets isError and clears data', () => { it('sets hasError to error code and clears data', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](state); const errorCode = 500;
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](state, errorCode);
expect(state.isLoadingTable).toBe(false); expect(state.isLoadingTable).toBe(false);
expect(state.hasError).toBe(true); expect(state.hasError).toBe(errorCode);
expect(state.mergeRequests).toEqual([]); expect(state.mergeRequests).toEqual([]);
expect(state.pageInfo).toEqual({}); expect(state.pageInfo).toEqual({});
}); });
......
...@@ -10567,6 +10567,9 @@ msgstr "" ...@@ -10567,6 +10567,9 @@ msgstr ""
msgid "Only users with an email address in this domain can be added to the group.<br>Example: <code>gitlab.com</code>. Some common domains are not allowed. %{read_more_link}." msgid "Only users with an email address in this domain can be added to the group.<br>Example: <code>gitlab.com</code>. Some common domains are not allowed. %{read_more_link}."
msgstr "" msgstr ""
msgid "Only ‘Reporter’ roles and above on tiers Premium / Silver and above can see Productivity Analytics."
msgstr ""
msgid "Oops, are you sure?" msgid "Oops, are you sure?"
msgstr "" msgstr ""
...@@ -17827,6 +17830,9 @@ msgstr "" ...@@ -17827,6 +17830,9 @@ msgstr ""
msgid "You don't have any recent searches" msgid "You don't have any recent searches"
msgstr "" msgstr ""
msgid "You don’t have acces to Productivity Analaytics in this group"
msgstr ""
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}." msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
msgstr "" 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