summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/analytics/instance_statistics/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/analytics/instance_statistics/components')
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue24
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/charts_config.js87
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue206
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue215
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue224
5 files changed, 538 insertions, 218 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
index 7aa5c98aa0b..8df4d2e2524 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -1,19 +1,23 @@
<script>
import InstanceCounts from './instance_counts.vue';
-import PipelinesChart from './pipelines_chart.vue';
+import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue';
import UsersChart from './users_chart.vue';
+import ProjectsAndGroupsChart from './projects_and_groups_chart.vue';
+import ChartsConfig from './charts_config';
import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
export default {
name: 'InstanceStatisticsApp',
components: {
InstanceCounts,
- PipelinesChart,
+ InstanceStatisticsCountChart,
UsersChart,
+ ProjectsAndGroupsChart,
},
TOTAL_DAYS_TO_SHOW,
START_DATE,
TODAY,
+ configs: ChartsConfig,
};
</script>
@@ -25,6 +29,20 @@ export default {
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
- <pipelines-chart />
+ <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"
+ :queries="chartOptions.queries"
+ :x-axis-title="chartOptions.xAxisTitle"
+ :y-axis-title="chartOptions.yAxisTitle"
+ :load-chart-error-message="chartOptions.loadChartError"
+ :no-data-message="chartOptions.noDataMessage"
+ :chart-title="chartOptions.chartTitle"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js b/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js
new file mode 100644
index 00000000000..6fba3c56cfe
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/charts_config.js
@@ -0,0 +1,87 @@
+import { s__, __, sprintf } from '~/locale';
+import query from '../graphql/queries/instance_count.query.graphql';
+
+const noDataMessage = s__('InstanceStatistics|No data available.');
+
+export default [
+ {
+ loadChartError: sprintf(
+ s__(
+ 'InstanceStatistics|Could not load the pipelines chart. Please refresh the page to try again.',
+ ),
+ ),
+ noDataMessage,
+ chartTitle: s__('InstanceStatistics|Pipelines'),
+ yAxisTitle: s__('InstanceStatistics|Items'),
+ xAxisTitle: s__('InstanceStatistics|Month'),
+ queries: [
+ {
+ query,
+ title: s__('InstanceStatistics|Pipelines total'),
+ identifier: 'PIPELINES',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the total pipelines'),
+ ),
+ },
+ {
+ query,
+ title: s__('InstanceStatistics|Pipelines succeeded'),
+ identifier: 'PIPELINES_SUCCEEDED',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the successful pipelines'),
+ ),
+ },
+ {
+ query,
+ title: s__('InstanceStatistics|Pipelines failed'),
+ identifier: 'PIPELINES_FAILED',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the failed pipelines'),
+ ),
+ },
+ {
+ query,
+ title: s__('InstanceStatistics|Pipelines canceled'),
+ identifier: 'PIPELINES_CANCELED',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the cancelled pipelines'),
+ ),
+ },
+ {
+ query,
+ title: s__('InstanceStatistics|Pipelines skipped'),
+ identifier: 'PIPELINES_SKIPPED',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the skipped pipelines'),
+ ),
+ },
+ ],
+ },
+ {
+ loadChartError: sprintf(
+ s__(
+ 'InstanceStatistics|Could not load the issues and merge requests chart. Please refresh the page to try again.',
+ ),
+ ),
+ noDataMessage,
+ chartTitle: s__('InstanceStatistics|Issues & Merge Requests'),
+ yAxisTitle: s__('InstanceStatistics|Items'),
+ xAxisTitle: s__('InstanceStatistics|Month'),
+ queries: [
+ {
+ query,
+ title: __('Issues'),
+ identifier: 'ISSUES',
+ loadError: sprintf(s__('InstanceStatistics|There was an error fetching the issues')),
+ },
+ {
+ query,
+ title: __('Merge requests'),
+ identifier: 'MERGE_REQUESTS',
+ loadError: sprintf(
+ s__('InstanceStatistics|There was an error fetching the merge requests'),
+ ),
+ },
+ ],
+ },
+];
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
new file mode 100644
index 00000000000..a9bd1bb2f41
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
@@ -0,0 +1,206 @@
+<script>
+import { GlLineChart } from '@gitlab/ui/dist/charts';
+import { GlAlert } from '@gitlab/ui';
+import { some, every } from 'lodash';
+import * as Sentry from '~/sentry/wrapper';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
+import {
+ differenceInMonths,
+ formatDateAsMonth,
+ getDayDifference,
+} from '~/lib/utils/datetime_utility';
+import { getAverageByMonth, getEarliestDate, generateDataKeys } from '../utils';
+import { TODAY, START_DATE } from '../constants';
+
+const QUERY_DATA_KEY = 'instanceStatisticsMeasurements';
+
+export default {
+ name: 'InstanceStatisticsCountChart',
+ components: {
+ GlLineChart,
+ GlAlert,
+ ChartSkeletonLoader,
+ },
+ startDate: START_DATE,
+ endDate: TODAY,
+ props: {
+ chartTitle: {
+ type: String,
+ required: true,
+ },
+ loadChartErrorMessage: {
+ type: String,
+ required: true,
+ },
+ noDataMessage: {
+ type: String,
+ required: true,
+ },
+ xAxisTitle: {
+ type: String,
+ required: true,
+ },
+ yAxisTitle: {
+ type: String,
+ required: true,
+ },
+ queries: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ errors: { ...generateDataKeys(this.queries, '') },
+ ...generateDataKeys(this.queries, []),
+ };
+ },
+ computed: {
+ errorMessages() {
+ return Object.values(this.errors);
+ },
+ isLoading() {
+ return some(this.$apollo.queries, query => query?.loading);
+ },
+ allQueriesFailed() {
+ return every(this.errorMessages, message => message.length);
+ },
+ hasLoadingErrors() {
+ return some(this.errorMessages, message => message.length);
+ },
+ errorMessage() {
+ // show the generic loading message if all requests fail
+ return this.allQueriesFailed ? this.loadChartErrorMessage : this.errorMessages.join('\n\n');
+ },
+ hasEmptyDataSet() {
+ return this.chartData.every(({ data }) => data.length === 0);
+ },
+ totalDaysToShow() {
+ return getDayDifference(this.$options.startDate, this.$options.endDate);
+ },
+ chartData() {
+ const options = { shouldRound: true };
+ return this.queries.map(({ identifier, title }) => ({
+ name: title,
+ data: getAverageByMonth(this[identifier]?.nodes, options),
+ }));
+ },
+ range() {
+ return {
+ min: this.$options.startDate,
+ max: this.$options.endDate,
+ };
+ },
+ chartOptions() {
+ const { endDate, startDate } = this.$options;
+ return {
+ xAxis: {
+ ...this.range,
+ name: this.xAxisTitle,
+ type: 'time',
+ splitNumber: differenceInMonths(startDate, endDate) + 1,
+ axisLabel: {
+ interval: 0,
+ showMinLabel: false,
+ showMaxLabel: false,
+ align: 'right',
+ formatter: formatDateAsMonth,
+ },
+ },
+ yAxis: {
+ name: this.yAxisTitle,
+ },
+ };
+ },
+ },
+ created() {
+ this.queries.forEach(({ query, identifier, loadError }) => {
+ this.$apollo.addSmartQuery(identifier, {
+ query,
+ variables() {
+ return {
+ identifier,
+ first: this.totalDaysToShow,
+ after: null,
+ };
+ },
+ update(data) {
+ const { nodes = [], pageInfo } = data[QUERY_DATA_KEY] || {};
+ return {
+ nodes,
+ pageInfo,
+ };
+ },
+ result() {
+ const { pageInfo, nodes } = this[identifier];
+ if (pageInfo?.hasNextPage && this.calculateDaysToFetch(getEarliestDate(nodes)) > 0) {
+ this.fetchNextPage({
+ query: this.$apollo.queries[identifier],
+ errorMessage: loadError,
+ pageInfo,
+ identifier,
+ });
+ }
+ },
+ error(error) {
+ this.handleError({
+ message: loadError,
+ identifier,
+ error,
+ });
+ },
+ });
+ });
+ },
+ methods: {
+ calculateDaysToFetch(firstDataPointDate = null) {
+ return firstDataPointDate
+ ? Math.max(0, getDayDifference(this.$options.startDate, new Date(firstDataPointDate)))
+ : 0;
+ },
+ handleError({ identifier, error, message }) {
+ this.loadingError = true;
+ this.errors = { ...this.errors, [identifier]: message };
+ Sentry.captureException(error);
+ },
+ fetchNextPage({ query, pageInfo, identifier, errorMessage }) {
+ query
+ .fetchMore({
+ variables: {
+ identifier,
+ first: this.calculateDaysToFetch(getEarliestDate(this[identifier].nodes)),
+ after: pageInfo.endCursor,
+ },
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ const { nodes, ...rest } = fetchMoreResult[QUERY_DATA_KEY];
+ const { nodes: previousNodes } = previousResult[QUERY_DATA_KEY];
+ return {
+ [QUERY_DATA_KEY]: { ...rest, nodes: [...previousNodes, ...nodes] },
+ };
+ },
+ })
+ .catch(error => this.handleError({ identifier, error, message: errorMessage }));
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h3>{{ chartTitle }}</h3>
+ <gl-alert v-if="hasLoadingErrors" variant="danger" :dismissible="false" class="gl-mt-3">
+ {{ errorMessage }}
+ </gl-alert>
+ <div v-if="!allQueriesFailed">
+ <chart-skeleton-loader v-if="isLoading" />
+ <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3">
+ {{ noDataMessage }}
+ </gl-alert>
+ <gl-line-chart
+ v-else
+ :option="chartOptions"
+ :include-legend-avg-max="true"
+ :data="chartData"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
deleted file mode 100644
index b16d960402b..00000000000
--- a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
+++ /dev/null
@@ -1,215 +0,0 @@
-<script>
-import { GlLineChart } from '@gitlab/ui/dist/charts';
-import { GlAlert } from '@gitlab/ui';
-import { mapKeys, mapValues, pick, some, sum } from 'lodash';
-import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
-import { s__ } from '~/locale';
-import {
- differenceInMonths,
- formatDateAsMonth,
- getDayDifference,
-} from '~/lib/utils/datetime_utility';
-import { getAverageByMonth, sortByDate, extractValues } from '../utils';
-import pipelineStatsQuery from '../graphql/queries/pipeline_stats.query.graphql';
-import { TODAY, START_DATE } from '../constants';
-
-const DATA_KEYS = [
- 'pipelinesTotal',
- 'pipelinesSucceeded',
- 'pipelinesFailed',
- 'pipelinesCanceled',
- 'pipelinesSkipped',
-];
-const PREFIX = 'pipelines';
-
-export default {
- name: 'PipelinesChart',
- components: {
- GlLineChart,
- GlAlert,
- ChartSkeletonLoader,
- },
- startDate: START_DATE,
- endDate: TODAY,
- i18n: {
- loadPipelineChartError: s__(
- 'InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again.',
- ),
- noDataMessage: s__('InstanceAnalytics|There is no data available.'),
- total: s__('InstanceAnalytics|Total'),
- succeeded: s__('InstanceAnalytics|Succeeded'),
- failed: s__('InstanceAnalytics|Failed'),
- canceled: s__('InstanceAnalytics|Canceled'),
- skipped: s__('InstanceAnalytics|Skipped'),
- chartTitle: s__('InstanceAnalytics|Pipelines'),
- yAxisTitle: s__('InstanceAnalytics|Items'),
- xAxisTitle: s__('InstanceAnalytics|Month'),
- },
- data() {
- return {
- loading: true,
- loadingError: null,
- };
- },
- apollo: {
- pipelineStats: {
- query: pipelineStatsQuery,
- variables() {
- return {
- firstTotal: this.totalDaysToShow,
- firstSucceeded: this.totalDaysToShow,
- firstFailed: this.totalDaysToShow,
- firstCanceled: this.totalDaysToShow,
- firstSkipped: this.totalDaysToShow,
- };
- },
- update(data) {
- const allData = extractValues(data, DATA_KEYS, PREFIX, 'nodes');
- const allPageInfo = extractValues(data, DATA_KEYS, PREFIX, 'pageInfo');
-
- return {
- ...mapValues(allData, sortByDate),
- ...allPageInfo,
- };
- },
- result() {
- if (this.hasNextPage) {
- this.fetchNextPage();
- }
- },
- error() {
- this.handleError();
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.pipelineStats.loading;
- },
- totalDaysToShow() {
- return getDayDifference(this.$options.startDate, this.$options.endDate);
- },
- firstVariables() {
- const allData = pick(this.pipelineStats, [
- 'nodesTotal',
- 'nodesSucceeded',
- 'nodesFailed',
- 'nodesCanceled',
- 'nodesSkipped',
- ]);
- const allDayDiffs = mapValues(allData, data => {
- const firstdataPoint = data[0];
- if (!firstdataPoint) {
- return 0;
- }
-
- return Math.max(
- 0,
- getDayDifference(this.$options.startDate, new Date(firstdataPoint.recordedAt)),
- );
- });
-
- return mapKeys(allDayDiffs, (value, key) => key.replace('nodes', 'first'));
- },
- cursorVariables() {
- const pageInfoKeys = [
- 'pageInfoTotal',
- 'pageInfoSucceeded',
- 'pageInfoFailed',
- 'pageInfoCanceled',
- 'pageInfoSkipped',
- ];
-
- return extractValues(this.pipelineStats, pageInfoKeys, 'pageInfo', 'endCursor');
- },
- hasNextPage() {
- return (
- sum(Object.values(this.firstVariables)) > 0 &&
- some(this.pipelineStats, ({ hasNextPage }) => hasNextPage)
- );
- },
- hasEmptyDataSet() {
- return this.chartData.every(({ data }) => data.length === 0);
- },
- chartData() {
- const allData = pick(this.pipelineStats, [
- 'nodesTotal',
- 'nodesSucceeded',
- 'nodesFailed',
- 'nodesCanceled',
- 'nodesSkipped',
- ]);
- const options = { shouldRound: true };
- return Object.keys(allData).map(key => {
- const i18nName = key.slice('nodes'.length).toLowerCase();
- return {
- name: this.$options.i18n[i18nName],
- data: getAverageByMonth(allData[key], options),
- };
- });
- },
- range() {
- return {
- min: this.$options.startDate,
- max: this.$options.endDate,
- };
- },
- chartOptions() {
- const { endDate, startDate, i18n } = this.$options;
- return {
- xAxis: {
- ...this.range,
- name: i18n.xAxisTitle,
- type: 'time',
- splitNumber: differenceInMonths(startDate, endDate) + 1,
- axisLabel: {
- interval: 0,
- showMinLabel: false,
- showMaxLabel: false,
- align: 'right',
- formatter: formatDateAsMonth,
- },
- },
- yAxis: {
- name: i18n.yAxisTitle,
- },
- };
- },
- },
- methods: {
- handleError() {
- this.loadingError = true;
- },
- fetchNextPage() {
- this.$apollo.queries.pipelineStats
- .fetchMore({
- variables: {
- ...this.firstVariables,
- ...this.cursorVariables,
- },
- updateQuery: (previousResult, { fetchMoreResult }) => {
- return Object.keys(fetchMoreResult).reduce((memo, key) => {
- const { nodes, ...rest } = fetchMoreResult[key];
- const previousNodes = previousResult[key].nodes;
- return { ...memo, [key]: { ...rest, nodes: [...previousNodes, ...nodes] } };
- }, {});
- },
- })
- .catch(this.handleError);
- },
- },
-};
-</script>
-<template>
- <div>
- <h3>{{ $options.i18n.chartTitle }}</h3>
- <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
- {{ this.$options.i18n.loadPipelineChartError }}
- </gl-alert>
- <chart-skeleton-loader v-else-if="isLoading" />
- <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3">
- {{ $options.i18n.noDataMessage }}
- </gl-alert>
- <gl-line-chart v-else :option="chartOptions" :include-legend-avg-max="true" :data="chartData" />
- </div>
-</template>
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>