diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-22 18:09:01 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-22 18:09:01 +0000 |
commit | b3c281c8c7109b3286a505d29330926e59139009 (patch) | |
tree | ccad33a8106da31a7f1ad6ea71250e2a80719674 /app/assets/javascripts/analytics | |
parent | 9ed2b33fff06f930bd7445b3316cbe9933c48ea0 (diff) | |
download | gitlab-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.vue | 61 | ||||
-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.graphql | 34 | ||||
-rw-r--r-- | app/assets/javascripts/analytics/instance_statistics/utils.js | 24 |
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 }; + }, {}); } /** |