Commit a4fd75e2 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch...

Merge branch '218460-group-activity-mrs-created-chart-w-single-series-add-configurable-column-chart' into 'master'

Group Activity - Fetch page config data

See merge request gitlab-org/gitlab!35953
parents 24c04674 c97df0a8
...@@ -5,7 +5,11 @@ import createFlash from '~/flash'; ...@@ -5,7 +5,11 @@ import createFlash from '~/flash';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import MetricCard from '../../shared/components/metric_card.vue'; import MetricCard from '../../shared/components/metric_card.vue';
const ENABLED_REPORT_PAGES = ['mergeRequests']; const REPORT_PAGE_CONFIGURATION = {
mergeRequests: {
id: 'recent_merge_requests_by_group',
},
};
export default { export default {
name: 'GroupActivityCard', name: 'GroupActivityCard',
...@@ -63,7 +67,7 @@ export default { ...@@ -63,7 +67,7 @@ export default {
}); });
}, },
displayReportLink(key) { displayReportLink(key) {
return this.enableReportPages && ENABLED_REPORT_PAGES.includes(key); return this.enableReportPages && Object.keys(REPORT_PAGE_CONFIGURATION).includes(key);
}, },
generateReportPageLink(key) { generateReportPageLink(key) {
return this.displayReportLink(key) return this.displayReportLink(key)
...@@ -71,6 +75,7 @@ export default { ...@@ -71,6 +75,7 @@ export default {
{ {
groupPath: this.groupFullPath, groupPath: this.groupFullPath,
groupName: this.groupName, groupName: this.groupName,
reportId: REPORT_PAGE_CONFIGURATION[key].id,
}, },
this.reportPagesPath, this.reportPagesPath,
) )
......
<script> <script>
import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; import { GlBreadcrumb, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { queryToObject } from '~/lib/utils/url_utility'; import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
export default { export default {
name: 'ReportsApp', name: 'ReportsApp',
components: { components: {
GlBreadcrumb, GlBreadcrumb,
GlIcon, GlIcon,
GlLoadingIcon,
}, },
methods: { computed: {
...mapState('page', ['config', 'groupName', 'groupPath', 'isLoading']),
breadcrumbs() { breadcrumbs() {
const { groupName = null, groupPath = null } = queryToObject(document.location.search); const {
groupName = null,
groupPath = null,
config: { title },
} = this;
return [ return [
groupName && groupPath ? { text: groupName, href: `/${groupPath}` } : null, groupName && groupPath ? { text: groupName, href: `/${groupPath}` } : null,
{ text: s__('GenericReports|Report'), href: '' }, { text: title, href: '' },
].filter(Boolean); ].filter(Boolean);
}, },
}, },
mounted() {
this.fetchPageConfigData();
},
methods: {
...mapActions('page', ['fetchPageConfigData']),
},
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-breadcrumb :items="breadcrumbs()"> <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-5" />
<gl-breadcrumb v-else :items="breadcrumbs">
<template #separator> <template #separator>
<gl-icon name="angle-right" :size="8" /> <gl-icon name="angle-right" :size="8" />
</template> </template>
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import { queryToObject } from '~/lib/utils/url_utility';
import ReportsApp from './components/app.vue'; import ReportsApp from './components/app.vue';
import createsStore from './store';
Vue.use(Vuex);
export default () => { export default () => {
const el = document.querySelector('#js-reports-app'); const el = document.querySelector('#js-reports-app');
if (!el) return false; if (!el) return false;
const store = createsStore();
const { configEndpoint } = el.dataset;
const { groupName = null, groupPath = null, reportId = null } = queryToObject(
document.location.search,
);
store.dispatch('page/setInitialPageData', { configEndpoint, groupName, groupPath, reportId });
return new Vue({ return new Vue({
el, el,
name: 'ReportsApp', name: 'ReportsApp',
render: createElement => store,
createElement(ReportsApp, { render: createElement => createElement(ReportsApp),
props: {},
}),
}); });
}; };
import Vuex from 'vuex';
import page from './modules/page/index';
export default () =>
new Vuex.Store({
modules: { page },
});
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const setInitialPageData = ({ commit }, data) => commit(types.SET_INITIAL_PAGE_DATA, data);
export const requestPageConfigData = ({ commit }) => commit(types.REQUEST_PAGE_CONFIG_DATA);
export const receivePageConfigDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_PAGE_CONFIG_DATA_SUCCESS, data);
export const receivePageConfigDataError = ({ commit }) => {
commit(types.RECEIVE_PAGE_CONFIG_DATA_ERROR);
createFlash(__('There was an error while fetching configuration data.'));
};
export const fetchPageConfigData = ({ dispatch, state }) => {
dispatch('requestPageConfigData');
const { groupPath, reportId, configEndpoint } = state;
return axios
.get(configEndpoint.replace('REPORT_ID', reportId), {
params: {
group_id: groupPath,
},
})
.then(response => {
const { data } = response;
dispatch('receivePageConfigDataSuccess', data);
})
.catch(() => dispatch('receivePageConfigDataError'));
};
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
actions,
};
export const SET_INITIAL_PAGE_DATA = 'SET_INITIAL_PAGE_DATA';
export const REQUEST_PAGE_CONFIG_DATA = 'REQUEST_PAGE_CONFIG_DATA';
export const RECEIVE_PAGE_CONFIG_DATA_SUCCESS = 'RECEIVE_PAGE_CONFIG_DATA_SUCCESS';
export const RECEIVE_PAGE_CONFIG_DATA_ERROR = 'RECEIVE_PAGE_CONFIG_DATA_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_PAGE_DATA](state, data) {
const { configEndpoint, reportId, groupName, groupPath } = data;
state.configEndpoint = configEndpoint;
state.reportId = reportId;
state.groupName = groupName;
state.groupPath = groupPath;
},
[types.REQUEST_PAGE_CONFIG_DATA](state) {
state.isLoading = true;
},
[types.RECEIVE_PAGE_CONFIG_DATA_SUCCESS](state, data) {
state.isLoading = false;
state.config = data;
},
[types.RECEIVE_PAGE_CONFIG_DATA_ERROR](state) {
state.isLoading = false;
},
};
import { s__ } from '~/locale';
export default () => ({
isLoading: false,
configEndpoint: '',
reportId: null,
groupName: null,
groupPath: null,
config: {
title: s__('GenericReports|Report'),
chart: null,
},
});
- @hide_breadcrumbs = true - @hide_breadcrumbs = true
- page_title _("Reports") - page_title _("Reports")
#js-reports-app #js-reports-app{ data: { config_endpoint: api_v4_analytics_reports_chart_path(report_id: 'REPORT_ID') } }
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlBreadcrumb, GlLoadingIcon } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
import ReportsApp from 'ee/analytics/reports/components/app.vue'; import ReportsApp from 'ee/analytics/reports/components/app.vue';
import { shallowMount } from '@vue/test-utils'; import createStore from 'ee/analytics/reports/store';
import { GlBreadcrumb } from '@gitlab/ui'; import { initialState, configData, pageData } from 'ee_jest/analytics/reports/mock_data';
import { objectToQuery } from '~/lib/utils/url_utility';
const GROUP_NAME = 'Gitlab Org'; const localVue = createLocalVue();
const GROUP_PATH = 'gitlab-org'; localVue.use(Vuex);
const DEFAULT_REPORT_TITLE = 'Report';
const GROUP_URL_QUERY = objectToQuery({
groupName: GROUP_NAME,
groupPath: GROUP_PATH,
});
describe('ReportsApp', () => { describe('ReportsApp', () => {
let wrapper; let wrapper;
let mock;
const createComponent = () => { const createComponent = () => {
return shallowMount(ReportsApp); const component = shallowMount(ReportsApp, {
localVue,
store: createStore(),
});
component.vm.$store.dispatch('page/setInitialPageData', pageData);
return component;
}; };
const findGlBreadcrumb = () => wrapper.find(GlBreadcrumb); const findGlBreadcrumb = () => wrapper.find(GlBreadcrumb);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet().reply(httpStatusCodes.OK, configData);
});
afterEach(() => { afterEach(() => {
mock.restore();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
describe('contains the correct breadcrumbs', () => { describe('loading icon', () => {
it('displays the report title by default', () => { it('displays the icon while page config is being retrieved', async () => {
wrapper = createComponent(); wrapper = createComponent();
const breadcrumbs = findGlBreadcrumb(); await wrapper.vm.$nextTick();
expect(breadcrumbs.props('items')).toStrictEqual([{ text: DEFAULT_REPORT_TITLE, href: '' }]); expect(findGlLoadingIcon().exists()).toBe(true);
}); });
describe('with a group in the URL', () => { it('hides the icon once page config has being retrieved', async () => {
beforeEach(() => { wrapper = createComponent();
window.history.replaceState({}, null, `?${GROUP_URL_QUERY}`);
}); wrapper.vm.$store.dispatch('page/receivePageConfigDataSuccess', configData);
it('displays the group name and report title', () => { await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(false);
});
});
describe('contains the correct breadcrumbs', () => {
it('displays the "Report" title by default', () => {
wrapper = createComponent();
const {
config: { title },
} = initialState;
expect(findGlBreadcrumb().props('items')).toStrictEqual([{ text: title, href: '' }]);
});
describe('with a config specified', () => {
it('displays the group name and report title once retrieved', async () => {
wrapper = createComponent(); wrapper = createComponent();
const breadcrumbs = findGlBreadcrumb(); wrapper.vm.$store.dispatch('page/receivePageConfigDataSuccess', configData);
await wrapper.vm.$nextTick();
const { groupName, groupPath } = pageData;
const { title } = configData;
expect(breadcrumbs.props('items')).toStrictEqual([ expect(findGlBreadcrumb().props('items')).toStrictEqual([
{ text: GROUP_NAME, href: `/${GROUP_PATH}` }, { text: groupName, href: `/${groupPath}` },
{ text: DEFAULT_REPORT_TITLE, href: '' }, { text: title, href: '' },
]); ]);
}); });
}); });
......
export const initialState = {
configEndpoint: '',
reportId: null,
groupName: null,
groupPath: null,
config: {
title: 'Report',
},
};
export const pageData = {
configEndpoint: 'foo_bar_endpoint',
reportId: 'foo_bar_id',
groupName: 'Foo Bar',
groupPath: 'foo_bar',
};
export const configData = {
title: 'Foo Bar Report',
};
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import httpStatusCodes from '~/lib/utils/http_status';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/analytics/reports/store/modules/page/actions';
import { initialState, pageData, configData } from 'ee_jest/analytics/reports/mock_data';
jest.mock('~/flash');
describe('Reports page actions', () => {
let state;
let mock;
beforeEach(() => {
state = initialState;
mock = new MockAdapter(axios);
});
afterEach(() => {
state = null;
mock.restore();
});
it.each`
action | type | payload
${'setInitialPageData'} | ${'SET_INITIAL_PAGE_DATA'} | ${pageData}
${'requestPageConfigData'} | ${'REQUEST_PAGE_CONFIG_DATA'} | ${null}
${'receivePageConfigDataSuccess'} | ${'RECEIVE_PAGE_CONFIG_DATA_SUCCESS'} | ${configData}
${'receivePageConfigDataError'} | ${'RECEIVE_PAGE_CONFIG_DATA_ERROR'} | ${null}
`('$action commits mutation $type with $payload', ({ action, type, payload }) => {
return testAction(
actions[action],
payload,
state,
[payload ? { type, payload } : { type }],
[],
);
});
describe('receivePageConfigDataError', () => {
it('displays an error message', () => {
actions.receivePageConfigDataError({ commit: jest.fn() });
expect(createFlash).toHaveBeenCalledWith(
'There was an error while fetching configuration data.',
);
});
});
describe('fetchPageConfigData', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet().reply(httpStatusCodes.OK, configData);
});
it('dispatches the "requestPageConfigData" and "receivePageConfigDataSuccess" actions', () => {
return testAction(
actions.fetchPageConfigData,
null,
state,
[],
[
{ type: 'requestPageConfigData' },
{ type: 'receivePageConfigDataSuccess', payload: configData },
],
);
});
});
describe('failure', () => {
beforeEach(() => {
mock.onGet().reply(httpStatusCodes.NOT_FOUND);
});
it('dispatches the "requestPageConfigData" and "receivePageConfigDataError" actions', () => {
return testAction(
actions.fetchPageConfigData,
null,
state,
[],
[{ type: 'requestPageConfigData' }, { type: 'receivePageConfigDataError' }],
);
});
});
});
});
import * as types from 'ee/analytics/reports/store/modules/page/mutation_types';
import mutations from 'ee/analytics/reports/store/modules/page/mutations';
import { initialState, pageData, configData } from 'ee_jest/analytics/reports/mock_data';
describe('Reports page mutations', () => {
let state;
beforeEach(() => {
state = initialState;
});
afterEach(() => {
state = null;
});
it.each`
mutation | stateKey | value
${types.REQUEST_PAGE_CONFIG_DATA} | ${'isLoading'} | ${true}
${types.RECEIVE_PAGE_CONFIG_DATA_ERROR} | ${'isLoading'} | ${false}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
expect(state[stateKey]).toEqual(value);
});
it.each`
mutation | payload | expectedState
${types.SET_INITIAL_PAGE_DATA} | ${pageData} | ${pageData}
${types.RECEIVE_PAGE_CONFIG_DATA_SUCCESS} | ${configData} | ${{ config: configData, isLoading: false }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
mutations[mutation](state, payload);
expect(state).toMatchObject(expectedState);
},
);
});
...@@ -23566,6 +23566,9 @@ msgstr "" ...@@ -23566,6 +23566,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label." msgid "There was an error when unsubscribing from this label."
msgstr "" msgstr ""
msgid "There was an error while fetching configuration data."
msgstr ""
msgid "There was an error while fetching value stream analytics data." msgid "There was an error while fetching value stream analytics data."
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