diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 13:34:06 +0300 |
commit | 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch) | |
tree | d7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/assets/javascripts/pipelines | |
parent | 446d496a6d000c73a304be52587cd9bbc7493136 (diff) |
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines')
48 files changed, 452 insertions, 612 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue index 2482af2c7f0..e44dedfe2ee 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag.vue @@ -1,14 +1,14 @@ <script> import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { isEmpty } from 'lodash'; -import { __ } from '~/locale'; import { fetchPolicies } from '~/lib/graphql'; +import { __ } from '~/locale'; +import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants'; import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql'; -import DagGraph from './dag_graph.vue'; -import DagAnnotations from './dag_annotations.vue'; -import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants'; import { parseData } from '../parsing_utils'; -import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants'; +import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants'; +import DagAnnotations from './dag_annotations.vue'; +import DagGraph from './dag_graph.vue'; export default { // eslint-disable-next-line @gitlab/require-i18n-strings diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue index 5ba0604fa01..7646c11773c 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue @@ -1,7 +1,10 @@ <script> import * as d3 from 'd3'; import { uniqueId } from 'lodash'; +import { PARSE_FAILURE } from '../../constants'; +import { getMaxNodes, removeOrphanNodes } from '../parsing_utils'; import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants'; +import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils'; import { currentIsLive, getLiveLinksAsDict, @@ -10,9 +13,6 @@ import { toggleLinkHighlight, togglePathHighlights, } from './interactions'; -import { getMaxNodes, removeOrphanNodes } from '../parsing_utils'; -import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils'; -import { PARSE_FAILURE } from '../../constants'; export default { viewOptions: { diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 0ce94d4f02f..1df693704d4 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,9 +1,10 @@ <script> import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; import { reportToSentry } from './utils'; /** @@ -62,7 +63,7 @@ export default { * */ onClickAction() { - this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`); + this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`); this.isDisabled = true; this.isLoading = true; diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index 6f0deccfef6..caa269f5095 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -2,5 +2,11 @@ export const DOWNSTREAM = 'downstream'; export const MAIN = 'main'; export const UPSTREAM = 'upstream'; +/* + this value is based on the gl-pipeline-job-width class + plus some extra for the margins +*/ +export const ONE_COL_WIDTH = 180; + export const REST = 'rest'; export const GRAPHQL = 'graphql'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index cd403757fe6..93156d5d05b 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,9 +1,9 @@ <script> import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; import LinksLayer from '../graph_shared/links_layer.vue'; +import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; -import { DOWNSTREAM, MAIN, UPSTREAM } from './constants'; import { reportToSentry } from './utils'; export default { @@ -86,11 +86,11 @@ export default { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, mounted() { - this.measurements = this.getMeasurements(); + this.getMeasurements(); }, methods: { getMeasurements() { - return { + this.measurements = { width: this.$refs[this.containerId].scrollWidth, height: this.$refs[this.containerId].scrollHeight, }; @@ -101,6 +101,13 @@ export default { setJob(jobName) { this.hoveredJobName = jobName; }, + slidePipelineContainer() { + this.$refs.mainPipelineContainer.scrollBy({ + left: ONE_COL_WIDTH, + top: 0, + behavior: 'smooth', + }); + }, togglePipelineExpanded(jobName, expanded) { this.pipelineExpanded = { expanded, @@ -116,8 +123,9 @@ export default { <template> <div class="js-pipeline-graph"> <div - class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" - :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }" + 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': !isLinkedPipeline }" > <linked-graph-wrapper> <template #upstream> @@ -153,6 +161,7 @@ export default { :pipeline-id="pipeline.id" @refreshPipelineGraph="$emit('refreshPipelineGraph')" @jobHover="setJob" + @updateMeasurements="getMeasurements" /> </links-layer> </div> @@ -160,11 +169,13 @@ export default { <template #downstream> <linked-pipelines-column v-if="showDownstreamPipelines" + class="gl-mr-6" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" :type="$options.pipelineTypeConstants.DOWNSTREAM" @downstreamHovered="setJob" @pipelineExpandToggle="togglePipelineExpanded" + @scrollContainer="slidePipelineContainer" @error="onError" /> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue index 2164dbf4d55..abbf8df6eed 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -1,10 +1,10 @@ <script> -import { escape, capitalize } from 'lodash'; import { GlLoadingIcon } from '@gitlab/ui'; -import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; -import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue'; +import { escape, capitalize } from 'lodash'; import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; +import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue'; +import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; import { reportToSentry } from './utils'; export default { 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 08d6162aeb8..f6aee8c5fcf 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -60,7 +60,7 @@ export default { > <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> <span class="gl-display-flex gl-align-items-center gl-min-w-0"> - <ci-icon :status="group.status" :size="24" /> + <ci-icon :status="group.status" :size="24" class="gl-line-height-0" /> <span class="gl-text-truncate mw-70p gl-pl-3"> {{ group.name }} </span> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 8262d728a24..46ef0457d40 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -1,11 +1,12 @@ <script> import { GlTooltipDirective, GlLink } from '@gitlab/ui'; -import ActionComponent from './action_component.vue'; -import JobNameComponent from './job_name_component.vue'; -import { sprintf } from '~/locale'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; +import { sprintf } from '~/locale'; import { accessValue } from './accessors'; +import ActionComponent from './action_component.vue'; import { REST } from './constants'; +import JobNameComponent from './job_name_component.vue'; import { reportToSentry } from './utils'; /** @@ -144,7 +145,7 @@ export default { }, methods: { hideTooltips() { - this.$root.$emit('bv::hide::tooltip'); + this.$root.$emit(BV_HIDE_TOOLTIP); }, pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 23a38fc053e..fffd8e1818a 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -30,7 +30,7 @@ export default { </script> <template> <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center"> - <ci-icon :size="iconSize" :status="status" /> + <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" /> <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block"> {{ name }} </span> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index d18e604f087..add7b3445f7 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,7 +1,8 @@ <script> import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; -import CiStatus from '~/vue_shared/components/ci_icon.vue'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; +import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { accessValue } from './accessors'; import { DOWNSTREAM, REST, UPSTREAM } from './constants'; import { reportToSentry } from './utils'; @@ -126,7 +127,7 @@ export default { this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded); }, hideTooltips() { - this.$root.$emit('bv::hide::tooltip'); + this.$root.$emit(BV_HIDE_TOOLTIP); }, onDownstreamHovered() { this.$emit('downstreamHovered', this.sourceJobName); diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 40e6a01b88c..3ce77a1c60a 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -1,8 +1,8 @@ <script> import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; -import LinkedPipeline from './linked_pipeline.vue'; import { LOAD_FAILURE } from '../../constants'; -import { UPSTREAM } from './constants'; +import { ONE_COL_WIDTH, UPSTREAM } from './constants'; +import LinkedPipeline from './linked_pipeline.vue'; import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; export default { @@ -39,6 +39,7 @@ export default { 'gl-pl-3', 'gl-mb-5', ], + minWidth: `${ONE_COL_WIDTH}px`, computed: { columnClass() { const positionValues = { @@ -47,12 +48,6 @@ export default { }; return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; }, - graphPosition() { - return this.isUpstream ? 'left' : 'right'; - }, - isUpstream() { - return this.type === UPSTREAM; - }, computedTitleClasses() { const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding'] @@ -60,6 +55,15 @@ export default { return [...this.$options.titleClasses, ...positionalClasses]; }, + graphPosition() { + return this.isUpstream ? 'left' : 'right'; + }, + isUpstream() { + return this.type === UPSTREAM; + }, + minWidth() { + return this.isUpstream ? 0 : this.$options.minWidth; + }, }, methods: { getPipelineData(pipeline) { @@ -79,6 +83,7 @@ export default { }, result() { this.loadingPipelineId = null; + this.$emit('scrollContainer'); }, error(err, _vm, _key, type) { this.$emit('error', LOAD_FAILURE); @@ -130,6 +135,9 @@ export default { this.$emit('pipelineExpandToggle', jobName, expanded); }, + showContainer(id) { + return this.isExpanded(id) || this.isLoadingPipeline(id); + }, }, }; </script> @@ -158,9 +166,13 @@ export default { @pipelineClicked="onPipelineClick(pipeline)" @pipelineExpandToggle="onPipelineExpandToggle" /> - <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block"> + <div + v-if="showContainer(pipeline.id)" + :style="{ minWidth }" + class="gl-display-inline-block" + > <pipeline-graph - v-if="currentPipeline" + v-if="isExpanded(pipeline.id)" :type="type" class="d-inline-block gl-mt-n2" :pipeline="currentPipeline" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue index 2f1390e07d1..0d1ff94c275 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue @@ -1,6 +1,6 @@ <script> -import LinkedPipeline from './linked_pipeline.vue'; import { UPSTREAM } from './constants'; +import LinkedPipeline from './linked_pipeline.vue'; import { reportToSentry } from './utils'; export default { 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 65f8c231885..0a762563114 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,11 +1,11 @@ <script> import { capitalize, escape, isEmpty } from 'lodash'; import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; -import JobItem from './job_item.vue'; -import JobGroupDropdown from './job_group_dropdown.vue'; +import { accessValue } from './accessors'; import ActionComponent from './action_component.vue'; import { GRAPHQL } from './constants'; -import { accessValue } from './accessors'; +import JobGroupDropdown from './job_group_dropdown.vue'; +import JobItem from './job_item.vue'; import { reportToSentry } from './utils'; export default { @@ -67,6 +67,9 @@ export default { errorCaptured(err, _vm, info) { reportToSentry('stage_column_component', `error: ${err}, info: ${info}`); }, + mounted() { + this.$emit('updateMeasurements'); + }, methods: { getGroupId(group) { return accessValue(GRAPHQL, 'groupId', group); @@ -75,17 +78,13 @@ export default { return `ci-badge-${escape(group.name)}`; }, isFadedOut(jobName) { - return ( - this.jobHovered && - this.highlightedJobs.length > 1 && - !this.highlightedJobs.includes(jobName) - ); + return this.highlightedJobs.length > 1 && !this.highlightedJobs.includes(jobName); }, }, }; </script> <template> - <main-graph-wrapper class="gl-px-6"> + <main-graph-wrapper class="gl-px-6" data-testid="stage-column"> <template #stages> <div data-testid="stage-column-title" @@ -123,12 +122,9 @@ export default { :class="{ 'gl-opacity-3': isFadedOut(group.name) }" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <job-group-dropdown - v-else - :group="group" - :pipeline-id="pipelineId" - :class="{ 'gl-opacity-3': isFadedOut(group.name) }" - /> + <div v-else :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> + <job-group-dropdown :group="group" :pipeline-id="pipelineId" /> + </div> </div> </template> </main-graph-wrapper> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue index 059e8f9f8db..2cee2fbbd8f 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue @@ -1,9 +1,9 @@ <script> import { isEmpty, escape } from 'lodash'; import stageColumnMixin from '../../mixins/stage_column_mixin'; -import JobItem from './job_item.vue'; -import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; +import JobGroupDropdown from './job_group_dropdown.vue'; +import JobItem from './job_item.vue'; import { reportToSentry } from './utils'; export default { diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js index 65c215be794..202498fb188 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js +++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js @@ -40,10 +40,10 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => { // positioned in the center of the job node by adding half the height // of the job pill. const paddingLeft = parseFloat( - window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'), + window.getComputedStyle(containerEl, null).getPropertyValue('padding-left') || 0, ); const paddingTop = parseFloat( - window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'), + window.getComputedStyle(containerEl, null).getPropertyValue('padding-top') || 0, ); const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft; diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue index 89444076ae0..289e04e02c5 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -118,22 +118,20 @@ export default { <div class="gl-display-flex gl-relative"> <svg id="link-svg" - class="gl-absolute" + class="gl-absolute gl-pointer-events-none" :viewBox="viewBox" :width="`${containerMeasurements.width}px`" :height="`${containerMeasurements.height}px`" > - <template> - <path - v-for="link in links" - :key="link.path" - :ref="link.ref" - :d="link.path" - class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" - :class="getLinkClasses(link)" - :stroke-width="$options.STROKE_WIDTH" - /> - </template> + <path + v-for="link in links" + :key="link.path" + :ref="link.ref" + :d="link.path" + class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" + :class="getLinkClasses(link)" + :stroke-width="$options.STROKE_WIDTH" + /> </svg> <slot></slot> </div> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue index 0993892a574..1c1bc7ecb2a 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -74,13 +74,15 @@ export default { <div v-else> <gl-alert v-if="showAlert" - class="gl-w-max-content gl-ml-4" + class="gl-ml-4 gl-mb-4" :primary-button-text="$options.i18n.showLinksAnyways" @primaryAction="overrideShowLinks" @dismiss="dismissAlert" > {{ $options.i18n.tooManyJobs }} </gl-alert> - <slot></slot> + <div class="gl-display-flex gl-relative"> + <slot></slot> + </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index a20bd70e90a..4ce43b92c93 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,13 +1,13 @@ <script> import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } 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 { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; -import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql'; +import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; +import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql'; import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql'; import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql'; -import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql'; -import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; +import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql'; const DELETE_MODAL_ID = 'pipeline-delete-modal'; const POLL_INTERVAL = 10000; diff --git a/app/assets/javascripts/pipelines/components/legacy_header_component.vue b/app/assets/javascripts/pipelines/components/legacy_header_component.vue deleted file mode 100644 index c7b72be36ad..00000000000 --- a/app/assets/javascripts/pipelines/components/legacy_header_component.vue +++ /dev/null @@ -1,132 +0,0 @@ -<script> -import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui'; -import ciHeader from '~/vue_shared/components/header_ci_component.vue'; -import eventHub from '../event_hub'; -import { __ } from '~/locale'; - -const DELETE_MODAL_ID = 'pipeline-delete-modal'; - -export default { - name: 'PipelineHeaderSection', - components: { - ciHeader, - GlLoadingIcon, - GlModal, - GlButton, - }, - directives: { - GlModal: GlModalDirective, - }, - props: { - pipeline: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - }, - data() { - return { - isCanceling: false, - isRetrying: false, - isDeleting: false, - }; - }, - - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; - }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.pipeline).length; - }, - deleteModalConfirmationText() { - return __( - 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.', - ); - }, - }, - - methods: { - cancelPipeline() { - this.isCanceling = true; - eventHub.$emit('headerPostAction', this.pipeline.cancel_path); - }, - retryPipeline() { - this.isRetrying = true; - eventHub.$emit('headerPostAction', this.pipeline.retry_path); - }, - deletePipeline() { - this.isDeleting = true; - eventHub.$emit('headerDeleteAction', this.pipeline.delete_path); - }, - }, - DELETE_MODAL_ID, -}; -</script> -<template> - <div class="pipeline-header-container"> - <ci-header - v-if="shouldRenderContent" - :status="status" - :item-id="pipeline.id" - :time="pipeline.created_at" - :user="pipeline.user" - item-name="Pipeline" - > - <gl-button - v-if="pipeline.retry_path" - :loading="isRetrying" - :disabled="isRetrying" - data-testid="retryButton" - category="secondary" - variant="info" - @click="retryPipeline()" - > - {{ __('Retry') }} - </gl-button> - - <gl-button - v-if="pipeline.cancel_path" - :loading="isCanceling" - :disabled="isCanceling" - data-testid="cancelPipeline" - class="gl-ml-3" - category="primary" - variant="danger" - @click="cancelPipeline()" - > - {{ __('Cancel running') }} - </gl-button> - - <gl-button - v-if="pipeline.delete_path" - v-gl-modal="$options.DELETE_MODAL_ID" - :loading="isDeleting" - :disabled="isDeleting" - data-testid="deletePipeline" - class="gl-ml-3" - category="secondary" - variant="danger" - > - {{ __('Delete') }} - </gl-button> - </ci-header> - - <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" /> - - <gl-modal - :modal-id="$options.DELETE_MODAL_ID" - :title="__('Delete pipeline')" - :ok-title="__('Delete pipeline')" - ok-variant="danger" - @ok="deletePipeline()" - > - <p> - {{ deleteModalConfirmationText }} - </p> - </gl-modal> - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue index 8636808b69e..4a7ee3b2af7 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -1,13 +1,13 @@ <script> import { GlAlert } from '@gitlab/ui'; import { __ } from '~/locale'; +import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; +import { createJobsHash, generateJobNeedsDict } from '../../utils'; import { generateLinksData } from '../graph_shared/drawing_utils'; +import { parseData } from '../parsing_utils'; import JobPill from './job_pill.vue'; import StagePill from './stage_pill.vue'; -import { parseData } from '../parsing_utils'; -import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; -import { createJobsHash, generateJobNeedsDict } from '../../utils'; -import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; export default { components: { @@ -224,17 +224,15 @@ export default { data-testid="graph-container" > <svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute"> - <template> - <path - v-for="link in links" - :key="link.path" - :ref="link.ref" - :d="link.path" - class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" - :class="getLinkClasses(link)" - :stroke-width="$options.STROKE_WIDTH" - /> - </template> + <path + v-for="link in links" + :key="link.path" + :ref="link.ref" + :d="link.path" + class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" + :class="getLinkClasses(link)" + :stroke-width="$options.STROKE_WIDTH" + /> </svg> <div v-for="(stage, index) in pipelineStages" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue index ee26ea2f007..8a656bb47f4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -1,24 +1,13 @@ <script> import { GlButton } from '@gitlab/ui'; -import { isExperimentEnabled } from '~/lib/utils/experimentation'; import { s__ } from '~/locale'; -import Tracking from '~/tracking'; export default { i18n: { - control: { - infoMessage: s__(`Pipelines|Continuous Integration can help - catch bugs by running your tests automatically, - while Continuous Deployment can help you deliver - code to your product environment.`), - buttonMessage: s__('Pipelines|Get started with Pipelines'), - }, - experiment: { - infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build, + infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating.`), - buttonMessage: s__('Pipelines|Get started with CI/CD'), - }, + buttonMessage: s__('Pipelines|Get started with CI/CD'), }, name: 'PipelinesEmptyState', components: { @@ -38,23 +27,6 @@ export default { required: true, }, }, - mounted() { - this.track('viewed'); - }, - methods: { - track(action) { - if (!gon.tracking_data) { - return; - } - - const { category, value, label, property } = gon.tracking_data; - - Tracking.event(category, action, { value, label, property }); - }, - isExperimentEnabled() { - return isExperimentEnabled('pipelinesEmptyState'); - }, - }, }; </script> <template> @@ -70,11 +42,7 @@ export default { {{ s__('Pipelines|Build with confidence') }} </h4> <p data-testid="info-text"> - {{ - isExperimentEnabled() - ? $options.i18n.experiment.infoMessage - : $options.i18n.control.infoMessage - }} + {{ $options.i18n.infoMessage }} </p> <div class="gl-text-center"> @@ -83,13 +51,8 @@ export default { variant="info" category="primary" data-testid="get-started-pipelines" - @click="track('documentation_clicked')" > - {{ - isExperimentEnabled() - ? $options.i18n.experiment.buttonMessage - : $options.i18n.control.buttonMessage - }} + {{ $options.i18n.buttonMessage }} </gl-button> </div> </template> 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 1569b326b31..24b5c85c9d6 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 @@ -1,9 +1,9 @@ <script> /* eslint-disable vue/no-v-html */ -import { isEmpty } from 'lodash'; import { GlLink, GlModal } from '@gitlab/ui'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { isEmpty } from 'lodash'; import { __, s__, sprintf } from '~/locale'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; /** * Pipeline Stop Modal. 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 d1bac078642..823ada133d2 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -77,6 +77,15 @@ export default { >{{ __('latest') }}</gl-badge > <gl-badge + v-if="pipeline.flags.merge_train_pipeline" + v-gl-tooltip + :title="__('This is a merge train pipeline')" + variant="info" + size="sm" + data-testid="pipeline-url-train" + >{{ __('train') }}</gl-badge + > + <gl-badge v-if="pipeline.flags.yaml_errors" v-gl-tooltip :title="pipeline.yaml_errors" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index ec7c5764be1..48009a9fcb8 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,28 +1,34 @@ <script> +import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { isEqual } from 'lodash'; -import { GlIcon } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import PipelinesService from '../../services/pipelines_service'; -import pipelinesMixin from '../../mixins/pipelines'; -import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { __, s__ } from '~/locale'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; +import PipelinesMixin from '../../mixins/pipelines_mixin'; +import PipelinesService from '../../services/pipelines_service'; +import { validateParams } from '../../utils'; +import SvgBlankState from './blank_state.vue'; +import EmptyState from './empty_state.vue'; import NavigationControls from './nav_controls.vue'; -import { getParameterByName } from '~/lib/utils/common_utils'; -import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; -import { validateParams } from '../../utils'; -import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; +import PipelinesTableComponent from './pipelines_table.vue'; export default { components: { - TablePagination, + EmptyState, + GlIcon, + GlLoadingIcon, NavigationTabs, NavigationControls, PipelinesFilteredSearch, - GlIcon, + PipelinesTableComponent, + SvgBlankState, + TablePagination, }, - mixins: [pipelinesMixin, CIPaginationMixin], + mixins: [PipelinesMixin], props: { store: { type: Object, @@ -217,6 +223,20 @@ export default { this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams }; }, methods: { + onChangeTab(scope) { + if (this.scope === scope) { + return; + } + + let params = { + scope, + page: '1', + }; + + params = this.onChangeWithFilter(params); + + this.updateContent(params); + }, successCallback(resp) { // Because we are polling & the user is interacting verify if the response received // matches the last request made diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue index 1ea71610897..6890cbb9bed 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -1,7 +1,7 @@ <script> -import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { deprecatedCreateFlash as flash } from '~/flash'; import { s__, __, sprintf } from '~/locale'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import eventHub from '../../event_hub'; @@ -11,10 +11,10 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - GlIcon, GlCountdown, - GlButton, - GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlIcon, }, props: { actions: { @@ -61,7 +61,7 @@ export default { }) .catch(() => { this.isLoading = false; - flash(__('An error occurred while making the request.')); + createFlash({ message: __('An error occurred while making the request.') }); }); }, @@ -76,39 +76,27 @@ export default { }; </script> <template> - <div class="btn-group"> - <button - v-gl-tooltip - type="button" - :disabled="isLoading" - class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" - :title="__('Run manual or delayed jobs')" - data-toggle="dropdown" - :aria-label="__('Run manual or delayed jobs')" + <gl-dropdown + v-gl-tooltip + :title="__('Run manual or delayed jobs')" + :loading="isLoading" + data-testid="pipelines-manual-actions-dropdown" + right + icon="play" + > + <gl-dropdown-item + v-for="action in actions" + :key="action.path" + :disabled="isActionDisabled(action)" + @click="onClickAction(action)" > - <gl-icon name="play" class="icon-play" /> - <gl-icon name="chevron-down" /> - <gl-loading-icon v-if="isLoading" /> - </button> - - <ul class="dropdown-menu dropdown-menu-right"> - <li v-for="action in actions" :key="action.path"> - <gl-button - category="tertiary" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" - class="js-pipeline-action-link" - @click="onClickAction(action)" - > - <div class="d-flex justify-content-between flex-wrap"> - {{ action.name }} - <span v-if="action.scheduled_at"> - <gl-icon name="clock" /> - <gl-countdown :end-date-string="action.scheduled_at" /> - </span> - </div> - </gl-button> - </li> - </ul> - </div> + <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"> + {{ action.name }} + <span v-if="action.scheduled_at"> + <gl-icon name="clock" /> + <gl-countdown :end-date-string="action.scheduled_at" /> + </span> + </div> + </gl-dropdown-item> + </gl-dropdown> </template> 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 127503f1307..492c562ec5c 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,10 +2,10 @@ import { GlFilteredSearch } from '@gitlab/ui'; import { map } from 'lodash'; import { __, s__ } from '~/locale'; -import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue'; import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue'; import PipelineStatusToken from './tokens/pipeline_status_token.vue'; import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue'; +import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue'; export default { userType: 'username', 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 6c60594efca..24c67184e56 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,8 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; -import PipelinesTableRowComponent from './pipelines_table_row.vue'; -import PipelineStopModal from './pipeline_stop_modal.vue'; import eventHub from '../../event_hub'; +import PipelineStopModal from './pipeline_stop_modal.vue'; +import PipelinesTableRowComponent from './pipelines_table_row.vue'; /** * Pipelines Table Component. diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index b6c4e617a90..572abe2a24a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -1,22 +1,17 @@ <script> import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; -import eventHub from '../../event_hub'; import { __ } from '~/locale'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; +import CommitComponent from '~/vue_shared/components/commit.vue'; +import { PIPELINES_TABLE } from '../../constants'; +import eventHub from '../../event_hub'; +import PipelineTriggerer from './pipeline_triggerer.vue'; +import PipelineUrl from './pipeline_url.vue'; import PipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import PipelineStage from './stage.vue'; -import PipelineUrl from './pipeline_url.vue'; -import PipelineTriggerer from './pipeline_triggerer.vue'; import PipelinesTimeago from './time_ago.vue'; -import CommitComponent from '~/vue_shared/components/commit.vue'; -import { PIPELINES_TABLE } from '../../constants'; -/** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ export default { i18n: { cancelTitle: __('Cancel'), @@ -127,116 +122,30 @@ export default { return commitAuthorInformation; }, - - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ commitTag() { - if (this.pipeline.ref && this.pipeline.ref.tag) { - return this.pipeline.ref.tag; - } - return undefined; + return this.pipeline?.ref?.tag; }, - - /** - * If provided, returns the commit ref. - * Needed to render the commit component column. - * - * Matches `path` prop sent in the API to `ref_url` prop needed - * in the commit component. - * - * @returns {Object|Undefined} - */ commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'path') { - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } - - return undefined; + return this.pipeline?.ref; }, - - /** - * If provided, returns the commit url. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ commitUrl() { - if (this.pipeline.commit && this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; + return this.pipeline?.commit?.commit_path; }, - - /** - * If provided, returns the commit short sha. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ commitShortSha() { - if (this.pipeline.commit && this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; + return this.pipeline?.commit?.short_id; }, - - /** - * If provided, returns the commit title. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ commitTitle() { - if (this.pipeline.commit && this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; + return this.pipeline?.commit?.title; }, - - /** - * Timeago components expects a number - * - * @return {type} description - */ pipelineDuration() { - if (this.pipeline.details && this.pipeline.details.duration) { - return this.pipeline.details.duration; - } - - return 0; + return this.pipeline?.details?.duration ?? 0; }, - - /** - * Timeago component expects a String. - * - * @return {String} - */ pipelineFinishedAt() { - if (this.pipeline.details && this.pipeline.details.finished_at) { - return this.pipeline.details.finished_at; - } - - return ''; + return this.pipeline?.details?.finished_at ?? ''; }, - pipelineStatus() { - if (this.pipeline.details && this.pipeline.details.status) { - return this.pipeline.details.status; - } - return {}; + return this.pipeline?.details?.status ?? {}; }, - displayPipelineActions() { return ( this.pipeline.flags.retryable || @@ -245,11 +154,9 @@ export default { this.pipeline.details.artifacts.length ); }, - isChildView() { return this.viewType === 'child'; }, - isCancelling() { return this.cancelingPipeline === this.pipeline.id; }, @@ -355,7 +262,7 @@ export default { :title="$options.i18n.redeployTitle" :disabled="isRetrying" :loading="isRetrying" - class="js-pipelines-retry-button btn-retry" + class="js-pipelines-retry-button" data-qa-selector="pipeline_retry_button" icon="repeat" variant="default" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index a9154d93194..f5dfb9e72d5 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -11,27 +11,27 @@ * 3. Merge request widget * 4. Commit widget */ - +import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import $ from 'jquery'; -import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { PIPELINES_TABLE } from '../../constants'; import eventHub from '../../event_hub'; import JobItem from '../graph/job_item.vue'; -import { PIPELINES_TABLE } from '../../constants'; export default { components: { GlIcon, - JobItem, GlLoadingIcon, + GlDropdown, + JobItem, }, - directives: { GlTooltip: GlTooltipDirective, }, - + mixins: [glFeatureFlagsMixin()], props: { stage: { type: Object, @@ -50,30 +50,25 @@ export default { default: '', }, }, - data() { return { isLoading: false, - dropdownContent: '', + dropdownContent: [], }; }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 - ? 'js-builds-dropdown-container' - : 'js-builds-dropdown-loading'; + isCiMiniPipelineGlDropdown() { + // Feature flag ci_mini_pipeline_gl_dropdown + // See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400 + return this.glFeatures?.ciMiniPipelineGlDropdown; }, - triggerButtonClass() { return `ci-status-icon-${this.stage.status.group}`; }, - borderlessIcon() { return `${this.stage.status.icon}_borderless`; }, }, - watch: { updateDropdown() { if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { @@ -81,14 +76,17 @@ export default { } }, }, - updated() { - if (this.dropdownContent.length > 0) { + if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) { this.stopDropdownClickPropagation(); } }, - methods: { + onShowDropdown() { + eventHub.$emit('clickedDropdown'); + this.isLoading = true; + this.fetchJobs(); + }, onClickStage() { if (!this.isDropdownOpen()) { eventHub.$emit('clickedDropdown'); @@ -96,7 +94,6 @@ export default { this.fetchJobs(); } }, - fetchJobs() { axios .get(this.stage.dropdown_path) @@ -105,13 +102,16 @@ export default { this.isLoading = false; }) .catch(() => { - this.closeDropdown(); + if (this.isCiMiniPipelineGlDropdown) { + this.$refs.stageGlDropdown.hide(); + } else { + this.closeDropdown(); + } this.isLoading = false; Flash(__('Something went wrong on our end.')); }); }, - /** * When the user right clicks or cmd/ctrl + click in the job name * the dropdown should not be closed and the link should open in another tab, @@ -119,6 +119,8 @@ export default { * * Since this component is rendered multiple times per page we need to guarantee we only * target the click event of this component. + * + * Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true. */ stopDropdownClickPropagation() { $( @@ -128,23 +130,24 @@ export default { e.stopPropagation(); }); }, - closeDropdown() { if (this.isDropdownOpen()) { $(this.$refs.dropdown).dropdown('toggle'); } }, - isDropdownOpen() { return this.$el.classList.contains('show'); }, - pipelineActionRequestComplete() { if (this.type === PIPELINES_TABLE) { // warn the table to update eventHub.$emit('refreshPipelinesTable'); + return; + } + // close the dropdown in mr widget + if (this.isCiMiniPipelineGlDropdown) { + this.$refs.stageGlDropdown.hide(); } else { - // close the dropdown in mr widget $(this.$refs.dropdown).dropdown('toggle'); } }, @@ -154,31 +157,30 @@ export default { <template> <div class="dropdown"> - <button - id="stageDropdown" - ref="dropdown" + <gl-dropdown + v-if="isCiMiniPipelineGlDropdown" + ref="stageGlDropdown" v-gl-tooltip.hover - :class="triggerButtonClass" + data-testid="mini-pipeline-graph-dropdown" :title="stage.title" - class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" - data-toggle="dropdown" - data-display="static" - type="button" - aria-haspopup="true" - aria-expanded="false" - @click="onClickStage" - > - <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none"> - <gl-icon :name="borderlessIcon" /> - </span> - </button> - - <div - class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown" + variant="link" + :lazy="true" + :popper-opts="{ placement: 'bottom' }" + :toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]" + menu-class="mini-pipeline-graph-dropdown-menu" + @show="onShowDropdown" > + <template #button-content> + <span class="gl-pointer-events-none"> + <gl-icon :name="borderlessIcon" /> + </span> + </template> <gl-loading-icon v-if="isLoading" /> - <ul v-else class="js-builds-dropdown-list scrollable-menu"> + <ul + v-else + class="js-builds-dropdown-list scrollable-menu" + data-testid="mini-pipeline-graph-dropdown-menu-list" + > <li v-for="job in dropdownContent" :key="job.id"> <job-item :dropdown-length="dropdownContent.length" @@ -188,6 +190,45 @@ export default { /> </li> </ul> - </div> + </gl-dropdown> + + <template v-else> + <button + id="stageDropdown" + ref="dropdown" + v-gl-tooltip.hover + :class="triggerButtonClass" + :title="stage.title" + class="mini-pipeline-graph-dropdown-toggle" + data-testid="mini-pipeline-graph-dropdown-toggle" + data-toggle="dropdown" + data-display="static" + type="button" + aria-haspopup="true" + aria-expanded="false" + @click="onClickStage" + > + <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none"> + <gl-icon :name="borderlessIcon" /> + </span> + </button> + + <div + class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" + aria-labelledby="stageDropdown" + > + <gl-loading-icon v-if="isLoading" /> + <ul v-else class="js-builds-dropdown-list scrollable-menu"> + <li v-for="job in dropdownContent" :key="job.id"> + <job-item + :dropdown-length="dropdownContent.length" + :job="job" + css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> + </ul> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue index 24456574a6f..20a232beb83 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue @@ -2,8 +2,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { debounce } from 'lodash'; import Api from '~/api'; -import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants'; export default { components: { diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue index 1241803c612..4a8d89ebe37 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue @@ -2,8 +2,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { debounce } from 'lodash'; import Api from '~/api'; -import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants'; export default { components: { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue index 504cf138d07..2edc84e62cb 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue @@ -1,12 +1,13 @@ <script> -import { GlModal } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlBadge, GlModal } from '@gitlab/ui'; +import { __, n__, sprintf } from '~/locale'; import CodeBlock from '~/vue_shared/components/code_block.vue'; export default { name: 'TestCaseDetails', components: { CodeBlock, + GlBadge, GlModal, }, props: { @@ -17,13 +18,37 @@ export default { testCase: { type: Object, required: true, - validator: ({ classname, formattedTime, name }) => - Boolean(classname) && Boolean(formattedTime) && Boolean(name), + }, + }, + computed: { + failureHistoryMessage() { + if (!this.hasRecentFailures) { + return null; + } + + return sprintf( + n__( + 'Reports|Failed %{count} time in %{baseBranch} in the last 14 days', + 'Reports|Failed %{count} times in %{baseBranch} in the last 14 days', + this.recentFailures.count, + ), + { + count: this.recentFailures.count, + baseBranch: this.recentFailures.base_branch, + }, + ); + }, + hasRecentFailures() { + return Boolean(this.recentFailures); + }, + recentFailures() { + return this.testCase.recent_failures; }, }, text: { name: __('Name'), duration: __('Execution time'), + history: __('History'), trace: __('System output'), }, modalCloseButton: { @@ -53,6 +78,13 @@ export default { </div> </div> + <div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"> + <strong class="gl-text-right col-sm-3">{{ $options.text.history }}</strong> + <div class="col-sm-9" data-testid="test-case-recent-failures"> + <gl-badge variant="warning">{{ failureHistoryMessage }}</gl-badge> + </div> + </div> + <div v-if="testCase.system_output" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3" diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue index a56dcf48d92..58d60e2a185 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -1,6 +1,6 @@ <script> -import { mapActions, mapGetters, mapState } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; +import { mapActions, mapGetters, mapState } from 'vuex'; import TestSuiteTable from './test_suite_table.vue'; import TestSummary from './test_summary.vue'; import TestSummaryTable from './test_summary_table.vue'; diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue index 4b4fb6082c6..51373e712ff 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -1,13 +1,14 @@ <script> -import { mapState, mapGetters, mapActions } from 'vuex'; import { GlModalDirective, GlTooltipDirective, GlFriendlyWrap, GlIcon, + GlLink, GlButton, GlPagination, } from '@gitlab/ui'; +import { mapState, mapGetters, mapActions } from 'vuex'; import { __ } from '~/locale'; import TestCaseDetails from './test_case_details.vue'; @@ -16,6 +17,7 @@ export default { components: { GlIcon, GlFriendlyWrap, + GlLink, GlButton, GlPagination, TestCaseDetails, @@ -97,8 +99,11 @@ export default { <div class="table-section section-10 section-wrap"> <div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div> <div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break"> - <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" /> + <gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank"> + <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" /> + </gl-link> <gl-button + v-if="testCase.file" v-gl-tooltip size="small" category="tertiary" @@ -114,7 +119,7 @@ export default { <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div> <div class="table-mobile-content text-center"> <div - class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center" + class="ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center" :class="`ci-status-icon-${testCase.status}`" > <gl-icon :size="24" :name="testCase.icon" /> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue index 5f9c0be3ccc..2b44ce57faa 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -1,6 +1,6 @@ <script> -import { mapGetters } from 'vuex'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; import { s__ } from '~/locale'; export default { diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js index 22cdb6b8f72..2321728e30c 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js @@ -1,21 +1,13 @@ import Visibility from 'visibilityjs'; -import { GlLoadingIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; -import EmptyState from '../components/pipelines_list/empty_state.vue'; -import SvgBlankState from '../components/pipelines_list/blank_state.vue'; -import PipelinesTableComponent from '../components/pipelines_list/pipelines_table.vue'; -import eventHub from '../event_hub'; +import { __ } from '~/locale'; +import { validateParams } from '~/pipelines/utils'; import { CANCEL_REQUEST } from '../constants'; +import eventHub from '../event_hub'; export default { - components: { - PipelinesTableComponent, - SvgBlankState, - EmptyState, - GlLoadingIcon, - }, data() { return { isLoading: false, @@ -76,6 +68,25 @@ export default { this.poll.stop(); }, methods: { + updateInternalState(parameters) { + this.poll.stop(); + + const queryString = Object.keys(parameters) + .map((parameter) => { + const value = parameters[parameter]; + // update internal state for UI + this[parameter] = value; + return `${parameter}=${encodeURIComponent(value)}`; + }) + .join('&'); + + // update polling parameters + this.requestData = parameters; + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + + this.isLoading = true; + }, /** * Handles URL and query parameter changes. * When the user uses the pagination or the tabs, @@ -184,5 +195,23 @@ export default { }) .finally(() => this.store.toggleIsRunningPipeline(false)); }, + onChangePage(page) { + /* URLS parameters are strings, we need to parse to match types */ + let params = { + page: Number(page).toString(), + }; + + if (this.scope) { + params.scope = this.scope; + } + + params = this.onChangeWithFilter(params); + + this.updateContent(params); + }, + + onChangeWithFilter(params) { + return { ...params, ...validateParams(this.requestData) }; + }, }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 133608b9801..f837851e5c1 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -1,16 +1,13 @@ import Vue from 'vue'; import { deprecatedCreateFlash as Flash } from '~/flash'; -import Translate from '~/vue_shared/translate'; import { __ } from '~/locale'; -import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; +import Translate from '~/vue_shared/translate'; import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue'; -import createDagApp from './pipeline_details_dag'; -import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; -import legacyPipelineHeader from './components/legacy_header_component.vue'; -import eventHub from './event_hub'; +import { reportToSentry } from './components/graph/utils'; import TestReports from './components/test_reports/test_reports.vue'; +import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; +import createDagApp from './pipeline_details_dag'; import createTestReportsStore from './stores/test_reports'; -import { reportToSentry } from './components/graph/utils'; Vue.use(Translate); @@ -59,62 +56,11 @@ const createLegacyPipelinesDetailApp = (mediator) => { }); }; -const createLegacyPipelineHeaderApp = (mediator) => { - if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) { - return; - } - // eslint-disable-next-line no-new - new Vue({ - el: SELECTORS.PIPELINE_HEADER, - components: { - legacyPipelineHeader, - }, - data() { - return { - mediator, - }; - }, - created() { - eventHub.$on('headerPostAction', this.postAction); - eventHub.$on('headerDeleteAction', this.deleteAction); - }, - beforeDestroy() { - eventHub.$off('headerPostAction', this.postAction); - eventHub.$off('headerDeleteAction', this.deleteAction); - }, - errorCaptured(err, _vm, info) { - reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`); - }, - methods: { - postAction(path) { - this.mediator.service - .postAction(path) - .then(() => this.mediator.refreshPipeline()) - .catch(() => Flash(__('An error occurred while making the request.'))); - }, - deleteAction(path) { - this.mediator.stopPipelinePoll(); - this.mediator.service - .deleteAction(path) - .then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success'))) - .catch(() => Flash(__('An error occurred while deleting the pipeline.'))); - }, - }, - render(createElement) { - return createElement('legacy-pipeline-header', { - props: { - isLoading: this.mediator.state.isLoading, - pipeline: this.mediator.store.state.pipeline, - }, - }); - }, - }); -}; - const createTestDetails = () => { const el = document.querySelector(SELECTORS.PIPELINE_TESTS); - const { summaryEndpoint, suiteEndpoint } = el?.dataset || {}; + const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {}; const testReportsStore = createTestReportsStore({ + blobPath, summaryEndpoint, suiteEndpoint, }); @@ -132,26 +78,16 @@ const createTestDetails = () => { }); }; -export default async function () { +export default async function initPipelineDetailsBundle() { createTestDetails(); createDagApp(); - const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); - let mediator; + const canShowNewPipelineDetails = + gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers; - if (!gon.features.graphqlPipelineHeader || !gon.features.graphqlPipelineDetails) { - try { - const { default: PipelinesMediator } = await import( - /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator' - ); - mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); - mediator.fetchPipeline(); - } catch { - Flash(__('An error occurred while loading the pipeline.')); - } - } + const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); - if (gon.features.graphqlPipelineDetails) { + if (canShowNewPipelineDetails) { try { const { createPipelinesDetailApp } = await import( /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' @@ -163,19 +99,21 @@ export default async function () { Flash(__('An error occurred while loading the pipeline.')); } } else { + const { default: PipelinesMediator } = await import( + /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator' + ); + const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); + mediator.fetchPipeline(); + createLegacyPipelinesDetailApp(mediator); } - if (gon.features.graphqlPipelineHeader) { - try { - const { createPipelineHeaderApp } = await import( - /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header' - ); - createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER); - } catch { - Flash(__('An error occurred while loading a section of this page.')); - } - } else { - createLegacyPipelineHeaderApp(mediator); + try { + const { createPipelineHeaderApp } = await import( + /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header' + ); + createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER); + } catch { + Flash(__('An error occurred while loading a section of this page.')); } } diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js index d37c72a4f2a..4ee0ad462d2 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_dag.js +++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js @@ -12,7 +12,7 @@ const apolloProvider = new VueApollo({ const createDagApp = () => { const el = document.querySelector('#js-pipeline-dag-vue'); - if (!window.gon?.features?.dagPipelineTab || !el) { + if (!el) { return; } diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js index 2d46bb5ec26..55f3731a3ca 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_graph.js +++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue'; import { GRAPHQL } from './components/graph/constants'; +import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue'; import { reportToSentry } from './components/graph/utils'; Vue.use(VueApollo); diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index 74c5fc45644..09637c25654 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -1,9 +1,9 @@ import Visibility from 'visibilityjs'; -import PipelineStore from './stores/pipeline_store'; import { deprecatedCreateFlash as Flash } from '../flash'; import Poll from '../lib/utils/poll'; import { __ } from '../locale'; import PipelineService from './services/pipeline_service'; +import PipelineStore from './stores/pipeline_store'; export default class pipelinesMediator { constructor(options = {}) { diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js index 4575a99f60f..7bcc51e18e5 100644 --- a/app/assets/javascripts/pipelines/pipelines_index.js +++ b/app/assets/javascripts/pipelines/pipelines_index.js @@ -1,12 +1,12 @@ -import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; -import { __ } from '~/locale'; -import { doesHashExistInUrl } from '~/lib/utils/url_utility'; +import Vue from 'vue'; import { parseBoolean, historyReplaceState, buildUrlWithCurrentLocation, } from '~/lib/utils/common_utils'; +import { doesHashExistInUrl } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import Translate from '~/vue_shared/translate'; import Pipelines from './components/pipelines_list/pipelines.vue'; import PipelinesStore from './stores/pipelines_store'; diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 0b06bcf243a..523ca13b6c3 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -1,5 +1,5 @@ -import axios from '../../lib/utils/axios_utils'; import Api from '~/api'; +import axios from '../../lib/utils/axios_utils'; import { validateParams } from '../utils'; export default class PipelinesService { diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js index 3c664457756..6de345233ae 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js @@ -1,7 +1,7 @@ -import axios from '~/lib/utils/axios_utils'; -import * as types from './mutation_types'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; +import * as types from './mutation_types'; export const fetchSummary = ({ state, commit, dispatch }) => { dispatch('toggleLoading'); @@ -28,16 +28,12 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => { dispatch('toggleLoading'); - const { name = '', build_ids = [] } = state.testReports?.test_suites?.[index] || {}; + const { build_ids = [] } = state.testReports?.test_suites?.[index] || {}; // Replacing `/:suite_name.json` with the name of the suite. Including the extra characters // to ensure that we replace exactly the template part of the URL string - const endpoint = state.suiteEndpoint?.replace( - '/:suite_name.json', - `/${encodeURIComponent(name)}.json`, - ); return axios - .get(endpoint, { params: { build_ids } }) + .get(state.suiteEndpoint, { params: { build_ids } }) .then(({ data }) => commit(types.SET_SUITE, { suite: data, index })) .catch(() => { createFlash(s__('TestReports|There was an error fetching the test suite.')); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js index c31e7dd114f..03680de0fa9 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js @@ -1,4 +1,4 @@ -import { addIconStatus, formattedTime } from './utils'; +import { addIconStatus, formatFilePath, formattedTime } from './utils'; export const getTestSuites = (state) => { const { test_suites: testSuites = [] } = state.testReports; @@ -17,7 +17,15 @@ export const getSuiteTests = (state) => { const { page, perPage } = state.pageInfo; const start = (page - 1) * perPage; - return testCases.map(addIconStatus).slice(start, start + perPage); + return testCases + .map((testCase) => ({ + ...testCase, + classname: testCase.classname || '', + name: testCase.name || '', + filePath: testCase.file ? `${state.blobPath}/${formatFilePath(testCase.file)}` : null, + })) + .map(addIconStatus) + .slice(start, start + perPage); }; export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js index 204dfc2fb01..64d4b8bafb1 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/index.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import state from './state'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; +import state from './state'; Vue.use(Vuex); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js index 7f5da549a9d..0ee6f53fa58 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/state.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js @@ -1,4 +1,5 @@ -export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({ +export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({ + blobPath, summaryEndpoint, suiteEndpoint, testReports: {}, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 5c1f27b166a..63a58798958 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -1,19 +1,28 @@ import { __, sprintf } from '../../../locale'; import { TestStatus } from '../../constants'; +/** + * Removes `./` from the beginning of a file path so it can be appended onto a blob path + * @param {String} file + * @returns {String} - formatted value + */ +export function formatFilePath(file) { + return file.replace(/^\.?\/*/, ''); +} + export function iconForTestStatus(status) { switch (status) { case TestStatus.SUCCESS: - return 'status_success_borderless'; + return 'status_success'; case TestStatus.FAILED: - return 'status_failed_borderless'; + return 'status_failed'; case TestStatus.ERROR: - return 'status_warning_borderless'; + return 'status_warning'; case TestStatus.SKIPPED: - return 'status_skipped_borderless'; + return 'status_skipped'; case TestStatus.UNKNOWN: default: - return 'status_notfound_borderless'; + return 'status_notfound'; } } diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 50bb23b7e63..22820fca43e 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -1,6 +1,6 @@ import { pickBy } from 'lodash'; -import { SUPPORTED_FILTER_PARAMETERS } from './constants'; import { createNodeDict } from './components/parsing_utils'; +import { SUPPORTED_FILTER_PARAMETERS } from './constants'; export const validateParams = (params) => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); @@ -49,10 +49,10 @@ export const generateJobNeedsDict = (jobs = {}) => { // to the list of `needs` to ensure we can properly reference it. const group = jobs[job]; if (group.size > 1) { - return [job, group.name, ...newNeeds]; + return [job, group.name, newNeeds]; } - return [job, ...newNeeds]; + return [job, newNeeds]; }) .flat(Infinity); }; |