diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-02 18:08:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-02 18:08:52 +0300 |
commit | 215cb099344f3e59304064c9fffea9c3489d31c0 (patch) | |
tree | 186585d360e44aefed78106e7b23b8ae726f547e /app/assets/javascripts/analytics | |
parent | c80b69a93fb4a6708b3337aa6e243ad09fe1d295 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/analytics')
4 files changed, 257 insertions, 0 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue index 2533854119f..abe9b45ed79 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue @@ -5,6 +5,7 @@ import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue' import UsersChart from './users_chart.vue'; import pipelinesStatsQuery from '../graphql/queries/pipeline_stats.query.graphql'; import issuesAndMergeRequestsQuery from '../graphql/queries/issues_and_merge_requests.query.graphql'; +import ProjectsAndGroupsChart from './projects_and_groups_chart.vue'; import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants'; const PIPELINES_KEY_TO_NAME_MAP = { @@ -32,6 +33,7 @@ export default { InstanceCounts, InstanceStatisticsCountChart, UsersChart, + ProjectsAndGroupsChart, }, TOTAL_DAYS_TO_SHOW, START_DATE, @@ -69,6 +71,11 @@ 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" + /> <instance-statistics-count-chart v-for="chartOptions in $options.configs" :key="chartOptions.chartTitle" diff --git a/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue new file mode 100644 index 00000000000..e8e35c22fe1 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue @@ -0,0 +1,224 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import produce from 'immer'; +import { sortBy } from 'lodash'; +import * as Sentry from '~/sentry/wrapper'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import { s__, __ } from '~/locale'; +import { formatDateAsMonth } from '~/lib/utils/datetime_utility'; +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__('InstanceStatistics|Total projects & groups'), + xAxisTitle: __('Month'), + loadChartError: s__( + 'InstanceStatistics|Could not load the projects and groups chart. Please refresh the page to try again.', + ), + loadProjectsDataError: s__('InstanceStatistics|There was an error while loading the projects'), + loadGroupsDataError: s__('InstanceStatistics|There was an error while loading the groups'), + noDataMessage: s__('InstanceStatistics|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__('InstanceStatistics|Total projects'), + data: this.projectChartData, + }, + { + name: s__('InstanceStatistics|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> diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/groups.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/groups.query.graphql new file mode 100644 index 00000000000..ec56d91ffaa --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/groups.query.graphql @@ -0,0 +1,13 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "../fragments/count.fragment.graphql" + +query getGroupsCount($first: Int, $after: String) { + groups: instanceStatisticsMeasurements(identifier: GROUPS, first: $first, after: $after) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/projects.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/projects.query.graphql new file mode 100644 index 00000000000..0845b703435 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/projects.query.graphql @@ -0,0 +1,13 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "../fragments/count.fragment.graphql" + +query getProjectsCount($first: Int, $after: String) { + projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: $first, after: $after) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} |