diff options
Diffstat (limited to 'app/assets/javascripts/pipelines')
31 files changed, 378 insertions, 329 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 31a34ab4fb5..1a05710a13e 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -170,7 +170,7 @@ export default { ref="mainPipelineContainer" class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap" :class="{ - 'gl-pipeline-min-h gl-py-5 gl-overflow-auto gl-border-t-solid gl-border-t-1 gl-border-gray-100': !isLinkedPipeline, + 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline, }" > <linked-graph-wrapper> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 14872c34afb..f822e2c0874 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -281,7 +281,6 @@ export default { :type="graphViewType" :show-links="showLinks" :tip-previously-dismissed="hoverTipPreviouslyDismissed" - :is-pipeline-complete="pipeline.complete" @dismissHoverTip="handleTipDismissal" @updateViewType="updateViewType" @updateShowLinksState="updateShowLinksState" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue index a8c5d85f4ed..6d8c35f4482 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue @@ -1,33 +1,19 @@ <script> -import { - GlAlert, - GlButton, - GlButtonGroup, - GlLoadingIcon, - GlToggle, - GlModalDirective, -} from '@gitlab/ui'; +import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui'; import { __, s__ } from '~/locale'; -import Tracking from '~/tracking'; -import PerformanceInsightsModal from '../performance_insights_modal.vue'; -import { performanceModalId } from '../../constants'; import { STAGE_VIEW, LAYER_VIEW } from './constants'; export default { name: 'GraphViewSelector', - performanceModalId, + components: { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle, - PerformanceInsightsModal, - }, - directives: { - GlModal: GlModalDirective, }, - mixins: [Tracking.mixin()], + props: { showLinks: { type: Boolean, @@ -41,10 +27,6 @@ export default { type: String, required: true, }, - isPipelineComplete: { - type: Boolean, - required: true, - }, }, data() { return { @@ -59,7 +41,6 @@ export default { hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'), linksLabelText: s__('GraphViewType|Show dependencies'), viewLabelText: __('Group jobs by'), - performanceBtnText: __('Performance insights'), }, views: { [STAGE_VIEW]: { @@ -150,9 +131,6 @@ export default { this.$emit('updateShowLinksState', val); }); }, - trackInsightsClick() { - this.track('click_insights_button', { label: 'performance_insights' }); - }, }, }; </script> @@ -178,15 +156,6 @@ export default { </gl-button> </gl-button-group> - <gl-button - v-if="isPipelineComplete" - v-gl-modal="$options.performanceModalId" - data-testid="pipeline-insights-btn" - @click="trackInsightsClick" - > - {{ $options.i18n.performanceBtnText }} - </gl-button> - <div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center"> <gl-toggle v-model="showLinksActive" @@ -202,7 +171,5 @@ export default { <gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip"> {{ $options.i18n.hoverTipText }} </gl-alert> - - <performance-insights-modal /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 8d764fad0c5..02d0c07ea54 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -82,7 +82,9 @@ export default { :stage-name="stageName" /> - <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div> + <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4 gl-align-self-center"> + {{ group.size }} + </div> </div> </button> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 6ab4eb58977..4aec28295bd 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,5 +1,5 @@ <script> -import { capitalize, escape, isEmpty } from 'lodash'; +import { escape, isEmpty } from 'lodash'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { reportToSentry } from '../../utils'; import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; @@ -64,8 +64,7 @@ export default { }, }, jobClasses: [ - 'gl-py-3', - 'gl-px-4', + 'gl-p-3', 'gl-border-gray-100', 'gl-border-solid', 'gl-border-1', @@ -92,9 +91,6 @@ export default { columnSpacingClass() { return this.isStageView ? 'gl-px-6' : 'gl-px-9'; }, - formattedTitle() { - return capitalize(escape(this.name)); - }, hasAction() { return !isEmpty(this.action); }, @@ -141,8 +137,8 @@ export default { class="gl-display-flex gl-justify-content-space-between gl-relative" :class="$options.titleClasses" > - <span :title="formattedTitle" class="gl-text-truncate gl-pr-3 gl-w-85p"> - {{ formattedTitle }} + <span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p"> + {{ name }} </span> <action-component v-if="hasAction && canUpdatePipeline" diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index fabae62fc45..a36d5d9b58f 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -9,7 +9,7 @@ import { } from '@gitlab/ui'; import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; -import ciHeader from '~/vue_shared/components/header_ci_component.vue'; +import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import { LOAD_FAILURE, POST_FAILURE, @@ -33,7 +33,7 @@ export default { pipelineRetry: 'pipelineRetry', finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'], components: { - ciHeader, + CiHeader, GlAlert, GlButton, GlLoadingIcon, diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue index 70d1a5c08cc..f4fc6893520 100644 --- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue @@ -1,5 +1,5 @@ <script> -import ciIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; /** * Component that renders both the CI icon status and the job name. @@ -9,7 +9,7 @@ import ciIcon from '~/vue_shared/components/ci_icon.vue'; */ export default { components: { - ciIcon, + CiIcon, }, props: { name: { diff --git a/app/assets/javascripts/pipelines/components/performance_insights_modal.vue b/app/assets/javascripts/pipelines/components/performance_insights_modal.vue deleted file mode 100644 index fdbf0ca19bc..00000000000 --- a/app/assets/javascripts/pipelines/components/performance_insights_modal.vue +++ /dev/null @@ -1,171 +0,0 @@ -<script> -import { GlAlert, GlCard, GlLink, GlLoadingIcon, GlModal } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; -import { humanizeTimeInterval } from '~/lib/utils/datetime_utility'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import getPerformanceInsightsQuery from '../graphql/queries/get_performance_insights.query.graphql'; -import { performanceModalId } from '../constants'; -import { calculateJobStats, calculateSlowestFiveJobs } from '../utils'; - -export default { - name: 'PerformanceInsightsModal', - i18n: { - queuedCardHeader: s__('Pipeline|Longest queued job'), - queuedCardHelp: s__( - 'Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner', - ), - executedCardHeader: s__('Pipeline|Last executed job'), - executedCardHelp: s__( - 'Pipeline|The last executed job is the last job to start in the pipeline.', - ), - viewDependency: s__('Pipeline|View dependency'), - slowJobsTitle: s__('Pipeline|Five slowest jobs'), - feeback: __('Feedback issue'), - insightsLimit: s__('Pipeline|Only able to show first 100 results'), - }, - modal: { - title: s__('Pipeline|Performance insights'), - actionCancel: { - text: __('Close'), - attributes: { - variant: 'confirm', - }, - }, - }, - performanceModalId, - components: { - GlAlert, - GlCard, - GlLink, - GlModal, - GlLoadingIcon, - HelpPopover, - }, - inject: { - pipelineIid: { - default: '', - }, - pipelineProjectPath: { - default: '', - }, - }, - apollo: { - jobs: { - query: getPerformanceInsightsQuery, - variables() { - return { - fullPath: this.pipelineProjectPath, - iid: this.pipelineIid, - }; - }, - update(data) { - return data.project?.pipeline?.jobs; - }, - }, - }, - data() { - return { - jobs: null, - }; - }, - computed: { - longestQueuedJob() { - return calculateJobStats(this.jobs, 'queuedDuration'); - }, - lastExecutedJob() { - return calculateJobStats(this.jobs, 'startedAt'); - }, - slowestFiveJobs() { - return calculateSlowestFiveJobs(this.jobs); - }, - queuedDurationDisplay() { - return humanizeTimeInterval(this.longestQueuedJob.queuedDuration); - }, - showLimitMessage() { - return this.jobs.pageInfo.hasNextPage; - }, - }, -}; -</script> - -<template> - <gl-modal - :modal-id="$options.performanceModalId" - :title="$options.modal.title" - :action-cancel="$options.modal.actionCancel" - > - <gl-loading-icon v-if="$apollo.queries.jobs.loading" size="lg" /> - - <template v-else> - <gl-alert class="gl-mb-4" :dismissible="false"> - <p v-if="showLimitMessage" data-testid="limit-alert-text"> - {{ $options.i18n.insightsLimit }} - </p> - <gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/365902" class="gl-mt-5"> - {{ $options.i18n.feeback }} - </gl-link> - </gl-alert> - - <div class="gl-display-flex gl-justify-content-space-between gl-mt-2 gl-mb-7"> - <gl-card class="gl-w-half gl-mr-7 gl-text-center"> - <template #header> - <span class="gl-font-weight-bold">{{ $options.i18n.queuedCardHeader }}</span> - <help-popover> - {{ $options.i18n.queuedCardHelp }} - </help-popover> - </template> - <div class="gl-display-flex gl-flex-direction-column"> - <span - class="gl-font-weight-bold gl-font-size-h2 gl-mb-2" - data-testid="insights-queued-card-data" - > - {{ queuedDurationDisplay }} - </span> - <gl-link - :href="longestQueuedJob.detailedStatus.detailsPath" - data-testid="insights-queued-card-link" - > - {{ longestQueuedJob.name }} - </gl-link> - </div> - </gl-card> - <gl-card class="gl-w-half gl-text-center" data-testid="insights-executed-card"> - <template #header> - <span class="gl-font-weight-bold">{{ $options.i18n.executedCardHeader }}</span> - <help-popover> - {{ $options.i18n.executedCardHelp }} - </help-popover> - </template> - <div class="gl-display-flex gl-flex-direction-column"> - <span - class="gl-font-weight-bold gl-font-size-h2 gl-mb-2" - data-testid="insights-executed-card-data" - > - {{ lastExecutedJob.name }} - </span> - <gl-link - :href="lastExecutedJob.detailedStatus.detailsPath" - data-testid="insights-executed-card-link" - > - {{ $options.i18n.viewDependency }} - </gl-link> - </div> - </gl-card> - </div> - - <div class="gl-mt-7"> - <span class="gl-font-weight-bold">{{ $options.i18n.slowJobsTitle }}</span> - <div - v-for="job in slowestFiveJobs" - :key="job.name" - class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-3 gl-p-4 gl-border-t-1 gl-border-t-solid gl-border-b-0 gl-border-b-solid gl-border-gray-100" - > - <span data-testid="insights-slow-job-stage">{{ job.stage.name }}</span> - <gl-link :href="job.detailedStatus.detailsPath" data-testid="insights-slow-job-link">{{ - job.name - }}</gl-link> - </div> - </div> - </template> - </gl-modal> -</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue index 793e343a02a..3f1d7255a2b 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue @@ -1,9 +1,9 @@ <script> -import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; export default { components: { - tooltipOnTruncate, + TooltipOnTruncate, }, props: { jobName: { diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue index e485b38ce11..600832b7633 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue @@ -1,10 +1,9 @@ <script> -import { capitalize, escape } from 'lodash'; -import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; export default { components: { - tooltipOnTruncate, + TooltipOnTruncate, }, props: { stageName: { @@ -12,17 +11,12 @@ export default { required: true, }, }, - computed: { - formattedTitle() { - return capitalize(escape(this.stageName)); - }, - }, }; </script> <template> <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top"> <div class="gl-py-2 gl-text-truncate gl-font-weight-bold gl-w-20"> - {{ formattedTitle }} + {{ stageName }} </div> </tooltip-on-truncate> </template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js new file mode 100644 index 00000000000..1ca9e35c008 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js @@ -0,0 +1,14 @@ +import { get } from 'lodash'; + +export const accessors = { + rest: { + detailedStatus: ['details', 'status'], + }, + graphql: { + detailedStatus: 'detailedStatus', + }, +}; + +export const accessValue = (pipeline, dataMethod, path) => { + return get(pipeline, accessors[dataMethod][path]); +}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue index 670fa398536..211c5f117c7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/job_item.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue @@ -158,7 +158,7 @@ export default { :href="detailsPath" :title="tooltipText" :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" + class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" data-testid="job-with-link" @click.stop="hideTooltips" @mouseout="hideTooltips" diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue new file mode 100644 index 00000000000..a5c6dc98694 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue @@ -0,0 +1,132 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { accessValue } from './accessors/linked_pipelines_accessors'; +/** + * Renders the upstream/downstream portions of the pipeline mini graph. + */ +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + CiIcon, + }, + inject: { + dataMethod: { + default: 'rest', + }, + }, + props: { + triggeredBy: { + type: Array, + required: false, + default: () => [], + }, + triggered: { + type: Array, + required: false, + default: () => [], + }, + pipelinePath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + maxRenderedPipelines: 3, + }; + }, + computed: { + // Exactly one of these (triggeredBy and triggered) must be truthy. Never both. Never neither. + isUpstream() { + return Boolean(this.triggeredBy.length) && !this.triggered.length; + }, + isDownstream() { + return !this.triggeredBy.length && Boolean(this.triggered.length); + }, + linkedPipelines() { + return this.isUpstream ? this.triggeredBy : this.triggered; + }, + totalPipelineCount() { + return this.linkedPipelines.length; + }, + linkedPipelinesTrimmed() { + return this.totalPipelineCount > this.maxRenderedPipelines + ? this.linkedPipelines.slice(0, this.maxRenderedPipelines) + : this.linkedPipelines; + }, + shouldRenderCounter() { + return this.isDownstream && this.linkedPipelines.length > this.maxRenderedPipelines; + }, + counterLabel() { + return `+${this.linkedPipelines.length - this.maxRenderedPipelines}`; + }, + counterTooltipText() { + return sprintf(s__('LinkedPipelines|%{counterLabel} more downstream pipelines'), { + counterLabel: this.counterLabel, + }); + }, + }, + methods: { + pipelineTooltipText(pipeline) { + const { label } = accessValue(pipeline, this.dataMethod, 'detailedStatus'); + + return `${pipeline.project.name} - ${label}`; + }, + pipelineStatus(pipeline) { + // detailedStatus is graphQL, details.status is REST + return pipeline?.detailedStatus || pipeline?.details?.status; + }, + triggerButtonClass(pipeline) { + const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus'); + + return `ci-status-icon-${group}`; + }, + }, +}; +</script> + +<template> + <span + v-if="linkedPipelines" + :class="{ + 'is-upstream': isUpstream, + 'is-downstream': isDownstream, + }" + class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle" + > + <a + v-for="pipeline in linkedPipelinesTrimmed" + :key="pipeline.id" + v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }" + :href="pipeline.path" + :class="triggerButtonClass(pipeline)" + class="linked-pipeline-mini-item gl-display-inline-block gl-h-6 gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle" + data-testid="linked-pipeline-mini-item" + > + <ci-icon + is-borderless + is-interactive + css-classes="gl-rounded-full" + :size="24" + :status="pipelineStatus(pipeline)" + class="gl-align-items-center gl-border gl-display-inline-flex" + /> + </a> + + <a + v-if="shouldRenderCounter" + v-gl-tooltip="{ title: counterTooltipText }" + :title="counterTooltipText" + :href="pipelinePath" + class="gl-align-items-center gl-bg-gray-50 gl-display-inline-flex gl-font-sm gl-h-6 gl-justify-content-center gl-rounded-pill gl-text-decoration-none gl-text-gray-500 gl-w-7 linked-pipelines-counter linked-pipeline-mini-item" + data-testid="linked-pipeline-counter" + > + {{ counterLabel }} + </a> + </span> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue new file mode 100644 index 00000000000..993fa121d89 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue @@ -0,0 +1,103 @@ +<script> +import { GlIcon } from '@gitlab/ui'; +import PipelineStages from './pipeline_stages.vue'; +import LinkedPipelinesMiniList from './linked_pipelines_mini_list.vue'; +/** + * Renders the pipeline mini graph. + */ +export default { + components: { + GlIcon, + LinkedPipelinesMiniList, + PipelineStages, + }, + arrowStyles: [ + 'arrow-icon gl-display-inline-block gl-mx-1 gl-text-gray-500 gl-vertical-align-middle!', + ], + props: { + downstreamPipelines: { + type: Array, + required: false, + default: () => [], + }, + isMergeTrain: { + type: Boolean, + required: false, + default: false, + }, + pipelinePath: { + type: String, + required: false, + default: '', + }, + stages: { + type: Array, + required: true, + default: () => [], + }, + stagesClass: { + type: [Array, Object, String], + required: false, + default: '', + }, + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, + upstreamPipeline: { + type: Object, + required: false, + default: () => {}, + }, + }, + computed: { + hasDownstreamPipelines() { + return Boolean(this.downstreamPipelines.length); + }, + }, + methods: { + onPipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> +<template> + <div class="stage-cell" data-testid="pipeline-mini-graph"> + <linked-pipelines-mini-list + v-if="upstreamPipeline" + :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [ + upstreamPipeline, + ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" + data-testid="pipeline-mini-graph-upstream" + /> + <gl-icon + v-if="upstreamPipeline" + :class="$options.arrowStyles" + name="long-arrow" + data-testid="upstream-arrow-icon" + /> + <pipeline-stages + :is-merge-train="isMergeTrain" + :stages="stages" + :update-dropdown="updateDropdown" + :stages-class="stagesClass" + data-testid="pipeline-stages" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + @miniGraphStageClick="$emit('miniGraphStageClick')" + /> + <gl-icon + v-if="hasDownstreamPipelines" + :class="$options.arrowStyles" + name="long-arrow" + data-testid="downstream-arrow-icon" + /> + <linked-pipelines-mini-list + v-if="hasDownstreamPipelines" + :triggered="downstreamPipelines" + :pipeline-path="pipelinePath" + data-testid="pipeline-mini-graph-downstream" + /> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue index d7e55d36ff6..a68797a7235 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue @@ -77,6 +77,10 @@ export default { this.isDropdownOpen = true; this.isLoading = true; this.fetchJobs(); + + // used for tracking and is separate from event hub + // to avoid complexity with mixin + this.$emit('miniGraphStageClick'); }, fetchJobs() { axios diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue index 05cb2ebb769..e965dc5e6b0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue @@ -1,7 +1,7 @@ <script> -import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; +import PipelineStage from './pipeline_stage.vue'; /** - * Renders the pipeline mini graph. + * Renders the pipeline stages portion of the pipeline mini graph. */ export default { components: { @@ -36,7 +36,7 @@ export default { }; </script> <template> - <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle"> + <div data-testid="pipeline-stages" class="gl-display-inline gl-vertical-align-middle"> <div v-for="stage in stages" :key="stage.name" @@ -48,6 +48,7 @@ export default { :update-dropdown="updateDropdown" :is-merge-train="isMergeTrain" @pipelineActionRequestComplete="onPipelineActionRequestComplete" + @miniGraphStageClick="$emit('miniGraphStageClick')" /> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue index 05a1ceface3..2d2f649f651 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue @@ -10,6 +10,8 @@ import { import fuzzaldrinPlus from 'fuzzaldrin-plus'; import axios from '~/lib/utils/axios_utils'; import { __, s__ } from '~/locale'; +import Tracking from '~/tracking'; +import { TRACKING_CATEGORIES } from '../../constants'; export const i18n = { downloadArtifacts: __('Download artifacts'), @@ -29,6 +31,7 @@ export default { GlSearchBoxByType, GlLoadingIcon, }, + mixins: [Tracking.mixin()], inject: { artifactsEndpoint: { default: '', @@ -60,6 +63,10 @@ export default { }, methods: { fetchArtifacts() { + // refactor tracking based on action once this dropdown supports + // actions other than artifacts + this.track('click_artifacts_dropdown', { label: TRACKING_CATEGORIES.table }); + this.isLoading = true; // Replace the placeholder with the ID of the pipeline we are viewing const endpoint = this.artifactsEndpoint.replace( diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue index 7a08dacb824..dd62ffb27f7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -1,7 +1,8 @@ <script> import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; +import Tracking from '~/tracking'; import eventHub from '../../event_hub'; -import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '../../constants'; +import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants'; import PipelineMultiActions from './pipeline_multi_actions.vue'; import PipelinesManualActions from './pipelines_manual_actions.vue'; @@ -17,6 +18,7 @@ export default { PipelineMultiActions, PipelinesManualActions, }, + mixins: [Tracking.mixin()], props: { pipeline: { type: Object, @@ -52,6 +54,7 @@ export default { }, methods: { handleCancelClick() { + this.trackClick('click_cancel_button'); eventHub.$emit('openConfirmationModal', { pipeline: this.pipeline, endpoint: this.pipeline.cancel_path, @@ -59,8 +62,12 @@ export default { }, handleRetryClick() { this.isRetrying = true; + this.trackClick('click_retry_button'); eventHub.$emit('retryPipeline', this.pipeline.retry_path); }, + trackClick(action) { + this.track(action, { label: TRACKING_CATEGORIES.table }); + }, }, }; </script> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue index ef21673115e..eb70b5fbb7a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue @@ -83,9 +83,7 @@ export default { <span class="font-weight-bold">{{ __('Pipeline') }}</span> - <a :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" - >#{{ pipeline.id }}</a - > + <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a> <template v-if="hasRef"> {{ __('from') }} <a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 09d588aaafd..39d41415456 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,9 +1,10 @@ <script> import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; +import Tracking from '~/tracking'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { ICONS } from '../../constants'; +import { ICONS, TRACKING_CATEGORIES } from '../../constants'; import PipelineLabels from './pipeline_labels.vue'; export default { @@ -17,6 +18,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [Tracking.mixin()], props: { pipeline: { type: Object, @@ -114,6 +116,11 @@ export default { return this.pipeline?.commit?.title; }, }, + methods: { + trackClick(action) { + this.track(action, { label: TRACKING_CATEGORIES.table }); + }, + }, }; </script> <template> @@ -125,6 +132,7 @@ export default { :href="commitUrl" class="commit-row-message gl-text-gray-900" data-testid="commit-title" + @click="trackClick('click_commit_title')" >{{ commitTitle }}</gl-link > </tooltip-on-truncate> @@ -137,6 +145,7 @@ export default { class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3" data-testid="pipeline-url-link" data-qa-selector="pipeline_url_link" + @click="trackClick('click_pipeline_id')" >#{{ pipeline[pipelineKey] }}</gl-link > <!--Commit row--> @@ -154,11 +163,17 @@ export default { :href="mergeRequestRef.path" class="ref-name gl-mr-3" data-testid="merge-request-ref" + @click="trackClick('click_mr_ref')" >{{ mergeRequestRef.iid }}</gl-link > - <gl-link v-else :href="refUrl" class="ref-name gl-mr-3" data-testid="commit-ref-name">{{ - commitRef.name - }}</gl-link> + <gl-link + v-else + :href="refUrl" + class="ref-name gl-mr-3" + data-testid="commit-ref-name" + @click="trackClick('click_commit_name')" + >{{ commitRef.name }}</gl-link + > </tooltip-on-truncate> <gl-icon v-gl-tooltip @@ -167,9 +182,13 @@ export default { :title="__('Commit')" data-testid="commit-icon" /> - <gl-link :href="commitUrl" class="commit-sha mr-0" data-testid="commit-short-sha">{{ - commitShortSha - }}</gl-link> + <gl-link + :href="commitUrl" + class="commit-sha mr-0" + data-testid="commit-short-sha" + @click="trackClick('click_commit_sha')" + >{{ commitShortSha }}</gl-link + > <user-avatar-link v-if="commitAuthor" :link-href="commitAuthor.path" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 485e338f639..f9022be888a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -4,6 +4,7 @@ import { isEqual } from 'lodash'; import createFlash from '~/flash'; import { getParameterByName } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; +import Tracking from '~/tracking'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import { @@ -11,6 +12,7 @@ import { RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER, PipelineKeyOptions, + TRACKING_CATEGORIES, } from '../../constants'; import PipelinesMixin from '../../mixins/pipelines_mixin'; import PipelinesService from '../../services/pipelines_service'; @@ -35,7 +37,7 @@ export default { PipelinesTableComponent, TablePagination, }, - mixins: [PipelinesMixin], + mixins: [PipelinesMixin, Tracking.mixin()], props: { store: { type: Object, @@ -246,6 +248,8 @@ export default { params = this.onChangeWithFilter(params); this.updateContent(params); + + this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs }); }, successCallback(resp) { // Because we are polling & the user is interacting verify if the response received diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue index 4d28545a035..af089aebbbe 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue @@ -2,7 +2,9 @@ import { GlFilteredSearch } from '@gitlab/ui'; import { map } from 'lodash'; import { s__ } from '~/locale'; +import Tracking from '~/tracking'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { TRACKING_CATEGORIES } from '../../constants'; import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue'; import PipelineSourceToken from './tokens/pipeline_source_token.vue'; import PipelineStatusToken from './tokens/pipeline_status_token.vue'; @@ -19,6 +21,7 @@ export default { components: { GlFilteredSearch, }, + mixins: [Tracking.mixin()], props: { projectId: { type: String, @@ -110,6 +113,7 @@ export default { }, methods: { onSubmit(filters) { + this.track('click_filtered_search', { label: TRACKING_CATEGORIES.search }); this.$emit('filterPipelines', filters); }, }, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue index 47fffa8a6b2..16a747f6165 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue @@ -4,8 +4,10 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { s__, __, sprintf } from '~/locale'; +import Tracking from '~/tracking'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import eventHub from '../../event_hub'; +import { TRACKING_CATEGORIES } from '../../constants'; export default { directives: { @@ -17,6 +19,7 @@ export default { GlDropdownItem, GlIcon, }, + mixins: [Tracking.mixin()], props: { actions: { type: Array, @@ -66,7 +69,6 @@ export default { createFlash({ message: __('An error occurred while making the request.') }); }); }, - isActionDisabled(action) { if (action.playable === undefined) { return false; @@ -74,6 +76,9 @@ export default { return !action.playable; }, + trackClick() { + this.track('click_manual_actions', { label: TRACKING_CATEGORIES.table }); + }, }, }; </script> @@ -86,6 +91,7 @@ export default { right lazy icon="play" + @shown="trackClick" > <gl-dropdown-item v-for="action in actions" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue index e765a8cd86c..936ae4da1ec 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -1,6 +1,7 @@ <script> -import { CHILD_VIEW } from '~/pipelines/constants'; +import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; +import Tracking from '~/tracking'; import PipelinesTimeago from './time_ago.vue'; export default { @@ -8,6 +9,7 @@ export default { CiBadge, PipelinesTimeago, }, + mixins: [Tracking.mixin()], props: { pipeline: { type: Object, @@ -26,6 +28,11 @@ export default { return this.viewType === CHILD_VIEW; }, }, + methods: { + trackClick() { + this.track('click_ci_status_badge', { label: TRACKING_CATEGORIES.table }); + }, + }, }; </script> @@ -37,6 +44,7 @@ export default { :show-text="!isChildView" :icon-classes="'gl-vertical-align-middle!'" data-qa-selector="pipeline_commit_status" + @ciStatusBadgeClick="trackClick" /> <pipelines-timeago class="gl-mt-3" :pipeline="pipeline" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 53da98434b0..f6e46c090d3 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,8 +1,10 @@ <script> import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; import { s__, __ } from '~/locale'; +import Tracking from '~/tracking'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import eventHub from '../../event_hub'; -import PipelineMiniGraph from './pipeline_mini_graph.vue'; +import { TRACKING_CATEGORIES } from '../../constants'; import PipelineOperations from './pipeline_operations.vue'; import PipelineStopModal from './pipeline_stop_modal.vue'; import PipelineTriggerer from './pipeline_triggerer.vue'; @@ -17,8 +19,6 @@ const DEFAULT_TH_CLASSES = export default { components: { GlTableLite, - LinkedPipelinesMiniList: () => - import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), PipelineMiniGraph, PipelineOperations, PipelinesStatusBadge, @@ -70,6 +70,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [Tracking.mixin()], props: { pipelines: { type: Array, @@ -126,6 +127,9 @@ export default { onPipelineActionRequestComplete() { eventHub.$emit('refreshPipelinesTable'); }, + trackPipelineMiniGraph() { + this.track('click_minigraph', { label: TRACKING_CATEGORIES.table }); + }, }, TBODY_TR_ATTR: { 'data-testid': 'pipeline-table-row', @@ -169,29 +173,15 @@ export default { </template> <template #cell(stages)="{ item }"> - <div class="stage-cell"> - <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> - <div></div> - <linked-pipelines-mini-list - v-if="item.triggered_by" - :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [ - item.triggered_by, - ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" - data-testid="mini-graph-upstream" - /> - <pipeline-mini-graph - v-if="item.details && item.details.stages && item.details.stages.length > 0" - :stages="item.details.stages" - :update-dropdown="updateGraphDropdown" - @pipelineActionRequestComplete="onPipelineActionRequestComplete" - /> - <linked-pipelines-mini-list - v-if="item.triggered.length" - :triggered="item.triggered" - :pipeline-path="item.path" - data-testid="mini-graph-downstream" - /> - </div> + <pipeline-mini-graph + :downstream-pipelines="item.triggered" + :pipeline-path="item.path" + :stages="item.details.stages" + :update-dropdown="updateGraphDropdown" + :upstream-pipeline="item.triggered_by" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + @miniGraphStageClick="trackPipelineMiniGraph" + /> </template> <template #cell(actions)="{ item }"> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 7b38f870cb6..327633dcb1a 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -110,4 +110,8 @@ export const DEFAULT_FIELDS = [ }, ]; -export const performanceModalId = 'performanceInsightsModal'; +export const TRACKING_CATEGORIES = { + table: 'pipelines_table_component', + tabs: 'pipelines_filter_tabs', + search: 'pipelines_filtered_search', +}; diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql deleted file mode 100644 index 25e990c8934..00000000000 --- a/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql +++ /dev/null @@ -1,28 +0,0 @@ -query getPerformanceInsightsData($fullPath: ID!, $iid: ID!) { - project(fullPath: $fullPath) { - id - pipeline(iid: $iid) { - id - jobs { - pageInfo { - hasNextPage - } - nodes { - id - duration - detailedStatus { - id - detailsPath - } - name - stage { - id - name - } - startedAt - queuedDuration - } - } - } - } -} diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql index 641ec7a3cf6..b0f875160d4 100644 --- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql +++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql @@ -11,6 +11,7 @@ query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) { } nodes { artifacts { + # eslint-disable-next-line @graphql-eslint/require-id-when-available nodes { downloadPath fileType diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js index 2fedd7e7a98..c9e60756407 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_header.js +++ b/app/assets/javascripts/pipelines/pipeline_details_header.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import pipelineHeader from './components/header_component.vue'; +import PipelineHeader from './components/header_component.vue'; Vue.use(VueApollo); @@ -16,7 +16,7 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou new Vue({ el, components: { - pipelineHeader, + PipelineHeader, }, apolloProvider, provide: { diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js index 7051d356089..508f188c229 100644 --- a/app/assets/javascripts/pipelines/pipeline_tabs.js +++ b/app/assets/javascripts/pipelines/pipeline_tabs.js @@ -20,6 +20,8 @@ export const createAppOptions = (selector, apolloProvider) => { const { canGenerateCodequalityReports, codequalityReportDownloadPath, + codequalityBlobPath, + codequalityProjectPath, downloadablePathForReportType, exposeSecurityDashboard, exposeLicenseScanningData, @@ -40,9 +42,12 @@ export const createAppOptions = (selector, apolloProvider) => { hasTestReport, emptyStateImagePath, artifactsExpiredImagePath, + isFullCodequalityReportAvailable, testsCount, } = dataset; + // TODO remove projectPath variable once https://gitlab.com/gitlab-org/gitlab/-/issues/371641 is resolved + const projectPath = fullPath; const defaultTabValue = getPipelineDefaultTab(window.location.href); return { @@ -63,6 +68,10 @@ export const createAppOptions = (selector, apolloProvider) => { provide: { canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports), codequalityReportDownloadPath, + codequalityBlobPath, + codequalityProjectPath, + isFullCodequalityReportAvailable: parseBoolean(isFullCodequalityReportAvailable), + projectPath, defaultTabValue, downloadablePathForReportType, exposeSecurityDashboard: parseBoolean(exposeSecurityDashboard), diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 83e00b80426..588d15495ab 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -153,24 +153,3 @@ export const getPipelineDefaultTab = (url) => { return null; }; - -export const calculateJobStats = (jobs, sortField) => { - const jobNodes = [...jobs.nodes]; - - const sorted = jobNodes.sort((a, b) => { - return b[sortField] - a[sortField]; - }); - - return sorted[0]; -}; - -export const calculateSlowestFiveJobs = (jobs) => { - const jobNodes = [...jobs.nodes]; - const limit = 5; - - return jobNodes - .sort((a, b) => { - return b.duration - a.duration; - }) - .slice(0, limit); -}; |