summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/analytics
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-22 18:09:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-22 18:09:01 +0000
commitb3c281c8c7109b3286a505d29330926e59139009 (patch)
treeccad33a8106da31a7f1ad6ea71250e2a80719674 /app/assets/javascripts/analytics
parent9ed2b33fff06f930bd7445b3316cbe9933c48ea0 (diff)
downloadgitlab-ce-b3c281c8c7109b3286a505d29330926e59139009.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/analytics')
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue61
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue (renamed from app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue)166
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql34
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/utils.js24
4 files changed, 198 insertions, 87 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..2533854119f 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -1,19 +1,63 @@
<script>
+import { s__ } from '~/locale';
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 pipelinesStatsQuery from '../graphql/queries/pipeline_stats.query.graphql';
+import issuesAndMergeRequestsQuery from '../graphql/queries/issues_and_merge_requests.query.graphql';
import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
+const PIPELINES_KEY_TO_NAME_MAP = {
+ total: s__('InstanceAnalytics|Total'),
+ succeeded: s__('InstanceAnalytics|Succeeded'),
+ failed: s__('InstanceAnalytics|Failed'),
+ canceled: s__('InstanceAnalytics|Canceled'),
+ skipped: s__('InstanceAnalytics|Skipped'),
+};
+const ISSUES_AND_MERGE_REQUESTS_KEY_TO_NAME_MAP = {
+ issues: s__('InstanceAnalytics|Issues'),
+ mergeRequests: s__('InstanceAnalytics|Merge Requests'),
+};
+const loadPipelineChartError = s__(
+ 'InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again.',
+);
+const loadIssuesAndMergeRequestsChartError = s__(
+ 'InstanceAnalytics|Could not load the issues and merge requests chart. Please refresh the page to try again.',
+);
+const noDataMessage = s__('InstanceAnalytics|There is no data available.');
+
export default {
name: 'InstanceStatisticsApp',
components: {
InstanceCounts,
- PipelinesChart,
+ InstanceStatisticsCountChart,
UsersChart,
},
TOTAL_DAYS_TO_SHOW,
START_DATE,
TODAY,
+ configs: [
+ {
+ keyToNameMap: PIPELINES_KEY_TO_NAME_MAP,
+ prefix: 'pipelines',
+ loadChartError: loadPipelineChartError,
+ noDataMessage,
+ chartTitle: s__('InstanceAnalytics|Pipelines'),
+ yAxisTitle: s__('InstanceAnalytics|Items'),
+ xAxisTitle: s__('InstanceAnalytics|Month'),
+ query: pipelinesStatsQuery,
+ },
+ {
+ keyToNameMap: ISSUES_AND_MERGE_REQUESTS_KEY_TO_NAME_MAP,
+ prefix: 'issuesAndMergeRequests',
+ loadChartError: loadIssuesAndMergeRequestsChartError,
+ noDataMessage,
+ chartTitle: s__('InstanceAnalytics|Issues & Merge Requests'),
+ yAxisTitle: s__('InstanceAnalytics|Items'),
+ xAxisTitle: s__('InstanceAnalytics|Month'),
+ query: issuesAndMergeRequestsQuery,
+ },
+ ],
};
</script>
@@ -25,6 +69,17 @@ export default {
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
- <pipelines-chart />
+ <instance-statistics-count-chart
+ v-for="chartOptions in $options.configs"
+ :key="chartOptions.chartTitle"
+ :prefix="chartOptions.prefix"
+ :key-to-name-map="chartOptions.keyToNameMap"
+ :query="chartOptions.query"
+ :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/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
index b16d960402b..740af834618 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
@@ -1,29 +1,19 @@
<script>
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
-import { mapKeys, mapValues, pick, some, sum } from 'lodash';
+import { mapValues, 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 { convertToTitleCase } from '~/lib/utils/text_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',
+ name: 'InstanceStatisticsCountChart',
components: {
GlLineChart,
GlAlert,
@@ -31,19 +21,42 @@ export default {
},
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'),
+ dataKey: 'nodes',
+ pageInfoKey: 'pageInfo',
+ firstKey: 'first',
+ props: {
+ prefix: {
+ type: String,
+ required: true,
+ },
+ keyToNameMap: {
+ type: Object,
+ required: true,
+ },
+ 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,
+ },
+ query: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -53,19 +66,23 @@ export default {
},
apollo: {
pipelineStats: {
- query: pipelineStatsQuery,
+ query() {
+ return this.query;
+ },
variables() {
- return {
- firstTotal: this.totalDaysToShow,
- firstSucceeded: this.totalDaysToShow,
- firstFailed: this.totalDaysToShow,
- firstCanceled: this.totalDaysToShow,
- firstSkipped: this.totalDaysToShow,
- };
+ return this.nameKeys.reduce((memo, key) => {
+ const firstKey = `${this.$options.firstKey}${convertToTitleCase(key)}`;
+ return { ...memo, [firstKey]: this.totalDaysToShow };
+ }, {});
},
update(data) {
- const allData = extractValues(data, DATA_KEYS, PREFIX, 'nodes');
- const allPageInfo = extractValues(data, DATA_KEYS, PREFIX, 'pageInfo');
+ const allData = extractValues(data, this.nameKeys, this.prefix, this.$options.dataKey);
+ const allPageInfo = extractValues(
+ data,
+ this.nameKeys,
+ this.prefix,
+ this.$options.pageInfoKey,
+ );
return {
...mapValues(allData, sortByDate),
@@ -83,6 +100,9 @@ export default {
},
},
computed: {
+ nameKeys() {
+ return Object.keys(this.keyToNameMap);
+ },
isLoading() {
return this.$apollo.queries.pipelineStats.loading;
},
@@ -90,37 +110,35 @@ export default {
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;
+ const firstDataPoints = extractValues(
+ this.pipelineStats,
+ this.nameKeys,
+ this.$options.dataKey,
+ '[0].recordedAt',
+ { renameKey: this.$options.firstKey },
+ );
+
+ return Object.keys(firstDataPoints).reduce((memo, name) => {
+ const recordedAt = firstDataPoints[name];
+ if (!recordedAt) {
+ return { ...memo, [name]: 0 };
}
- return Math.max(
+ const numberOfDays = Math.max(
0,
- getDayDifference(this.$options.startDate, new Date(firstdataPoint.recordedAt)),
+ getDayDifference(this.$options.startDate, new Date(recordedAt)),
);
- });
- return mapKeys(allDayDiffs, (value, key) => key.replace('nodes', 'first'));
+ return { ...memo, [name]: numberOfDays };
+ }, {});
},
cursorVariables() {
- const pageInfoKeys = [
- 'pageInfoTotal',
- 'pageInfoSucceeded',
- 'pageInfoFailed',
- 'pageInfoCanceled',
- 'pageInfoSkipped',
- ];
-
- return extractValues(this.pipelineStats, pageInfoKeys, 'pageInfo', 'endCursor');
+ return extractValues(
+ this.pipelineStats,
+ this.nameKeys,
+ this.$options.pageInfoKey,
+ 'endCursor',
+ );
},
hasNextPage() {
return (
@@ -132,19 +150,13 @@ export default {
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 this.nameKeys.map(key => {
+ const dataKey = `${this.$options.dataKey}${convertToTitleCase(key)}`;
return {
- name: this.$options.i18n[i18nName],
- data: getAverageByMonth(allData[key], options),
+ name: this.keyToNameMap[key],
+ data: getAverageByMonth(this.pipelineStats?.[dataKey], options),
};
});
},
@@ -155,11 +167,11 @@ export default {
};
},
chartOptions() {
- const { endDate, startDate, i18n } = this.$options;
+ const { endDate, startDate } = this.$options;
return {
xAxis: {
...this.range,
- name: i18n.xAxisTitle,
+ name: this.xAxisTitle,
type: 'time',
splitNumber: differenceInMonths(startDate, endDate) + 1,
axisLabel: {
@@ -171,7 +183,7 @@ export default {
},
},
yAxis: {
- name: i18n.yAxisTitle,
+ name: this.yAxisTitle,
},
};
},
@@ -202,13 +214,13 @@ export default {
</script>
<template>
<div>
- <h3>{{ $options.i18n.chartTitle }}</h3>
+ <h3>{{ chartTitle }}</h3>
<gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
- {{ this.$options.i18n.loadPipelineChartError }}
+ {{ loadChartErrorMessage }}
</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 }}
+ {{ noDataMessage }}
</gl-alert>
<gl-line-chart v-else :option="chartOptions" :include-legend-avg-max="true" :data="chartData" />
</div>
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql
new file mode 100644
index 00000000000..96f21403b34
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/issues_and_merge_requests.query.graphql
@@ -0,0 +1,34 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "./count.fragment.graphql"
+
+query issuesAndMergeRequests(
+ $firstIssues: Int
+ $firstMergeRequests: Int
+ $endCursorIssues: String
+ $endCursorMergeRequests: String
+) {
+ issuesAndMergeRequestsIssues: instanceStatisticsMeasurements(
+ identifier: ISSUES
+ first: $firstIssues
+ after: $endCursorIssues
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ issuesAndMergeRequestsMergeRequests: instanceStatisticsMeasurements(
+ identifier: MERGE_REQUESTS
+ first: $firstMergeRequests
+ after: $endCursorMergeRequests
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js
index 907482c0c72..eef66165945 100644
--- a/app/assets/javascripts/analytics/instance_statistics/utils.js
+++ b/app/assets/javascripts/analytics/instance_statistics/utils.js
@@ -1,6 +1,7 @@
import { masks } from 'dateformat';
-import { mapKeys, mapValues, pick, sortBy } from 'lodash';
+import { get, sortBy } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
+import { convertToTitleCase } from '~/lib/utils/text_utility';
const { isoDate } = masks;
@@ -46,16 +47,25 @@ export function getAverageByMonth(items = [], options = {}) {
* const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' };
* extractValues(data, ['fooBar'], 'foo', 'baz') => { bazBar: 'quis' }
* @param {Object} data set to extract values from
- * @param {Array} dataKeys keys describing where to look for values in the data set
- * @param {String} replaceKey name key to be replaced in the data set
+ * @param {Array} nameKeys keys describing where to look for values in the data set
+ * @param {String} dataPrefix prefix to `nameKey` on where to get the data
* @param {String} nestedKey key nested in the data set to be extracted,
* this is also used to rename the newly created data set
+ * @param {Object} options
+ * @param {String} options.renameKey? optional rename key, if not provided nestedKey will be used
* @return {Object} the newly created data set with the extracted values
*/
-export function extractValues(data, dataKeys = [], replaceKey, nestedKey) {
- return mapKeys(pick(mapValues(data, nestedKey), dataKeys), (value, key) =>
- key.replace(replaceKey, nestedKey),
- );
+export function extractValues(data, nameKeys = [], dataPrefix, nestedKey, options = {}) {
+ const { renameKey = nestedKey } = options;
+
+ return nameKeys.reduce((memo, name) => {
+ const titelCaseName = convertToTitleCase(name);
+ const dataKey = `${dataPrefix}${titelCaseName}`;
+ const newKey = `${renameKey}${titelCaseName}`;
+ const itemData = get(data[dataKey], nestedKey);
+
+ return { ...memo, [newKey]: itemData };
+ }, {});
}
/**