Commit c97df0a8 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Martin Wortschack

Integrate report pages with API

The commit introduces the first iteration of integrating the
report pages frontend with the API

A modularised store is introduced and the first functional API
request is implemented
parent 065effb8
......@@ -5,7 +5,11 @@ import createFlash from '~/flash';
import { mergeUrlParams } from '~/lib/utils/url_utility';
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 {
name: 'GroupActivityCard',
......@@ -63,7 +67,7 @@ export default {
});
},
displayReportLink(key) {
return this.enableReportPages && ENABLED_REPORT_PAGES.includes(key);
return this.enableReportPages && Object.keys(REPORT_PAGE_CONFIGURATION).includes(key);
},
generateReportPageLink(key) {
return this.displayReportLink(key)
......@@ -71,6 +75,7 @@ export default {
{
groupPath: this.groupFullPath,
groupName: this.groupName,
reportId: REPORT_PAGE_CONFIGURATION[key].id,
},
this.reportPagesPath,
)
......
<script>
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { GlBreadcrumb, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
export default {
name: 'ReportsApp',
components: {
GlBreadcrumb,
GlIcon,
GlLoadingIcon,
},
methods: {
computed: {
...mapState('page', ['config', 'groupName', 'groupPath', 'isLoading']),
breadcrumbs() {
const { groupName = null, groupPath = null } = queryToObject(document.location.search);
const {
groupName = null,
groupPath = null,
config: { title },
} = this;
return [
groupName && groupPath ? { text: groupName, href: `/${groupPath}` } : null,
{ text: s__('GenericReports|Report'), href: '' },
{ text: title, href: '' },
].filter(Boolean);
},
},
mounted() {
this.fetchPageConfigData();
},
methods: {
...mapActions('page', ['fetchPageConfigData']),
},
};
</script>
<template>
<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>
<gl-icon name="angle-right" :size="8" />
</template>
......
import Vue from 'vue';
import Vuex from 'vuex';
import { queryToObject } from '~/lib/utils/url_utility';
import ReportsApp from './components/app.vue';
import createsStore from './store';
Vue.use(Vuex);
export default () => {
const el = document.querySelector('#js-reports-app');
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({
el,
name: 'ReportsApp',
render: createElement =>
createElement(ReportsApp, {
props: {},
}),
store,
render: createElement => createElement(ReportsApp),
});
};
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
- 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 { shallowMount } from '@vue/test-utils';
import { GlBreadcrumb } from '@gitlab/ui';
import { objectToQuery } from '~/lib/utils/url_utility';
import createStore from 'ee/analytics/reports/store';
import { initialState, configData, pageData } from 'ee_jest/analytics/reports/mock_data';
const GROUP_NAME = 'Gitlab Org';
const GROUP_PATH = 'gitlab-org';
const DEFAULT_REPORT_TITLE = 'Report';
const GROUP_URL_QUERY = objectToQuery({
groupName: GROUP_NAME,
groupPath: GROUP_PATH,
});
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ReportsApp', () => {
let wrapper;
let mock;
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 findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet().reply(httpStatusCodes.OK, configData);
});
afterEach(() => {
mock.restore();
wrapper.destroy();
wrapper = null;
});
describe('contains the correct breadcrumbs', () => {
it('displays the report title by default', () => {
describe('loading icon', () => {
it('displays the icon while page config is being retrieved', async () => {
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', () => {
beforeEach(() => {
window.history.replaceState({}, null, `?${GROUP_URL_QUERY}`);
});
it('hides the icon once page config has being retrieved', async () => {
wrapper = createComponent();
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();
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([
{ text: GROUP_NAME, href: `/${GROUP_PATH}` },
{ text: DEFAULT_REPORT_TITLE, href: '' },
expect(findGlBreadcrumb().props('items')).toStrictEqual([
{ text: groupName, href: `/${groupPath}` },
{ 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);
},
);
});
......@@ -23425,6 +23425,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
msgid "There was an error while fetching configuration data."
msgstr ""
msgid "There was an error while fetching value stream analytics data."
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