diff options
Diffstat (limited to 'app/assets/javascripts')
17 files changed, 442 insertions, 340 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 6e22e181c02..463b7f5cff4 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -56,10 +56,11 @@ export default { }, computed: { ...mapState({ - noteableData: (state) => state.notes.noteableData, - diffViewType: (state) => state.diffs.diffViewType, + diffViewType: ({ diffs }) => diffs.diffViewType, + showSuggestPopover: ({ diffs }) => diffs.showSuggestPopover, + noteableData: ({ notes }) => notes.noteableData, + selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition, }), - ...mapState('diffs', ['showSuggestPopover']), ...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']), ...mapGetters([ 'isLoggedIn', @@ -126,6 +127,10 @@ export default { this.initAutoSave(this.noteableData, keys); } + + if (this.selectedCommentPosition) { + this.commentLineStart = this.selectedCommentPosition.start; + } }, methods: { ...mapActions('diffs', [ diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue index 9e1385853b6..9054a8aec04 100644 --- a/app/assets/javascripts/diffs/components/diff_row.vue +++ b/app/assets/javascripts/diffs/components/diff_row.vue @@ -10,6 +10,7 @@ import { CONFLICT_THEIR, CONFLICT_MARKER, } from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import DiffGutterAvatars from './diff_gutter_avatars.vue'; import * as utils from './diff_row_utils'; @@ -22,6 +23,7 @@ export default { GlTooltip: GlTooltipDirective, SafeHtml, }, + mixins: [glFeatureFlagsMixin()], props: { fileHash: { type: String, @@ -45,6 +47,15 @@ export default { required: false, default: false, }, + index: { + type: Number, + required: true, + }, + }, + data() { + return { + dragging: false, + }; }, computed: { ...mapGetters('diffs', ['fileLineCoverage']), @@ -52,26 +63,35 @@ export default { ...mapState({ isHighlighted(state) { const line = this.line.left?.line_code ? this.line.left : this.line.right; - return utils.isHighlighted(state, line, this.isCommented); + return utils.isHighlighted(state, line, false); }, }), classNameMap() { return { [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, [PARALLEL_DIFF_VIEW_TYPE]: !this.inline, + commented: this.isCommented, }; }, parallelViewLeftLineType() { - return utils.parallelViewLeftLineType(this.line, this.isHighlighted); + return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented); }, coverageState() { return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, classNameMapCellLeft() { - return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn); + return utils.classNameMapCell({ + line: this.line.left, + hll: this.isHighlighted || this.isCommented, + isLoggedIn: this.isLoggedIn, + }); }, classNameMapCellRight() { - return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn); + return utils.classNameMapCell({ + line: this.line.right, + hll: this.isHighlighted || this.isCommented, + isLoggedIn: this.isLoggedIn, + }); }, addCommentTooltipLeft() { return utils.addCommentTooltip(this.line.left); @@ -131,6 +151,22 @@ export default { ? this.$options.THEIR_CHANGES : this.$options.OUR_CHANGES; }, + onDragEnd() { + this.dragging = false; + if (!this.glFeatures.dragCommentSelection) return; + + this.$emit('stopdragging'); + }, + onDragEnter(line, index) { + if (!this.glFeatures.dragCommentSelection) return; + + this.$emit('enterdragging', { ...line, index }); + }, + onDragStart(line) { + this.$root.$emit('bv::hide::tooltip'); + this.dragging = true; + this.$emit('startdragging', line); + }, }, OUR_CHANGES: 'HEAD//our changes', THEIR_CHANGES: 'origin//their changes', @@ -143,7 +179,13 @@ export default { <template> <div :class="classNameMap" class="diff-grid-row diff-tr line_holder"> - <div class="diff-grid-left left-side"> + <div + data-testid="left-side" + class="diff-grid-left left-side" + @dragover.prevent + @dragenter="onDragEnter(line.left, index)" + @dragend="onDragEnd" + > <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER"> <div :class="classNameMapCellLeft" @@ -159,10 +201,13 @@ export default { :title="addCommentTooltipLeft" > <button + :draggable="glFeatures.dragCommentSelection" type="button" class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :class="{ 'gl-cursor-grab': dragging }" :disabled="line.left.commentsDisabled" @click="handleCommentButton(line.left)" + @dragstart="onDragStart({ ...line.left, index })" > <gl-icon :size="12" name="comment" /> </button> @@ -234,7 +279,14 @@ export default { ></div> </template> </div> - <div v-if="!inline" class="diff-grid-right right-side"> + <div + v-if="!inline" + data-testid="right-side" + class="diff-grid-right right-side" + @dragover.prevent + @dragenter="onDragEnter(line.right, index)" + @dragend="onDragEnd" + > <template v-if="line.right"> <div :class="classNameMapCellRight" class="diff-td diff-line-num new_line"> <template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR"> @@ -246,10 +298,13 @@ export default { :title="addCommentTooltipRight" > <button + :draggable="glFeatures.dragCommentSelection" type="button" class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :class="{ 'gl-cursor-grab': dragging }" :disabled="line.right.commentsDisabled" @click="handleCommentButton(line.right)" + @dragstart="onDragStart({ ...line.right, index })" > <gl-icon :size="12" name="comment" /> </button> @@ -279,13 +334,21 @@ export default { <div v-gl-tooltip.hover :title="coverageState.text" - :class="[line.right.type, coverageState.class, { hll: isHighlighted }]" + :class="[line.right.type, coverageState.class, { hll: isHighlighted, hll: isCommented }]" class="diff-td line-coverage right-side" ></div> <div :id="line.right.line_code" :key="line.right.rich_text" - :class="[line.right.type, { hll: isHighlighted, parallel: !inline }]" + v-safe-html="line.right.rich_text" + :class="[ + line.right.type, + { + hll: isHighlighted, + hll: isCommented, + parallel: !inline, + }, + ]" class="diff-td line_content with-coverage right-side" @mousedown="handleParallelLineMouseDown" > diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index 3ef271ddb8f..7606c39ad37 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -35,7 +35,7 @@ export const lineCode = (line) => { return line.line_code || line.left?.line_code || line.right?.line_code; }; -export const classNameMapCell = (line, hll, isLoggedIn, isHover) => { +export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => { if (!line) return []; const { type } = line; @@ -54,7 +54,9 @@ export const addCommentTooltip = (line) => { let tooltip; if (!line) return tooltip; - tooltip = __('Add a comment to this line'); + tooltip = gon.drag_comment_selection + ? __('Add a comment to this line or drag for multiple lines') + : __('Add a comment to this line'); const brokenSymlinks = line.commentsDisabled; if (brokenSymlinks) { diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue index 84429f62a1c..79800f835f4 100644 --- a/app/assets/javascripts/diffs/components/diff_view.vue +++ b/app/assets/javascripts/diffs/components/diff_view.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters, mapState } from 'vuex'; +import { mapGetters, mapState, mapActions } from 'vuex'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import DiffRow from './diff_row.vue'; @@ -35,6 +35,12 @@ export default { default: false, }, }, + data() { + return { + dragStart: null, + updatedLineRange: null, + }; + }, computed: { ...mapGetters('diffs', ['commitId']), ...mapState({ @@ -52,12 +58,39 @@ export default { }, }, methods: { + ...mapActions(['setSelectedCommentPosition']), + ...mapActions('diffs', ['showCommentForm']), showCommentLeft(line) { return !this.inline || line.left; }, showCommentRight(line) { return !this.inline || (line.right && !line.left); }, + onStartDragging(line) { + this.dragStart = line; + }, + onDragOver(line) { + if (line.chunk !== this.dragStart.chunk) return; + + let start = this.dragStart; + let end = line; + + if (this.dragStart.index >= line.index) { + start = line; + end = this.dragStart; + } + + this.updatedLineRange = { start, end }; + + this.setSelectedCommentPosition(this.updatedLineRange); + }, + onStopDragging() { + this.showCommentForm({ + lineCode: this.updatedLineRange?.end?.line_code, + fileHash: this.diffFile.file_hash, + }); + this.dragStart = null; + }, }, userColorScheme: window.gon.user_color_scheme, }; @@ -94,6 +127,10 @@ export default { :is-bottom="index + 1 === diffLinesLength" :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :inline="inline" + :index="index" + @enterdragging="onDragOver" + @startdragging="onStartDragging" + @stopdragging="onStopDragging" /> <div v-if="line.renderCommentRow" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 2d8ffb047ca..014b1ebe54b 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -72,7 +72,12 @@ export default { return this.fileLineCoverage(this.filePath, this.line.new_line); }, classNameMapCell() { - return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover); + return classNameMapCell({ + line: this.line, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isHover, + }); }, addCommentTooltip() { return addCommentTooltip(this.line); diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index b1618fb0688..47eecef2385 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -68,20 +68,20 @@ export default { return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, classNameMapCellLeft() { - return utils.classNameMapCell( - this.line.left, - this.isHighlighted, - this.isLoggedIn, - this.isLeftHover, - ); + return utils.classNameMapCell({ + line: this.line.left, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isLeftHover, + }); }, classNameMapCellRight() { - return utils.classNameMapCell( - this.line.right, - this.isHighlighted, - this.isLoggedIn, - this.isRightHover, - ); + return utils.classNameMapCell({ + line: this.line.right, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isRightHover, + }); }, addCommentTooltipLeft() { return utils.addCommentTooltip(this.line.left); diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 8712220b25e..c52da558be2 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -55,8 +55,17 @@ export const parallelizeDiffLines = (diffLines, inline) => { let conflictStartIndex = -1; const lines = []; + // `chunk` is used for dragging to select diff lines + // we are restricting commenting to only lines that appear between + // "expansion rows". Here equal chunks are lines grouped together + // inbetween expansion rows. + let chunk = 0; + for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) { const line = diffLines[i]; + line.chunk = chunk; + + if (isMeta(line)) chunk += 1; if (isRemoved(line) || isConflictOur(line) || inline) { lines.push({ diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue index 10078d5cd64..fcb70dd45a6 100644 --- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue @@ -28,13 +28,13 @@ export default { if (this.isCurrentUser) { return sprintf( s__('Members|Are you sure you want to withdraw your access request for "%{source}"'), - { source: source.name }, + { source: source.fullName }, ); } return sprintf( s__('Members|Are you sure you want to deny %{usersName}\'s request to join "%{source}"'), - { usersName: user.name, source: source.name }, + { usersName: user.name, source: source.fullName }, ); }, }, diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue index 2b0a75640e2..9a27348f146 100644 --- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue @@ -25,7 +25,7 @@ export default { s__( 'Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join "%{source}"', ), - { inviteEmail: invite.email, source: source.name }, + { inviteEmail: invite.email, source: source.fullName }, ); }, }, diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue index f2bc9c7e876..0e5df961782 100644 --- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue @@ -36,7 +36,7 @@ export default { s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'), { usersName: user.name, - source: source.name, + source: source.fullName, }, ); } @@ -44,7 +44,7 @@ export default { return sprintf( s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'), { - source: source.name, + source: source.fullName, }, ); }, diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue index 57a5da774e3..d231c7eabfa 100644 --- a/app/assets/javascripts/members/components/modals/leave_modal.vue +++ b/app/assets/javascripts/members/components/modals/leave_modal.vue @@ -35,7 +35,7 @@ export default { return this.memberPath.replace(/:id$/, 'leave'); }, modalTitle() { - return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.name }); + return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName }); }, }, methods: { @@ -59,7 +59,7 @@ export default { <gl-form ref="form" :action="leavePath" method="post"> <p> <gl-sprintf :message="$options.modalContent"> - <template #source>{{ member.source.name }}</template> + <template #source>{{ member.source.fullName }}</template> </gl-sprintf> </p> diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index 030d72c3420..30fcbfcd3f8 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -22,6 +22,6 @@ export default { <template> <span v-if="isDirectMember">{{ __('Direct member') }}</span> <a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{ - memberSource.name + memberSource.fullName }}</a> </template> diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index bb13eb87af7..9fbf2c9265c 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_form.vue +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { GlFormSelect, GlSprintf } from '@gitlab/ui'; import { getSymbol, getLineClasses } from './multiline_comment_utils'; @@ -27,12 +27,13 @@ export default { }; }, computed: { + ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }), lineNumber() { return this.commentLineOptions[this.commentLineOptions.length - 1].text; }, }, created() { - const line = this.lineRange?.start || this.line; + const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line; this.commentLineStart = { line_code: line.line_code, @@ -40,6 +41,8 @@ export default { old_line: line.old_line, new_line: line.new_line, }; + + if (this.selectedCommentPosition) return; this.highlightSelection(); }, destroyed() { diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 4421a84a6b1..144a3d7ba90 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -15,7 +15,7 @@ export default () => ({ batchSuggestionsInfo: [], currentlyFetchingDiscussions: false, /** - * selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`: + * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`: * { * start: { line_code: string, new_line: number, old_line:number, type: string }, * end: { line_code: string, new_line: number, old_line:number, type: string }, diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 75f5edbfd2b..7bb62cf4a73 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,38 +1,18 @@ <script> -import dateFormat from 'dateformat'; -import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; -import { getDateInPast } from '~/lib/utils/datetime_utility'; +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 StatisticsList from './statistics_list.vue'; -import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; +import PipelineCharts from './pipeline_charts.vue'; import { - CHART_CONTAINER_HEIGHT, - 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: [], @@ -47,36 +27,40 @@ const defaultAnalyticsValues = { pipelineTimesValues: [], }; +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; + export default { components: { GlAlert, - GlColumnChart, - GlSkeletonLoader, - StatisticsList, - CiCdAnalyticsAreaChart, + GlTabs, + GlTab, + PipelineCharts, DeploymentFrequencyCharts: () => import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'), }, inject: { - projectPath: { - type: String, - default: '', - }, shouldRenderDeploymentFrequencyCharts: { type: Boolean, default: false, }, + projectPath: { + type: String, + default: '', + }, }, data() { return { - counts: { - ...defaultCountValues, - }, - analytics: { - ...defaultAnalyticsValues, - }, showFailureAlert: false, failureType: null, + analytics: { ...defaultAnalyticsValues }, + counts: { ...defaultCountValues }, }; }, apollo: { @@ -134,41 +118,6 @@ export default { }; } }, - 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, - }; - }, - 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, @@ -190,39 +139,32 @@ export default { success: this.analytics.yearPipelinesSuccessful, }; }, - timesChartTransformedData() { - return [ - { - name: 'full', - data: this.mergeLabelsAndValues( - this.analytics.pipelineTimesLabels, - this.analytics.pipelineTimesValues, - ), - }, - ]; + timesChartData() { + return { + labels: this.analytics.pipelineTimesLabels, + values: this.analytics.pipelineTimesValues, + }; }, - }, - methods: { - mergeLabelsAndValues(labels, values) { - return labels.map((label, index) => [label, values[index]]); + 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; }, - buildAreaChartData(title, data) { - const { labels, totals, success } = data; + formattedCounts() { + const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; return { - title, - data: [ - { - name: 'all', - data: this.mergeLabelsAndValues(labels, totals), - }, - { - name: 'success', - data: this.mergeLabelsAndValues(labels, success), - }, - ], + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, }; }, + }, + methods: { hideAlert() { this.showFailureAlert = false; }, @@ -231,16 +173,6 @@ export default { this.failureType = type; }, }, - chartContainerHeight: CHART_CONTAINER_HEIGHT, - timesChartOptions: { - height: INNER_CHART_HEIGHT, - xAxis: { - axisLabel: { - rotate: X_AXIS_LABEL_ROTATION, - }, - nameGap: X_AXIS_TITLE_OFFSET, - }, - }, errorTexts: { [LOAD_ANALYTICS_FAILURE]: s__( 'PipelineCharts|An error has ocurred when retrieving the analytics data', @@ -251,74 +183,38 @@ export default { [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) => - 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'), - }; - }, - areaChartOptions: { - xAxis: { - name: s__('Pipeline|Date'), - type: 'category', - }, - yAxis: { - name: s__('Pipeline|Pipelines'), - }, - }, }; </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> - <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> - <div class="row"> - <div class="col-md-6"> - <gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" /> - <statistics-list v-else :counts="formattedCounts" /> - </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" + <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ + failure.text + }}</gl-alert> + <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts"> + <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" /> - </div> - </div> - <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> - <template v-if="shouldRenderDeploymentFrequencyCharts"> - <hr /> - <deployment-frequency-charts /> - </template> + </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" + /> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue index 915d334f949..03e02659060 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue @@ -1,26 +1,12 @@ <script> -import dateFormat from 'dateformat'; -import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { __, s__, sprintf } from '~/locale'; -import { getDateInPast } from '~/lib/utils/datetime_utility'; -import StatisticsList from './statistics_list.vue'; -import CiCdAnalyticsAreaChart from './ci_cd_analytics_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'; +import { GlTabs, GlTab } from '@gitlab/ui'; +import PipelineCharts from './pipeline_charts.vue'; export default { components: { - StatisticsList, - GlColumnChart, - CiCdAnalyticsAreaChart, + GlTabs, + GlTab, + PipelineCharts, DeploymentFrequencyCharts: () => import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'), }, @@ -54,121 +40,41 @@ export default { }, data() { return { - timesChartTransformedData: [ - { - name: 'full', - data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), - }, - ], + // this loading flag gives the echarts library just enough time + // to ensure all DOM nodes have been mounted. + // + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1131 + loading: true, }; }, - 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'), - }; - }, - areaChartOptions: { - xAxis: { - name: s__('Pipeline|Date'), - type: 'category', - }, - yAxis: { - name: s__('Pipeline|Pipelines'), - }, + async mounted() { + await this.$nextTick(); + this.loading = false; }, }; </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> - <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> - <template v-if="shouldRenderDeploymentFrequencyCharts"> - <hr /> + <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts"> + <gl-tab :title="__('Pipelines')"> + <pipeline-charts + :counts="counts" + :last-week="lastWeekChartData" + :last-month="lastMonthChartData" + :last-year="lastYearChartData" + :times-chart="timesChartData" + :loading="loading" + /> + </gl-tab> + <gl-tab :title="__('Deployments')"> <deployment-frequency-charts /> - </template> - </div> + </gl-tab> + </gl-tabs> + <pipeline-charts + v-else + :counts="counts" + :last-week="lastWeekChartData" + :last-month="lastMonthChartData" + :last-year="lastYearChartData" + :times-chart="timesChartData" + /> </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 new file mode 100644 index 00000000000..bec4ab407f0 --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -0,0 +1,176 @@ +<script> +import dateFormat from 'dateformat'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { __, s__, sprintf } from '~/locale'; +import { getDateInPast } from '~/lib/utils/datetime_utility'; +import { + CHART_CONTAINER_HEIGHT, + CHART_DATE_FORMAT, + INNER_CHART_HEIGHT, + ONE_WEEK_AGO_DAYS, + ONE_MONTH_AGO_DAYS, + X_AXIS_LABEL_ROTATION, + X_AXIS_TITLE_OFFSET, + PARSE_FAILURE, +} from '../constants'; +import StatisticsList from './statistics_list.vue'; +import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; + +export default { + components: { + GlColumnChart, + GlSkeletonLoader, + StatisticsList, + CiCdAnalyticsAreaChart, + }, + props: { + counts: { + required: true, + type: Object, + }, + loading: { + required: false, + default: false, + type: Boolean, + }, + lastWeek: { + required: true, + type: Object, + }, + lastMonth: { + required: true, + type: Object, + }, + lastYear: { + required: true, + type: Object, + }, + timesChart: { + required: true, + type: Object, + }, + }, + computed: { + areaCharts() { + const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + const charts = [ + { title: lastWeek, data: this.lastWeek }, + { title: lastMonth, data: this.lastMonth }, + { title: lastYear, data: this.lastYear }, + ]; + let areaChartsData = []; + + try { + areaChartsData = charts.map(this.buildAreaChartData); + } catch { + areaChartsData = []; + this.vm.$emit('report-failure', PARSE_FAILURE); + } + + return areaChartsData; + }, + timesChartTransformedData() { + return [ + { + name: 'full', + data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values), + }, + ]; + }, + }, + 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, + }, + }, + areaChartOptions: { + xAxis: { + name: s__('Pipeline|Date'), + type: 'category', + }, + yAxis: { + name: s__('Pipeline|Pipelines'), + }, + }, + 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="gl-mb-3"> + <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> + </div> + <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> + <div class="row"> + <div class="col-md-6"> + <gl-skeleton-loader v-if="loading" :lines="5" /> + <statistics-list v-else :counts="counts" /> + </div> + <div v-if="!loading" 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> + <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 + > + </template> + </div> +</template> |