Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-16 21:09:04 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-16 21:09:04 +0300
commitb58ab6c33c0369e402109d5388d4f6f73b7eb2bb (patch)
treeb4f09ac9cf03dd11328050ab1e26df5fad351695 /app/assets/javascripts/analytics
parent3940f59a61a749824aa4425ebdcaed6f3ed601f2 (diff)
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.vue7
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue216
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/constants.js5
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql4
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql76
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/utils.js29
6 files changed, 336 insertions, 1 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
index eb0b67a1629..64c1a2565be 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -1,14 +1,19 @@
<script>
import InstanceCounts from './instance_counts.vue';
+import PipelinesChart from './pipelines_chart.vue';
export default {
name: 'InstanceStatisticsApp',
components: {
InstanceCounts,
+ PipelinesChart,
},
};
</script>
<template>
- <instance-counts />
+ <div>
+ <instance-counts />
+ <pipelines-chart />
+ </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
new file mode 100644
index 00000000000..279fcfe736f
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
@@ -0,0 +1,216 @@
+<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 { 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,
+ };
+ },
+ differenceInMonths() {
+ const yearDiff = this.$options.endDate.getYear() - this.$options.startDate.getYear();
+ const monthDiff = this.$options.endDate.getMonth() - this.$options.startDate.getMonth();
+
+ return monthDiff + 12 * yearDiff;
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ ...this.range,
+ name: this.$options.i18n.xAxisTitle,
+ type: 'time',
+ splitNumber: this.differenceInMonths + 1,
+ axisLabel: {
+ interval: 0,
+ showMinLabel: false,
+ showMaxLabel: false,
+ align: 'right',
+ formatter: formatDateAsMonth,
+ },
+ },
+ yAxis: {
+ name: this.$options.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/constants.js b/app/assets/javascripts/analytics/instance_statistics/constants.js
new file mode 100644
index 00000000000..5ea5d17c974
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/constants.js
@@ -0,0 +1,5 @@
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+
+const TOTAL_DAYS_TO_SHOW = 365;
+export const TODAY = new Date();
+export const START_DATE = getDateInPast(TODAY, TOTAL_DAYS_TO_SHOW);
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql
new file mode 100644
index 00000000000..40cef95c2e7
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Count on InstanceStatisticsMeasurement {
+ count
+ recordedAt
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql
new file mode 100644
index 00000000000..3bf40403f91
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql
@@ -0,0 +1,76 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "./count.fragment.graphql"
+
+query pipelineStats(
+ $firstTotal: Int
+ $firstSucceeded: Int
+ $firstFailed: Int
+ $firstCanceled: Int
+ $firstSkipped: Int
+ $endCursorTotal: String
+ $endCursorSucceeded: String
+ $endCursorFailed: String
+ $endCursorCanceled: String
+ $endCursorSkipped: String
+) {
+ pipelinesTotal: instanceStatisticsMeasurements(
+ identifier: PIPELINES
+ first: $firstTotal
+ after: $endCursorTotal
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesSucceeded: instanceStatisticsMeasurements(
+ identifier: PIPELINES_SUCCEEDED
+ first: $firstSucceeded
+ after: $endCursorSucceeded
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesFailed: instanceStatisticsMeasurements(
+ identifier: PIPELINES_FAILED
+ first: $firstFailed
+ after: $endCursorFailed
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesCanceled: instanceStatisticsMeasurements(
+ identifier: PIPELINES_CANCELED
+ first: $firstCanceled
+ after: $endCursorCanceled
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesSkipped: instanceStatisticsMeasurements(
+ identifier: PIPELINES_SKIPPED
+ first: $firstSkipped
+ after: $endCursorSkipped
+ ) {
+ 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 30c6205b7ff..907482c0c72 100644
--- a/app/assets/javascripts/analytics/instance_statistics/utils.js
+++ b/app/assets/javascripts/analytics/instance_statistics/utils.js
@@ -1,4 +1,5 @@
import { masks } from 'dateformat';
+import { mapKeys, mapValues, pick, sortBy } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
const { isoDate } = masks;
@@ -38,3 +39,31 @@ export function getAverageByMonth(items = [], options = {}) {
return [month, avg];
});
}
+
+/**
+ * Extracts values given a data set and a set of keys
+ * @example
+ * 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 {String} nestedKey key nested in the data set to be extracted,
+ * this is also used to rename the newly created data set
+ * @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),
+ );
+}
+
+/**
+ * Creates a new array of items sorted by the date string of each item
+ * @param {Array} items [description]
+ * @param {String} items[0] date string
+ * @return {Array} the new sorted array.
+ */
+export function sortByDate(items = []) {
+ return sortBy(items, ({ recordedAt }) => new Date(recordedAt).getTime());
+}