Commit 9c39998a authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Kushal Pandya

Replace summary table with recent activity card

Encapsulates the code to display recent activity
in a separate component. Removes the need to
store the activity data in the vuex store

Render recent activity using a shared
metrics component
parent 46264342
......@@ -10,11 +10,11 @@ import Scatterplot from '../../shared/components/scatterplot.vue';
import { LAST_ACTIVITY_AT, dateFormats, DATE_RANGE_LIMIT } from '../../shared/constants';
import DateRange from '../../shared/components/daterange.vue';
import StageDropdownFilter from './stage_dropdown_filter.vue';
import SummaryTable from './summary_table.vue';
import StageTable from './stage_table.vue';
import TasksByTypeChart from './tasks_by_type_chart.vue';
import UrlSyncMixin from '../../shared/mixins/url_sync_mixin';
import { toYmd } from '../../shared/utils';
import RecentActivityCard from './recent_activity_card.vue';
export default {
name: 'CycleAnalytics',
......@@ -24,11 +24,11 @@ export default {
GlEmptyState,
GroupsDropdownFilter,
ProjectsDropdownFilter,
SummaryTable,
StageTable,
StageDropdownFilter,
Scatterplot,
TasksByTypeChart,
RecentActivityCard,
},
mixins: [glFeatureFlagsMixin(), UrlSyncMixin],
props: {
......@@ -84,6 +84,7 @@ export default {
'activeStages',
'selectedProjectIds',
'enableCustomOrdering',
'cycleAnalyticsRequestParams',
]),
shouldRenderEmptyState() {
return !this.selectedGroup;
......@@ -281,11 +282,16 @@ export default {
"
/>
<div v-else-if="!errorCode">
<div class="js-recent-activity mt-3">
<recent-activity-card
:group-path="currentGroupPath"
:additional-params="cycleAnalyticsRequestParams"
/>
</div>
<div v-if="isLoading">
<gl-loading-icon class="mt-4" size="md" />
</div>
<div v-else>
<summary-table class="js-summary-table" :items="summary" />
<stage-table
v-if="selectedStage"
:key="stageCount"
......
<script>
import Api from 'ee/api';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { slugify } from '~/lib/utils/text_utility';
import MetricCard from '../../shared/components/metric_card.vue';
import { removeFlash } from '../utils';
export default {
name: 'RecentActivityCard',
components: {
MetricCard,
},
props: {
groupPath: {
type: String,
required: true,
},
additionalParams: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
data: [],
loading: false,
};
},
watch: {
additionalParams() {
this.fetchData();
},
},
mounted() {
this.fetchData();
},
methods: {
fetchData() {
removeFlash();
this.loading = true;
return Api.cycleAnalyticsSummaryData(
this.groupPath,
this.additionalParams ? this.additionalParams : {},
)
.then(({ data }) => {
this.data = data.map(({ title: label, value }) => ({
value: value || '-',
label,
key: slugify(label),
}));
})
.catch(() => {
createFlash(
__('There was an error while fetching value stream analytics recent activity data.'),
);
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>
<template>
<metric-card :title="__('Recent Activity')" :metrics="data" :is-loading="loading" />
</template>
<script>
export default {
name: 'SummaryTable',
props: {
items: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="wrapper mt-3">
<div class="card">
<div class="card-header font-weight-bold">{{ __('Recent Activity') }}</div>
<div class="content-block">
<div class="container-fluid">
<div class="row">
<div class="col-sm-3"></div>
<div
v-for="{ title, value } in items"
:key="title"
class="col-sm-3 col-12 column text-center"
>
<h3 class="header">{{ value }}</h3>
<p class="text">{{ title }}</p>
</div>
<div class="col-sm-3"></div>
</div>
</div>
</div>
</div>
</div>
</template>
......@@ -133,7 +133,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
return Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('fetchSummaryData'))
.then(() => dispatch('receiveCycleAnalyticsDataSuccess'))
.catch(error => dispatch('receiveCycleAnalyticsDataError', error));
};
......@@ -170,37 +169,6 @@ export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}
removeFlash();
};
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
export const receiveSummaryDataError = ({ commit }, error) => {
commit(types.RECEIVE_SUMMARY_DATA_ERROR, error);
createFlash(__('There was an error while fetching value stream analytics summary data.'));
};
export const receiveSummaryDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_SUMMARY_DATA_SUCCESS, data);
export const fetchSummaryData = ({ state, dispatch, getters }) => {
const {
cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
} = getters;
dispatch('requestSummaryData');
const {
selectedGroup: { fullPath },
} = state;
return Api.cycleAnalyticsSummaryData(fullPath, {
created_after,
created_before,
project_ids,
})
.then(({ data }) => dispatch('receiveSummaryDataSuccess', data))
.catch(error =>
handleErrorOrRethrow({ error, action: () => dispatch('receiveSummaryDataError', error) }),
);
};
export const requestGroupStagesAndEvents = ({ commit }) =>
commit(types.REQUEST_GROUP_STAGES_AND_EVENTS);
......@@ -294,7 +262,6 @@ export const receiveCreateCustomStageSuccess = ({ commit, dispatch }, { data: {
return Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchSummaryData'))
.catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
......
......@@ -28,10 +28,6 @@ export const REQUEST_TOP_RANKED_GROUP_LABELS = 'REQUEST_TOP_RANKED_GROUP_LABELS'
export const RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS = 'RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS';
export const RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR = 'RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR';
export const REQUEST_SUMMARY_DATA = 'REQUEST_SUMMARY_DATA';
export const RECEIVE_SUMMARY_DATA_SUCCESS = 'RECEIVE_SUMMARY_DATA_SUCCESS';
export const RECEIVE_SUMMARY_DATA_ERROR = 'RECEIVE_SUMMARY_DATA_ERROR';
export const REQUEST_GROUP_STAGES_AND_EVENTS = 'REQUEST_GROUP_STAGES_AND_EVENTS';
export const RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS = 'RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS';
export const RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR = 'RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR';
......
......@@ -115,18 +115,6 @@ export default {
[types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) {
state.customStageFormErrors = null;
},
[types.RECEIVE_SUMMARY_DATA_ERROR](state) {
state.summary = [];
},
[types.REQUEST_SUMMARY_DATA](state) {
state.summary = [];
},
[types.RECEIVE_SUMMARY_DATA_SUCCESS](state, data) {
state.summary = data.map(item => ({
...item,
value: item.value || '-',
}));
},
[types.REQUEST_GROUP_STAGES_AND_EVENTS](state) {
state.stages = [];
state.customStageFormEvents = [];
......
......@@ -37,7 +37,7 @@ export default {
v-for="metric in metrics"
:key="metric.key"
ref="metricItem"
class="flex-grow text-center"
class="js-metric-card-item flex-grow text-center"
>
<h3 class="my-2">
<template v-if="metric.value === null"
......
......@@ -189,7 +189,7 @@ describe 'Group Value Stream Analytics', :js do
shared_examples 'group value stream analytics' do
context 'summary table', :js do
it 'will display recent activity' do
page.within(find('.js-summary-table')) do
page.within(find('.js-recent-activity')) do
expect(page).to have_selector('.card-header')
expect(page).to have_content('Recent Activity')
end
......@@ -221,23 +221,27 @@ describe 'Group Value Stream Analytics', :js do
end
context 'with a group selected' do
card_metric_selector = ".js-recent-activity .js-metric-card-item"
before do
select_group
expect(page).to have_css(card_metric_selector)
end
it_behaves_like 'group value stream analytics'
it 'displays the number of issues' do
expect(page).to have_content('New Issues')
issue_count = page.all(card_metric_selector).first
issue_count = find(".card .header", match: :first)
expect(issue_count).to have_content('New Issues')
expect(issue_count).to have_content('3')
end
it 'displays the number of deploys' do
expect(page).to have_content('Deploys')
deploys_count = page.all(card_metric_selector).last
deploys_count = page.all(".card .header").last
expect(deploys_count).to have_content('Deploys')
expect(deploys_count).to have_content('-')
end
end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecentActivityCard matches the snapshot 1`] = `
<div
class="card"
>
<!---->
<div
class="card-header"
>
<strong>
Recent Activity
</strong>
</div>
<div
class="card-body"
>
<!---->
<!---->
<div
class="d-flex"
>
<div
class="js-metric-card-item flex-grow text-center"
>
<h3
class="my-2"
>
3
</h3>
<p
class="text-secondary gl-font-size-small mb-2"
>
New Issues
</p>
</div>
<div
class="js-metric-card-item flex-grow text-center"
>
<h3
class="my-2"
>
-
</h3>
<p
class="text-secondary gl-font-size-small mb-2"
>
Deploys
</p>
</div>
</div>
</div>
<!---->
<!---->
</div>
`;
......@@ -8,7 +8,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import GroupsDropdownFilter from 'ee/analytics/shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue';
import SummaryTable from 'ee/analytics/cycle_analytics/components/summary_table.vue';
import RecentActivityCard from 'ee/analytics/cycle_analytics/components/recent_activity_card.vue';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import 'bootstrap';
import '~/gl_dropdown';
......@@ -34,7 +34,7 @@ const localVue = createLocalVue();
localVue.use(Vuex);
const defaultStubs = {
'summary-table': true,
'recent-activity-card': true,
'stage-event-list': true,
'stage-nav-item': true,
'tasks-by-type-chart': true,
......@@ -119,8 +119,8 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(Daterange).exists()).toBe(flag);
};
const displaysSummaryTable = flag => {
expect(wrapper.find(SummaryTable).exists()).toBe(flag);
const displaysRecentActivityCard = flag => {
expect(wrapper.find(RecentActivityCard).exists()).toBe(flag);
};
const displaysStageTable = flag => {
......@@ -176,7 +176,7 @@ describe('Cycle Analytics component', () => {
});
it('does not display the summary table', () => {
displaysSummaryTable(false);
displaysRecentActivityCard(false);
});
it('does not display the stage table', () => {
......@@ -230,7 +230,7 @@ describe('Cycle Analytics component', () => {
});
it('displays the summary table', () => {
displaysSummaryTable(true);
displaysRecentActivityCard(true);
});
it('displays the stage table', () => {
......@@ -341,7 +341,7 @@ describe('Cycle Analytics component', () => {
});
it('does not display the summary table', () => {
displaysSummaryTable(false);
displaysRecentActivityCard(false);
});
it('does not display the stage table', () => {
......@@ -435,11 +435,6 @@ describe('Cycle Analytics component', () => {
}) => {
const defaultStatus = 200;
const defaultRequests = {
fetchSummaryData: {
status: defaultStatus,
endpoint: mockData.endpoints.summaryData,
response: [...mockData.summaryData],
},
fetchGroupStagesAndEvents: {
status: defaultStatus,
endpoint: mockData.endpoints.baseStagesEndpoint,
......@@ -500,24 +495,6 @@ describe('Cycle Analytics component', () => {
});
};
it('will display an error if the fetchSummaryData request fails', () => {
expect(findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({
overrides: {
fetchSummaryData: {
status: httpStatusCodes.NOT_FOUND,
endpoint: mockData.endpoints.summaryData,
response: { response: { status: httpStatusCodes.NOT_FOUND } },
},
},
});
return selectGroupAndFindError(
'There was an error while fetching value stream analytics summary data.',
);
});
it('will display an error if the fetchGroupStagesAndEvents request fails', () => {
expect(findFlashError()).toBeNull();
......
import { mount } from '@vue/test-utils';
import createFlash from '~/flash';
import RecentActivityCard from 'ee/analytics/cycle_analytics/components/recent_activity_card.vue';
import { group, recentActivityData } from '../mock_data';
import Api from 'ee/api';
jest.mock('~/flash');
describe('RecentActivityCard', () => {
const { full_path: groupPath } = group;
let wrapper;
const createComponent = (additionalParams = {}) => {
return mount(RecentActivityCard, {
propsData: {
groupPath,
additionalParams,
},
});
};
beforeEach(() => {
jest.spyOn(Api, 'cycleAnalyticsSummaryData').mockResolvedValue({ data: recentActivityData });
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('fetches the recent activity data', () => {
expect(Api.cycleAnalyticsSummaryData).toHaveBeenCalledWith(groupPath, {});
});
describe('with a failing request', () => {
beforeEach(() => {
jest.spyOn(Api, 'cycleAnalyticsSummaryData').mockRejectedValue();
wrapper = createComponent();
});
it('should render an error message', () => {
expect(createFlash).toHaveBeenCalledWith(
'There was an error while fetching value stream analytics recent activity data.',
);
});
});
describe('with additional params', () => {
beforeEach(() => {
wrapper = createComponent({
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
});
});
it('sends additional parameters as query paremeters', () => {
expect(Api.cycleAnalyticsSummaryData).toHaveBeenCalledWith(groupPath, {
'project_ids[]': [1],
created_after: '2020-01-01',
created_before: '2020-02-01',
});
});
});
});
......@@ -13,13 +13,13 @@ const fixtureEndpoints = {
customizableCycleAnalyticsStagesAndEvents: 'analytics/value_stream_analytics/stages.json', // customizable stages and events endpoint
stageEvents: stage => `analytics/value_stream_analytics/stages/${stage}/records.json`,
stageMedian: stage => `analytics/value_stream_analytics/stages/${stage}/median.json`,
summaryData: 'analytics/value_stream_analytics/summary.json',
recentActivityData: 'analytics/value_stream_analytics/summary.json',
groupLabels: 'api/group_labels.json',
};
export const endpoints = {
groupLabels: /groups\/[A-Z|a-z|\d|\-|_]+\/-\/labels.json/,
summaryData: /analytics\/value_stream_analytics\/summary/,
recentActivityData: /analytics\/value_stream_analytics\/summary/,
durationData: /analytics\/value_stream_analytics\/stages\/\d+\/duration_chart/,
stageData: /analytics\/value_stream_analytics\/stages\/\d+\/records/,
stageMedian: /analytics\/value_stream_analytics\/stages\/\d+\/median/,
......@@ -43,7 +43,7 @@ export const group = {
const getStageByTitle = (stages, title) =>
stages.find(stage => stage.title && stage.title.toLowerCase().trim() === title) || {};
export const summaryData = getJSONFixture(fixtureEndpoints.summaryData);
export const recentActivityData = getJSONFixture(fixtureEndpoints.recentActivityData);
export const customizableStagesAndEvents = getJSONFixture(
fixtureEndpoints.customizableCycleAnalyticsStagesAndEvents,
......
......@@ -12,7 +12,6 @@ import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import {
group,
summaryData,
allowedStages as stages,
groupLabels,
startDate,
......@@ -275,7 +274,6 @@ describe('Cycle analytics actions', () => {
fetchStageMedianValues: overrides.fetchStageMedianValues || jest.fn().mockResolvedValue(),
fetchGroupStagesAndEvents:
overrides.fetchGroupStagesAndEvents || jest.fn().mockResolvedValue(),
fetchSummaryData: overrides.fetchSummaryData || jest.fn().mockResolvedValue(),
receiveCycleAnalyticsDataSuccess:
overrides.receiveCycleAnalyticsDataSuccess || jest.fn().mockResolvedValue(),
};
......@@ -286,14 +284,12 @@ describe('Cycle analytics actions', () => {
.mockImplementationOnce(mocks.requestCycleAnalyticsData)
.mockImplementationOnce(mocks.fetchGroupStagesAndEvents)
.mockImplementationOnce(mocks.fetchStageMedianValues)
.mockImplementationOnce(mocks.fetchSummaryData)
.mockImplementationOnce(mocks.receiveCycleAnalyticsDataSuccess),
};
}
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock.onGet(endpoints.summaryData).replyOnce(200, summaryData);
state = { ...state, selectedGroup, startDate, endDate };
});
......@@ -307,7 +303,6 @@ describe('Cycle analytics actions', () => {
{ type: 'requestCycleAnalyticsData' },
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' },
{ type: 'fetchSummaryData' },
{ type: 'receiveCycleAnalyticsDataSuccess' },
],
done,
......@@ -340,34 +335,6 @@ describe('Cycle analytics actions', () => {
.catch(done.fail);
});
it(`displays an error if fetchSummaryData fails`, done => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchSummaryData: actions.fetchSummaryData({
dispatch: jest
.fn()
.mockResolvedValueOnce()
.mockImplementation(actions.receiveSummaryDataError({ commit: () => {} })),
commit: () => {},
state: { ...state },
getters,
}),
});
actions
.fetchCycleAnalyticsData({
dispatch: mockDispatchContext,
state: {},
commit: () => {},
})
.then(() => {
shouldFlashAMessage(
'There was an error while fetching value stream analytics summary data.',
);
done();
})
.catch(done.fail);
});
it(`displays an error if fetchGroupStagesAndEvents fails`, done => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchGroupStagesAndEvents: actions.fetchGroupStagesAndEvents({
......@@ -684,7 +651,7 @@ describe('Cycle analytics actions', () => {
title: 'NEW - COOL',
};
it('will dispatch fetchGroupStagesAndEvents and fetchSummaryData', () =>
it('will dispatch fetchGroupStagesAndEvents', () =>
testAction(
actions.receiveUpdateStageSuccess,
response,
......@@ -1502,13 +1469,13 @@ describe('Cycle analytics actions', () => {
},
};
it('will dispatch fetchGroupStagesAndEvents and fetchSummaryData', () =>
it('will dispatch fetchGroupStagesAndEvents', () =>
testAction(
actions.receiveCreateCustomStageSuccess,
response,
state,
[{ type: types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents' }, { type: 'fetchSummaryData' }],
[{ type: 'fetchGroupStagesAndEvents' }],
));
describe('with an error', () => {
......
......@@ -4,7 +4,6 @@ import { TASKS_BY_TYPE_FILTERS } from 'ee/analytics/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
summaryData,
rawIssueEvents,
issueEvents as transformedEvents,
issueStage,
......@@ -52,8 +51,6 @@ describe('Cycle analytics mutations', () => {
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_TOP_RANKED_GROUP_LABELS} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'stages'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]}
......@@ -149,7 +146,6 @@ describe('Cycle analytics mutations', () => {
it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
stats: [],
summary: [],
stages: [],
});
......@@ -175,20 +171,6 @@ describe('Cycle analytics mutations', () => {
});
});
describe(`${types.RECEIVE_SUMMARY_DATA_SUCCESS}`, () => {
beforeEach(() => {
state = { stages: [{ slug: 'plan' }, { slug: 'issue' }, { slug: 'test' }] };
mutations[types.RECEIVE_SUMMARY_DATA_SUCCESS](state, summaryData);
});
it('will set each summary item with a value of 0 to "-"', () => {
expect(state.summary).toEqual([
{ value: 3, title: 'New Issues' },
{ value: '-', title: 'Deploys' },
]);
});
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR}`, () => {
it('sets errorCode correctly', () => {
const errorCode = 403;
......
......@@ -22,7 +22,7 @@ exports[`GroupActivity component matches the snapshot 1`] = `
class="d-flex"
>
<div
class="flex-grow text-center"
class="js-metric-card-item flex-grow text-center"
>
<h3
class="my-2"
......@@ -37,7 +37,7 @@ exports[`GroupActivity component matches the snapshot 1`] = `
</p>
</div>
<div
class="flex-grow text-center"
class="js-metric-card-item flex-grow text-center"
>
<h3
class="my-2"
......@@ -52,7 +52,7 @@ exports[`GroupActivity component matches the snapshot 1`] = `
</p>
</div>
<div
class="flex-grow text-center"
class="js-metric-card-item flex-grow text-center"
>
<h3
class="my-2"
......
......@@ -20495,7 +20495,7 @@ msgstr ""
msgid "There was an error while fetching value stream analytics duration median data."
msgstr ""
msgid "There was an error while fetching value stream analytics summary data."
msgid "There was an error while fetching value stream analytics recent activity data."
msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
......
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