Commit 8bbb3725 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Brandon Labuschagne

Use config for projects and groups usage trends

Refactor the projects and groups usage trends chart
to use the shared config for usage trends

Removes the existing component
parent f162b3ff
<script>
import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
import ChartsConfig from './charts_config';
import ProjectsAndGroupsChart from './projects_and_groups_chart.vue';
import UsageCounts from './usage_counts.vue';
import UsageTrendsCountChart from './usage_trends_count_chart.vue';
import UsersChart from './users_chart.vue';
......@@ -12,7 +11,6 @@ export default {
UsageCounts,
UsageTrendsCountChart,
UsersChart,
ProjectsAndGroupsChart,
},
TOTAL_DAYS_TO_SHOW,
START_DATE,
......@@ -29,11 +27,6 @@ export default {
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
<projects-and-groups-chart
:start-date="$options.START_DATE"
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
<usage-trends-count-chart
v-for="chartOptions in $options.configs"
:key="chartOptions.chartTitle"
......
import { s__, __, sprintf } from '~/locale';
import { s__, __ } from '~/locale';
import query from '../graphql/queries/usage_count.query.graphql';
const noDataMessage = s__('UsageTrends|No data available.');
export default [
{
loadChartError: sprintf(
s__('UsageTrends|Could not load the pipelines chart. Please refresh the page to try again.'),
loadChartError: s__(
'UsageTrends|Could not load the projects and groups chart. Please refresh the page to try again.',
),
noDataMessage,
chartTitle: s__('UsageTrends|Total projects & groups'),
yAxisTitle: s__('UsageTrends|Total projects & groups'),
xAxisTitle: s__('UsageTrends|Month'),
queries: [
{
query,
title: s__('UsageTrends|Total projects'),
identifier: 'PROJECTS',
loadError: s__('UsageTrends|There was an error fetching the projects. Please try again.'),
},
{
query,
title: s__('UsageTrends|Total groups'),
identifier: 'GROUPS',
loadError: s__('UsageTrends|There was an error fetching the groups. Please try again.'),
},
],
},
{
loadChartError: s__(
'UsageTrends|Could not load the pipelines chart. Please refresh the page to try again.',
),
noDataMessage,
chartTitle: s__('UsageTrends|Pipelines'),
......@@ -17,39 +40,47 @@ export default [
query,
title: s__('UsageTrends|Pipelines total'),
identifier: 'PIPELINES',
loadError: sprintf(s__('UsageTrends|There was an error fetching the total pipelines')),
loadError: s__(
'UsageTrends|There was an error fetching the total pipelines. Please try again.',
),
},
{
query,
title: s__('UsageTrends|Pipelines succeeded'),
identifier: 'PIPELINES_SUCCEEDED',
loadError: sprintf(s__('UsageTrends|There was an error fetching the successful pipelines')),
loadError: s__(
'UsageTrends|There was an error fetching the successful pipelines. Please try again.',
),
},
{
query,
title: s__('UsageTrends|Pipelines failed'),
identifier: 'PIPELINES_FAILED',
loadError: sprintf(s__('UsageTrends|There was an error fetching the failed pipelines')),
loadError: s__(
'UsageTrends|There was an error fetching the failed pipelines. Please try again.',
),
},
{
query,
title: s__('UsageTrends|Pipelines canceled'),
identifier: 'PIPELINES_CANCELED',
loadError: sprintf(s__('UsageTrends|There was an error fetching the cancelled pipelines')),
loadError: s__(
'UsageTrends|There was an error fetching the cancelled pipelines. Please try again.',
),
},
{
query,
title: s__('UsageTrends|Pipelines skipped'),
identifier: 'PIPELINES_SKIPPED',
loadError: sprintf(s__('UsageTrends|There was an error fetching the skipped pipelines')),
loadError: s__(
'UsageTrends|There was an error fetching the skipped pipelines. Please try again.',
),
},
],
},
{
loadChartError: sprintf(
s__(
'UsageTrends|Could not load the issues and merge requests chart. Please refresh the page to try again.',
),
loadChartError: s__(
'UsageTrends|Could not load the issues and merge requests chart. Please refresh the page to try again.',
),
noDataMessage,
chartTitle: s__('UsageTrends|Issues & Merge Requests'),
......@@ -60,13 +91,15 @@ export default [
query,
title: __('Issues'),
identifier: 'ISSUES',
loadError: sprintf(s__('UsageTrends|There was an error fetching the issues')),
loadError: s__('UsageTrends|There was an error fetching the issues. Please try again.'),
},
{
query,
title: __('Merge requests'),
identifier: 'MERGE_REQUESTS',
loadError: sprintf(s__('UsageTrends|There was an error fetching the merge requests')),
loadError: s__(
'UsageTrends|There was an error fetching the merge requests. Please try again.',
),
},
],
},
......
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import * as Sentry from '@sentry/browser';
import produce from 'immer';
import { sortBy } from 'lodash';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import latestGroupsQuery from '../graphql/queries/groups.query.graphql';
import latestProjectsQuery from '../graphql/queries/projects.query.graphql';
import { getAverageByMonth } from '../utils';
const sortByDate = (data) => sortBy(data, (item) => new Date(item[0]).getTime());
const averageAndSortData = (data = [], maxDataPoints) => {
const averaged = getAverageByMonth(
data.length > maxDataPoints ? data.slice(0, maxDataPoints) : data,
{ shouldRound: true },
);
return sortByDate(averaged);
};
export default {
name: 'ProjectsAndGroupsChart',
components: { GlAlert, GlLineChart, ChartSkeletonLoader },
props: {
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
totalDataPoints: {
type: Number,
required: true,
},
},
data() {
return {
loadingError: false,
errorMessage: '',
groups: [],
projects: [],
groupsPageInfo: null,
projectsPageInfo: null,
};
},
apollo: {
groups: {
query: latestGroupsQuery,
variables() {
return {
first: this.totalDataPoints,
after: null,
};
},
update(data) {
return data.groups?.nodes || [];
},
result({ data }) {
const {
groups: { pageInfo },
} = data;
this.groupsPageInfo = pageInfo;
this.fetchNextPage({
query: this.$apollo.queries.groups,
pageInfo: this.groupsPageInfo,
dataKey: 'groups',
errorMessage: this.$options.i18n.loadGroupsDataError,
});
},
error(error) {
this.handleError({
message: this.$options.i18n.loadGroupsDataError,
error,
dataKey: 'groups',
});
},
},
projects: {
query: latestProjectsQuery,
variables() {
return {
first: this.totalDataPoints,
after: null,
};
},
update(data) {
return data.projects?.nodes || [];
},
result({ data }) {
const {
projects: { pageInfo },
} = data;
this.projectsPageInfo = pageInfo;
this.fetchNextPage({
query: this.$apollo.queries.projects,
pageInfo: this.projectsPageInfo,
dataKey: 'projects',
errorMessage: this.$options.i18n.loadProjectsDataError,
});
},
error(error) {
this.handleError({
message: this.$options.i18n.loadProjectsDataError,
error,
dataKey: 'projects',
});
},
},
},
i18n: {
yAxisTitle: s__('UsageTrends|Total projects & groups'),
xAxisTitle: __('Month'),
loadChartError: s__(
'UsageTrends|Could not load the projects and groups chart. Please refresh the page to try again.',
),
loadProjectsDataError: s__('UsageTrends|There was an error while loading the projects'),
loadGroupsDataError: s__('UsageTrends|There was an error while loading the groups'),
noDataMessage: s__('UsageTrends|No data available.'),
},
computed: {
isLoadingGroups() {
return this.$apollo.queries.groups.loading || this.groupsPageInfo?.hasNextPage;
},
isLoadingProjects() {
return this.$apollo.queries.projects.loading || this.projectsPageInfo?.hasNextPage;
},
isLoading() {
return this.isLoadingProjects && this.isLoadingGroups;
},
groupChartData() {
return averageAndSortData(this.groups, this.totalDataPoints);
},
projectChartData() {
return averageAndSortData(this.projects, this.totalDataPoints);
},
hasNoData() {
const { projectChartData, groupChartData } = this;
return Boolean(!projectChartData.length && !groupChartData.length);
},
options() {
return {
xAxis: {
name: this.$options.i18n.xAxisTitle,
type: 'category',
axisLabel: {
formatter: (value) => {
return formatDateAsMonth(value);
},
},
},
yAxis: {
name: this.$options.i18n.yAxisTitle,
},
};
},
chartData() {
return [
{
name: s__('UsageTrends|Total projects'),
data: this.projectChartData,
},
{
name: s__('UsageTrends|Total groups'),
data: this.groupChartData,
},
];
},
},
methods: {
handleError({ error, message = this.$options.i18n.loadChartError, dataKey = null }) {
this.loadingError = true;
this.errorMessage = message;
if (!dataKey) {
this.projects = [];
this.groups = [];
} else {
this[dataKey] = [];
}
Sentry.captureException(error);
},
fetchNextPage({ pageInfo, query, dataKey, errorMessage }) {
if (pageInfo?.hasNextPage) {
query
.fetchMore({
variables: { first: this.totalDataPoints, after: pageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const results = produce(fetchMoreResult, (newData) => {
// eslint-disable-next-line no-param-reassign
newData[dataKey].nodes = [
...previousResult[dataKey].nodes,
...newData[dataKey].nodes,
];
});
return results;
},
})
.catch((error) => {
this.handleError({ error, message: errorMessage, dataKey });
});
}
},
},
};
</script>
<template>
<div>
<h3>{{ $options.i18n.yAxisTitle }}</h3>
<chart-skeleton-loader v-if="isLoading" />
<gl-alert v-else-if="hasNoData" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.noDataMessage }}
</gl-alert>
<div v-else>
<gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">{{
errorMessage
}}</gl-alert>
<gl-line-chart :option="options" :include-legend-avg-max="true" :data="chartData" />
</div>
</div>
</template>
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/count.fragment.graphql"
query getGroupsCount($first: Int, $after: String) {
groups: usageTrendsMeasurements(identifier: GROUPS, first: $first, after: $after) {
nodes {
...Count
}
pageInfo {
...PageInfo
}
}
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/count.fragment.graphql"
query getProjectsCount($first: Int, $after: String) {
projects: usageTrendsMeasurements(identifier: PROJECTS, first: $first, after: $after) {
nodes {
...Count
}
pageInfo {
...PageInfo
}
}
}
......@@ -32142,31 +32142,31 @@ msgstr ""
msgid "UsageTrends|Projects"
msgstr ""
msgid "UsageTrends|There was an error fetching the cancelled pipelines"
msgid "UsageTrends|There was an error fetching the cancelled pipelines. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the failed pipelines"
msgid "UsageTrends|There was an error fetching the failed pipelines. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the issues"
msgid "UsageTrends|There was an error fetching the groups. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the merge requests"
msgid "UsageTrends|There was an error fetching the issues. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the skipped pipelines"
msgid "UsageTrends|There was an error fetching the merge requests. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the successful pipelines"
msgid "UsageTrends|There was an error fetching the projects. Please try again."
msgstr ""
msgid "UsageTrends|There was an error fetching the total pipelines"
msgid "UsageTrends|There was an error fetching the skipped pipelines. Please try again."
msgstr ""
msgid "UsageTrends|There was an error while loading the groups"
msgid "UsageTrends|There was an error fetching the successful pipelines. Please try again."
msgstr ""
msgid "UsageTrends|There was an error while loading the projects"
msgid "UsageTrends|There was an error fetching the total pipelines. Please try again."
msgstr ""
msgid "UsageTrends|Total groups"
......
import { shallowMount } from '@vue/test-utils';
import UsageTrendsApp from '~/analytics/usage_trends/components/app.vue';
import ProjectsAndGroupsChart from '~/analytics/usage_trends/components/projects_and_groups_chart.vue';
import UsageCounts from '~/analytics/usage_trends/components/usage_counts.vue';
import UsageTrendsCountChart from '~/analytics/usage_trends/components/usage_trends_count_chart.vue';
import UsersChart from '~/analytics/usage_trends/components/users_chart.vue';
......@@ -25,7 +24,7 @@ describe('UsageTrendsApp', () => {
expect(wrapper.find(UsageCounts).exists()).toBe(true);
});
['Pipelines', 'Issues & Merge Requests'].forEach((usage) => {
['Total projects & groups', 'Pipelines', 'Issues & Merge Requests'].forEach((usage) => {
it(`displays the ${usage} chart`, () => {
const chartTitles = wrapper
.findAll(UsageTrendsCountChart)
......@@ -38,8 +37,4 @@ describe('UsageTrendsApp', () => {
it('displays the users chart component', () => {
expect(wrapper.find(UsersChart).exists()).toBe(true);
});
it('displays the projects and groups chart component', () => {
expect(wrapper.find(ProjectsAndGroupsChart).exists()).toBe(true);
});
});
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import ProjectsAndGroupChart from '~/analytics/usage_trends/components/projects_and_groups_chart.vue';
import groupsQuery from '~/analytics/usage_trends/graphql/queries/groups.query.graphql';
import projectsQuery from '~/analytics/usage_trends/graphql/queries/projects.query.graphql';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { mockQueryResponse } from '../apollo_mock_data';
import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('ProjectsAndGroupChart', () => {
let wrapper;
let queryResponses = { projects: null, groups: null };
const mockAdditionalData = [{ recordedAt: '2020-07-21', count: 5 }];
const createComponent = ({
loadingError = false,
projects = [],
groups = [],
projectsLoading = false,
groupsLoading = false,
projectsAdditionalData = [],
groupsAdditionalData = [],
} = {}) => {
queryResponses = {
projects: mockQueryResponse({
key: 'projects',
data: projects,
loading: projectsLoading,
additionalData: projectsAdditionalData,
}),
groups: mockQueryResponse({
key: 'groups',
data: groups,
loading: groupsLoading,
additionalData: groupsAdditionalData,
}),
};
return shallowMount(ProjectsAndGroupChart, {
props: {
startDate: new Date(2020, 9, 26),
endDate: new Date(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
localVue,
apolloProvider: createMockApollo([
[projectsQuery, queryResponses.projects],
[groupsQuery, queryResponses.groups],
]),
data() {
return { loadingError };
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
queryResponses = {
projects: null,
groups: null,
};
});
const findLoader = () => wrapper.find(ChartSkeletonLoader);
const findAlert = () => wrapper.find(GlAlert);
const findChart = () => wrapper.find(GlLineChart);
describe('while loading', () => {
beforeEach(() => {
wrapper = createComponent({ projectsLoading: true, groupsLoading: true });
});
it('displays the skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
it('hides the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('while loading 1 data set', () => {
beforeEach(async () => {
wrapper = createComponent({
projects: mockCountsData2,
groupsLoading: true,
});
await wrapper.vm.$nextTick();
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(true);
});
});
describe('without data', () => {
beforeEach(async () => {
wrapper = createComponent({ projects: [] });
await wrapper.vm.$nextTick();
});
it('renders a no data message', () => {
expect(findAlert().text()).toBe('No data available.');
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('does not render the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ projects: mockCountsData2 });
await wrapper.vm.$nextTick();
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(true);
});
it('passes the data to the line chart', () => {
expect(findChart().props('data')).toEqual([
{ data: roundedSortedCountsMonthlyChartData2, name: 'Total projects' },
{ data: [], name: 'Total groups' },
]);
});
});
describe('with errors', () => {
beforeEach(async () => {
wrapper = createComponent({ loadingError: true });
await wrapper.vm.$nextTick();
});
it('renders an error message', () => {
expect(findAlert().text()).toBe('No data available.');
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('hides the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe.each`
metric | loadingState | newData
${'projects'} | ${{ projectsAdditionalData: mockAdditionalData }} | ${{ projects: mockCountsData2 }}
${'groups'} | ${{ groupsAdditionalData: mockAdditionalData }} | ${{ groups: mockCountsData2 }}
`('$metric - fetchMore', ({ metric, loadingState, newData }) => {
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
wrapper = createComponent({
...loadingState,
...newData,
});
jest.spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore');
await wrapper.vm.$nextTick();
});
it('requests data twice', () => {
expect(queryResponses[metric]).toBeCalledTimes(2);
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1);
});
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
wrapper = createComponent({
...loadingState,
...newData,
});
jest
.spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
return wrapper.vm.$nextTick();
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1);
});
it('renders an error message', () => {
expect(findAlert().text()).toBe('No data available.');
});
});
});
});
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