diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-10 00:09:43 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-10 00:09:43 +0300 |
commit | 03d56c8af04d2982aff573b78f08192b07107c5b (patch) | |
tree | 6c9d2a9ffe63565d074a41397f06be0d22f9a8f4 /app/assets/javascripts/projects | |
parent | 9b09561f47159655d05171b4bee980c669859864 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/projects')
7 files changed, 441 insertions, 49 deletions
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index c6e2b2e1140..8b5bed08931 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,66 +1,202 @@ <script> import dateFormat from 'dateformat'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { __, sprintf } from '~/locale'; +import { GlAlert } from '@gitlab/ui'; +import { __, s__, sprintf } from '~/locale'; import { getDateInPast } from '~/lib/utils/datetime_utility'; +import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; +import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; import StatisticsList from './statistics_list.vue'; import PipelinesAreaChart from './pipelines_area_chart.vue'; import { CHART_CONTAINER_HEIGHT, - INNER_CHART_HEIGHT, - X_AXIS_LABEL_ROTATION, - X_AXIS_TITLE_OFFSET, CHART_DATE_FORMAT, + DEFAULT, + INNER_CHART_HEIGHT, + LOAD_ANALYTICS_FAILURE, + LOAD_PIPELINES_FAILURE, ONE_WEEK_AGO_DAYS, ONE_MONTH_AGO_DAYS, + PARSE_FAILURE, + UNSUPPORTED_DATA, + X_AXIS_LABEL_ROTATION, + X_AXIS_TITLE_OFFSET, } from '../constants'; +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; + +const defaultAnalyticsValues = { + weekPipelinesTotals: [], + weekPipelinesLabels: [], + weekPipelinesSuccessful: [], + monthPipelinesLabels: [], + monthPipelinesTotals: [], + monthPipelinesSuccessful: [], + yearPipelinesLabels: [], + yearPipelinesTotals: [], + yearPipelinesSuccessful: [], + pipelineTimesLabels: [], + pipelineTimesValues: [], +}; + export default { components: { - StatisticsList, + GlAlert, GlColumnChart, + StatisticsList, PipelinesAreaChart, }, - props: { - counts: { - type: Object, - required: true, - }, - timesChartData: { - type: Object, - required: true, - }, - lastWeekChartData: { - type: Object, - required: true, - }, - lastMonthChartData: { - type: Object, - required: true, - }, - lastYearChartData: { - type: Object, - required: true, + inject: { + projectPath: { + type: String, + default: '', }, }, data() { return { - timesChartTransformedData: [ - { - name: 'full', - data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), - }, - ], + counts: { + ...defaultCountValues, + }, + analytics: { + ...defaultAnalyticsValues, + }, + showFailureAlert: false, + failureType: null, }; }, + apollo: { + counts: { + query: getPipelineCountByStatus, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project; + }, + error() { + this.reportFailure(LOAD_PIPELINES_FAILURE); + }, + }, + analytics: { + query: getProjectPipelineStatistics, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project?.pipelineAnalytics; + }, + error() { + this.reportFailure(LOAD_ANALYTICS_FAILURE); + }, + }, + }, computed: { + failure() { + switch (this.failureType) { + case LOAD_ANALYTICS_FAILURE: + return { + text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE], + variant: 'danger', + }; + case PARSE_FAILURE: + return { + text: this.$options.errorTexts[PARSE_FAILURE], + variant: 'danger', + }; + case UNSUPPORTED_DATA: + return { + text: this.$options.errorTexts[UNSUPPORTED_DATA], + variant: 'info', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } + }, + successRatio() { + const { successfulPipelines, failedPipelines } = this.counts; + const successfulCount = successfulPipelines?.count; + const failedCount = failedPipelines?.count; + const ratio = (successfulCount / (successfulCount + failedCount)) * 100; + + return failedCount === 0 ? 100 : ratio; + }, + formattedCounts() { + const { + totalPipelines, + successfulPipelines, + failedPipelines, + totalPipelineDuration, + } = this.counts; + + return { + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, + totalDuration: totalPipelineDuration, + }; + }, areaCharts() { const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + let areaChartsData = []; + try { + areaChartsData = [ + this.buildAreaChartData(lastWeek, this.lastWeekChartData), + this.buildAreaChartData(lastMonth, this.lastMonthChartData), + this.buildAreaChartData(lastYear, this.lastYearChartData), + ]; + } catch { + areaChartsData = []; + this.reportFailure(PARSE_FAILURE); + } + + return areaChartsData; + }, + lastWeekChartData() { + return { + labels: this.analytics.weekPipelinesLabels, + totals: this.analytics.weekPipelinesTotals, + success: this.analytics.weekPipelinesSuccessful, + }; + }, + lastMonthChartData() { + return { + labels: this.analytics.monthPipelinesLabels, + totals: this.analytics.monthPipelinesTotals, + success: this.analytics.monthPipelinesSuccessful, + }; + }, + lastYearChartData() { + return { + labels: this.analytics.yearPipelinesLabels, + totals: this.analytics.yearPipelinesTotals, + success: this.analytics.yearPipelinesSuccessful, + }; + }, + timesChartTransformedData() { return [ - this.buildAreaChartData(lastWeek, this.lastWeekChartData), - this.buildAreaChartData(lastMonth, this.lastMonthChartData), - this.buildAreaChartData(lastYear, this.lastYearChartData), + { + name: 'full', + data: this.mergeLabelsAndValues( + this.analytics.pipelineTimesLabels, + this.analytics.pipelineTimesValues, + ), + }, ]; }, }, @@ -85,6 +221,13 @@ export default { ], }; }, + hideAlert() { + this.showFailureAlert = false; + }, + reportFailure(type) { + this.showFailureAlert = true; + this.failureType = type; + }, }, chartContainerHeight: CHART_CONTAINER_HEIGHT, timesChartOptions: { @@ -96,6 +239,16 @@ export default { nameGap: X_AXIS_TITLE_OFFSET, }, }, + errorTexts: { + [LOAD_ANALYTICS_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the analytics data', + ), + [LOAD_PIPELINES_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the pipelines data', + ), + [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), + [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), + }, get chartTitles() { const today = dateFormat(new Date(), CHART_DATE_FORMAT); const pastDate = timeScale => @@ -116,13 +269,16 @@ export default { </script> <template> <div> - <div class="mb-3"> + <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert"> + {{ failure.text }} + </gl-alert> + <div class="gl-mb-3"> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> </div> - <h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> + <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> <div class="row"> <div class="col-md-6"> - <statistics-list :counts="counts" /> + <statistics-list :counts="formattedCounts" /> </div> <div class="col-md-6"> <strong> @@ -139,7 +295,7 @@ export default { </div> </div> <hr /> - <h4 class="my-4">{{ __('Pipelines charts') }}</h4> + <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> <pipelines-area-chart v-for="(chart, index) in areaCharts" :key="index" diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue new file mode 100644 index 00000000000..c6e2b2e1140 --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue @@ -0,0 +1,151 @@ +<script> +import dateFormat from 'dateformat'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import { __, sprintf } from '~/locale'; +import { getDateInPast } from '~/lib/utils/datetime_utility'; +import StatisticsList from './statistics_list.vue'; +import PipelinesAreaChart from './pipelines_area_chart.vue'; +import { + CHART_CONTAINER_HEIGHT, + INNER_CHART_HEIGHT, + X_AXIS_LABEL_ROTATION, + X_AXIS_TITLE_OFFSET, + CHART_DATE_FORMAT, + ONE_WEEK_AGO_DAYS, + ONE_MONTH_AGO_DAYS, +} from '../constants'; + +export default { + components: { + StatisticsList, + GlColumnChart, + PipelinesAreaChart, + }, + props: { + counts: { + type: Object, + required: true, + }, + timesChartData: { + type: Object, + required: true, + }, + lastWeekChartData: { + type: Object, + required: true, + }, + lastMonthChartData: { + type: Object, + required: true, + }, + lastYearChartData: { + type: Object, + required: true, + }, + }, + data() { + return { + timesChartTransformedData: [ + { + name: 'full', + data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), + }, + ], + }; + }, + computed: { + areaCharts() { + const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + + return [ + this.buildAreaChartData(lastWeek, this.lastWeekChartData), + this.buildAreaChartData(lastMonth, this.lastMonthChartData), + this.buildAreaChartData(lastYear, this.lastYearChartData), + ]; + }, + }, + methods: { + mergeLabelsAndValues(labels, values) { + return labels.map((label, index) => [label, values[index]]); + }, + buildAreaChartData(title, data) { + const { labels, totals, success } = data; + + return { + title, + data: [ + { + name: 'all', + data: this.mergeLabelsAndValues(labels, totals), + }, + { + name: 'success', + data: this.mergeLabelsAndValues(labels, success), + }, + ], + }; + }, + }, + chartContainerHeight: CHART_CONTAINER_HEIGHT, + timesChartOptions: { + height: INNER_CHART_HEIGHT, + xAxis: { + axisLabel: { + rotate: X_AXIS_LABEL_ROTATION, + }, + nameGap: X_AXIS_TITLE_OFFSET, + }, + }, + get chartTitles() { + const today = dateFormat(new Date(), CHART_DATE_FORMAT); + const pastDate = timeScale => + dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT); + return { + lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), { + oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS), + today, + }), + lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), { + oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS), + today, + }), + lastYear: __('Pipelines for last year'), + }; + }, +}; +</script> +<template> + <div> + <div class="mb-3"> + <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> + </div> + <h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> + <div class="row"> + <div class="col-md-6"> + <statistics-list :counts="counts" /> + </div> + <div class="col-md-6"> + <strong> + {{ __('Duration for the last 30 commits') }} + </strong> + <gl-column-chart + :height="$options.chartContainerHeight" + :option="$options.timesChartOptions" + :bars="timesChartTransformedData" + :y-axis-title="__('Minutes')" + :x-axis-title="__('Commit')" + x-axis-type="category" + /> + </div> + </div> + <hr /> + <h4 class="my-4">{{ __('Pipelines charts') }}</h4> + <pipelines-area-chart + v-for="(chart, index) in areaCharts" + :key="index" + :chart-data="chart.data" + > + {{ chart.title }} + </pipelines-area-chart> + </div> +</template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue index aa59717ddcd..94cecd2e479 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue @@ -1,7 +1,10 @@ <script> import { formatTime } from '~/lib/utils/datetime_utility'; +import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { s__, n__ } from '~/locale'; +const defaultPrecision = 2; + export default { props: { counts: { @@ -14,6 +17,8 @@ export default { return formatTime(this.counts.totalDuration); }, statistics() { + const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred); + return [ { title: s__('PipelineCharts|Total:'), @@ -29,7 +34,7 @@ export default { }, { title: s__('PipelineCharts|Success ratio:'), - value: `${this.counts.successRatio}%`, + value: formatter(this.counts.successRatio, defaultPrecision), }, { title: s__('PipelineCharts|Total duration:'), diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js index 5dbe3c01100..079e23943c1 100644 --- a/app/assets/javascripts/projects/pipelines/charts/constants.js +++ b/app/assets/javascripts/projects/pipelines/charts/constants.js @@ -11,3 +11,9 @@ export const ONE_WEEK_AGO_DAYS = 7; export const ONE_MONTH_AGO_DAYS = 31; export const CHART_DATE_FORMAT = 'dd mmm'; + +export const DEFAULT = 'default'; +export const PARSE_FAILURE = 'parse_failure'; +export const LOAD_ANALYTICS_FAILURE = 'load_analytics_failure'; +export const LOAD_PIPELINES_FAILURE = 'load_analytics_failure'; +export const UNSUPPORTED_DATA = 'unsupported_data'; diff --git a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql new file mode 100644 index 00000000000..eb0dbf8dd16 --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql @@ -0,0 +1,14 @@ +query getPipelineCountByStatus($projectPath: ID!) { + project(fullPath: $projectPath) { + totalPipelines: pipelines { + count + } + successfulPipelines: pipelines(status: SUCCESS) { + count + } + failedPipelines: pipelines(status: FAILED) { + count + } + totalPipelineDuration + } +} diff --git a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql new file mode 100644 index 00000000000..18b645f8831 --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql @@ -0,0 +1,17 @@ +query getProjectPipelineStatistics($projectPath: ID!) { + project(fullPath: $projectPath) { + pipelineAnalytics { + weekPipelinesTotals + weekPipelinesLabels + weekPipelinesSuccessful + monthPipelinesLabels + monthPipelinesTotals + monthPipelinesSuccessful + yearPipelinesLabels + yearPipelinesTotals + yearPipelinesSuccessful + pipelineTimesLabels + pipelineTimesValues + } + } +} diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js index eef1bc2d28b..f6e79f0ab51 100644 --- a/app/assets/javascripts/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/projects/pipelines/charts/index.js @@ -1,8 +1,20 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import ProjectPipelinesChartsLegacy from './components/app_legacy.vue'; import ProjectPipelinesCharts from './components/app.vue'; -export default () => { - const el = document.querySelector('#js-project-pipelines-charts-app'); +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +const mountPipelineChartsApp = el => { + // Not all of the values will be defined since some them will be + // empty depending on the value of the graphql_pipeline_analytics + // feature flag, once the rollout of the feature flag is completed + // the undefined values will be deleted const { countsFailed, countsSuccess, @@ -20,22 +32,48 @@ export default () => { lastYearChartLabels, lastYearChartTotals, lastYearChartSuccess, + projectPath, } = el.dataset; - const parseAreaChartData = (labels, totals, success) => ({ - labels: JSON.parse(labels), - totals: JSON.parse(totals), - success: JSON.parse(success), - }); + const parseAreaChartData = (labels, totals, success) => { + let parsedData = {}; + + try { + parsedData = { + labels: JSON.parse(labels), + totals: JSON.parse(totals), + success: JSON.parse(success), + }; + } catch { + parsedData = {}; + } + + return parsedData; + }; + + if (gon?.features?.graphqlPipelineAnalytics) { + return new Vue({ + el, + name: 'ProjectPipelinesChartsApp', + components: { + ProjectPipelinesCharts, + }, + apolloProvider, + provide: { + projectPath, + }, + render: createElement => createElement(ProjectPipelinesCharts, {}), + }); + } return new Vue({ el, - name: 'ProjectPipelinesChartsApp', + name: 'ProjectPipelinesChartsAppLegacy', components: { - ProjectPipelinesCharts, + ProjectPipelinesChartsLegacy, }, render: createElement => - createElement(ProjectPipelinesCharts, { + createElement(ProjectPipelinesChartsLegacy, { props: { counts: { failed: countsFailed, @@ -67,3 +105,8 @@ export default () => { }), }); }; + +export default () => { + const el = document.querySelector('#js-project-pipelines-charts-app'); + return !el ? {} : mountPipelineChartsApp(el); +}; |