diff options
Diffstat (limited to 'app/assets/javascripts/projects/pipelines/charts')
4 files changed, 262 insertions, 226 deletions
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 7bb62cf4a73..4a8e1424fa8 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,44 +1,12 @@ <script> -import { GlAlert, GlTabs, GlTab } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; -import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; +import { GlTabs, GlTab } from '@gitlab/ui'; +import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import PipelineCharts from './pipeline_charts.vue'; -import { - DEFAULT, - LOAD_ANALYTICS_FAILURE, - LOAD_PIPELINES_FAILURE, - PARSE_FAILURE, - UNSUPPORTED_DATA, -} from '../constants'; - -const defaultAnalyticsValues = { - weekPipelinesTotals: [], - weekPipelinesLabels: [], - weekPipelinesSuccessful: [], - monthPipelinesLabels: [], - monthPipelinesTotals: [], - monthPipelinesSuccessful: [], - yearPipelinesLabels: [], - yearPipelinesTotals: [], - yearPipelinesSuccessful: [], - pipelineTimesLabels: [], - pipelineTimesValues: [], -}; - -const defaultCountValues = { - totalPipelines: { - count: 0, - }, - successfulPipelines: { - count: 0, - }, -}; +const charts = ['pipelines', 'deployments']; export default { components: { - GlAlert, GlTabs, GlTab, PipelineCharts, @@ -50,171 +18,42 @@ export default { type: Boolean, default: false, }, - projectPath: { - type: String, - default: '', - }, }, data() { return { - showFailureAlert: false, - failureType: null, - analytics: { ...defaultAnalyticsValues }, - counts: { ...defaultCountValues }, + selectedTab: 0, }; }, - 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', - }; - } - }, - 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, - }; - }, - timesChartData() { - return { - labels: this.analytics.pipelineTimesLabels, - values: this.analytics.pipelineTimesValues, - }; - }, - 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 } = this.counts; - - return { - total: totalPipelines?.count, - success: successfulPipelines?.count, - failed: failedPipelines?.count, - successRatio: this.successRatio, - }; - }, + created() { + this.selectTab(); + window.addEventListener('popstate', this.selectTab); }, methods: { - hideAlert() { - this.showFailureAlert = false; - }, - reportFailure(type) { - this.showFailureAlert = true; - this.failureType = type; + selectTab() { + const [chart] = getParameterValues('chart') || charts; + const tab = charts.indexOf(chart); + this.selectedTab = tab >= 0 ? tab : 0; + }, + onTabChange(index) { + if (index !== this.selectedTab) { + this.selectedTab = index; + const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname); + updateHistory({ url: path, title: window.title }); + } }, }, - 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.'), - }, }; </script> <template> <div> - <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ - failure.text - }}</gl-alert> - <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts"> + <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange"> <gl-tab :title="__('Pipelines')"> - <pipeline-charts - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts /> </gl-tab> <gl-tab :title="__('Deployments')"> <deployment-frequency-charts /> </gl-tab> </gl-tabs> - <pipeline-charts - v-else - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts v-else /> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue new file mode 100644 index 00000000000..43b36da8b2c --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue @@ -0,0 +1,50 @@ +<script> +import { GlSegmentedControl } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; + +export default { + components: { + GlSegmentedControl, + CiCdAnalyticsAreaChart, + }, + props: { + charts: { + required: true, + type: Array, + }, + chartOptions: { + required: true, + type: Object, + }, + }, + data() { + return { + selectedChart: 0, + }; + }, + computed: { + chartRanges() { + return this.charts.map(({ title }, index) => ({ text: title, value: index })); + }, + chart() { + return this.charts[this.selectedChart]; + }, + dateRange() { + return sprintf(s__('CiCdAnalytics|Date range: %{range}'), { range: this.chart.range }); + }, + }, +}; +</script> +<template> + <div> + <gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" /> + <ci-cd-analytics-area-chart + v-if="chart" + :chart-data="chart.data" + :area-chart-options="chartOptions" + > + {{ dateRange }} + </ci-cd-analytics-area-chart> + </div> +</template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index bec4ab407f0..733f833d51a 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -1,63 +1,184 @@ <script> -import dateFormat from 'dateformat'; +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlSkeletonLoader } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; +import dateFormat from 'dateformat'; import { getDateInPast } from '~/lib/utils/datetime_utility'; +import { __, s__, sprintf } from '~/locale'; import { + DEFAULT, CHART_CONTAINER_HEIGHT, CHART_DATE_FORMAT, INNER_CHART_HEIGHT, ONE_WEEK_AGO_DAYS, ONE_MONTH_AGO_DAYS, + ONE_YEAR_AGO_DAYS, X_AXIS_LABEL_ROTATION, X_AXIS_TITLE_OFFSET, PARSE_FAILURE, + LOAD_ANALYTICS_FAILURE, + LOAD_PIPELINES_FAILURE, + UNSUPPORTED_DATA, } from '../constants'; +import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; +import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; +import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue'; import StatisticsList from './statistics_list.vue'; -import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; + +const defaultAnalyticsValues = { + weekPipelinesTotals: [], + weekPipelinesLabels: [], + weekPipelinesSuccessful: [], + monthPipelinesLabels: [], + monthPipelinesTotals: [], + monthPipelinesSuccessful: [], + yearPipelinesLabels: [], + yearPipelinesTotals: [], + yearPipelinesSuccessful: [], + pipelineTimesLabels: [], + pipelineTimesValues: [], +}; + +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; export default { components: { + GlAlert, GlColumnChart, GlSkeletonLoader, StatisticsList, - CiCdAnalyticsAreaChart, + CiCdAnalyticsCharts, + }, + inject: { + projectPath: { + type: String, + default: '', + }, }, - props: { + data() { + return { + showFailureAlert: false, + failureType: null, + analytics: { ...defaultAnalyticsValues }, + counts: { ...defaultCountValues }, + }; + }, + apollo: { counts: { - required: true, - type: Object, + 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: { + loading() { + return this.$apollo.queries.counts.loading; }, - loading: { - required: false, - default: false, - type: Boolean, + 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', + }; + } }, - lastWeek: { - required: true, - type: Object, + lastWeekChartData() { + return { + labels: this.analytics.weekPipelinesLabels, + totals: this.analytics.weekPipelinesTotals, + success: this.analytics.weekPipelinesSuccessful, + }; }, - lastMonth: { - required: true, - type: Object, + lastMonthChartData() { + return { + labels: this.analytics.monthPipelinesLabels, + totals: this.analytics.monthPipelinesTotals, + success: this.analytics.monthPipelinesSuccessful, + }; }, - lastYear: { - required: true, - type: Object, + lastYearChartData() { + return { + labels: this.analytics.yearPipelinesLabels, + totals: this.analytics.yearPipelinesTotals, + success: this.analytics.yearPipelinesSuccessful, + }; }, - timesChart: { - required: true, - type: Object, + timesChartData() { + return { + labels: this.analytics.pipelineTimesLabels, + values: this.analytics.pipelineTimesValues, + }; + }, + 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 } = this.counts; + + return { + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, + }; }, - }, - computed: { areaCharts() { const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + const { lastWeekRange, lastMonthRange, lastYearRange } = this.$options.chartRanges; const charts = [ - { title: lastWeek, data: this.lastWeek }, - { title: lastMonth, data: this.lastMonth }, - { title: lastYear, data: this.lastYear }, + { title: lastWeek, range: lastWeekRange, data: this.lastWeekChartData }, + { title: lastMonth, range: lastMonthRange, data: this.lastMonthChartData }, + { title: lastYear, range: lastYearRange, data: this.lastYearChartData }, ]; let areaChartsData = []; @@ -65,7 +186,7 @@ export default { areaChartsData = charts.map(this.buildAreaChartData); } catch { areaChartsData = []; - this.vm.$emit('report-failure', PARSE_FAILURE); + this.reportFailure(PARSE_FAILURE); } return areaChartsData; @@ -74,20 +195,28 @@ export default { return [ { name: 'full', - data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values), + data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), }, ]; }, }, methods: { + hideAlert() { + this.showFailureAlert = false; + }, + reportFailure(type) { + this.showFailureAlert = true; + this.failureType = type; + }, mergeLabelsAndValues(labels, values) { return labels.map((label, index) => [label, values[index]]); }, - buildAreaChartData({ title, data }) { + buildAreaChartData({ title, data, range }) { const { labels, totals, success } = data; return { title, + range, data: [ { name: 'all', @@ -118,28 +247,50 @@ export default { }, yAxis: { name: s__('Pipeline|Pipelines'), + minInterval: 1, }, }, - get chartTitles() { + 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.'), + }, + chartTitles: { + lastWeek: __('Last week'), + lastMonth: __('Last month'), + lastYear: __('Last year'), + }, + get chartRanges() { 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})'), { + lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), { oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS), today, }), - lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), { + lastMonthRange: sprintf(__('%{oneMonthAgo} - %{today}'), { oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS), today, }), - lastYear: __('Pipelines for last year'), + lastYearRange: sprintf(__('%{oneYearAgo} - %{today}'), { + oneYearAgo: pastDate(ONE_YEAR_AGO_DAYS), + today, + }), }; }, }; </script> <template> <div> + <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> @@ -147,7 +298,7 @@ export default { <div class="row"> <div class="col-md-6"> <gl-skeleton-loader v-if="loading" :lines="5" /> - <statistics-list v-else :counts="counts" /> + <statistics-list v-else :counts="formattedCounts" /> </div> <div v-if="!loading" class="col-md-6"> <strong>{{ __('Duration for the last 30 commits') }}</strong> @@ -164,13 +315,7 @@ export default { <template v-if="!loading"> <hr /> <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> - <ci-cd-analytics-area-chart - v-for="(chart, index) in areaCharts" - :key="index" - :chart-data="chart.data" - :area-chart-options="$options.areaChartOptions" - >{{ chart.title }}</ci-cd-analytics-area-chart - > + <ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" /> </template> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js index 079e23943c1..41fe81f21ea 100644 --- a/app/assets/javascripts/projects/pipelines/charts/constants.js +++ b/app/assets/javascripts/projects/pipelines/charts/constants.js @@ -10,6 +10,8 @@ export const ONE_WEEK_AGO_DAYS = 7; export const ONE_MONTH_AGO_DAYS = 31; +export const ONE_YEAR_AGO_DAYS = 365; + export const CHART_DATE_FORMAT = 'dd mmm'; export const DEFAULT = 'default'; |