diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /app/assets/javascripts/pipelines | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'app/assets/javascripts/pipelines')
28 files changed, 534 insertions, 247 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 137455bcae1..efa11580c41 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,10 +1,9 @@ <script> -import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import Icon from '~/vue_shared/components/icon.vue'; /** * Renders either a cancel, retry or play icon button and handles the post request @@ -18,7 +17,7 @@ import Icon from '~/vue_shared/components/icon.vue'; */ export default { components: { - Icon, + GlIcon, GlButton, GlLoadingIcon, }, @@ -92,6 +91,6 @@ export default { @click="onClickAction" > <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" /> - <icon v-else :name="actionIcon" /> + <gl-icon v-else :name="actionIcon" class="gl-mr-0!" /> </gl-button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index f5bf6a6ed34..924cdeebba1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -44,6 +44,10 @@ export default { return { downstreamMarginTop: null, jobName: null, + pipelineExpanded: { + jobName: '', + expanded: false, + }, }; }, computed: { @@ -120,6 +124,19 @@ export default { setJob(jobName) { this.jobName = jobName; }, + setPipelineExpanded(jobName, expanded) { + if (expanded) { + this.pipelineExpanded = { + jobName, + expanded, + }; + } else { + this.pipelineExpanded = { + expanded, + jobName: '', + }; + } + }, }, }; </script> @@ -181,6 +198,7 @@ export default { :has-triggered-by="hasTriggeredBy" :action="stage.status.action" :job-hovered="jobName" + :pipeline-expanded="pipelineExpanded" @refreshPipelineGraph="refreshPipelineGraph" /> </ul> @@ -193,6 +211,7 @@ export default { graph-position="right" @linkedPipelineClick="handleClickedDownstream" @downstreamHovered="setJob" + @pipelineExpandToggle="setPipelineExpanded" /> <pipeline-graph 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 15c220a554d..11fb2b18e9d 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -69,9 +69,7 @@ export default { > <ci-icon :status="group.status" /> - <span - class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" - > + <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom"> {{ 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 4d72cc55b34..0fe0b671273 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -31,7 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; */ export default { - hoverClass: 'gl-inset-border-1-blue-500', + hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, JobNameComponent, @@ -61,6 +61,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { boundary() { @@ -101,8 +106,14 @@ export default { hasAction() { return this.job.status && this.job.status.action && this.job.status.action.path; }, + relatedDownstreamHovered() { + return this.job.name === this.jobHovered; + }, + relatedDownstreamExpanded() { + return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded; + }, jobClasses() { - return this.job.name === this.jobHovered + return this.relatedDownstreamHovered || this.relatedDownstreamExpanded ? `${this.$options.hoverClass} ${this.cssClassJobName}` : this.cssClassJobName; }, @@ -121,8 +132,9 @@ export default { v-gl-tooltip="{ boundary, placement: 'bottom' }" :href="status.details_path" :title="tooltipText" - :class="cssClassJobName" + :class="jobClasses" class="js-pipeline-graph-job-link qa-job-link menu-item" + data-testid="job-with-link" > <job-name-component :name="job.name" :status="job.status" /> </gl-link> 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 74a261f35d7..30ba243077e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -27,9 +27,7 @@ export default { <template> <span class="ci-job-name-component mw-100"> <ci-icon :status="status" /> - <span - class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" - > + <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom"> {{ name }} </span> </span> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index f0a8f9f7ab7..e359fc787c5 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; @@ -10,6 +10,8 @@ export default { components: { CiStatus, GlButton, + GlLink, + GlLoadingIcon, }, props: { pipeline: { @@ -25,6 +27,11 @@ export default { required: true, }, }, + data() { + return { + expanded: false, + }; + }, computed: { tooltipText() { return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} @@ -66,11 +73,22 @@ export default { ? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name }) : ''; }, + expandedIcon() { + if (this.parentPipeline) { + return this.expanded ? 'angle-right' : 'angle-left'; + } + return this.expanded ? 'angle-left' : 'angle-right'; + }, + expandButtonPosition() { + return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; + }, }, methods: { onClickLinkedPipeline() { this.$root.$emit('bv::hide::tooltip', this.buttonId); + this.expanded = !this.expanded; this.$emit('pipelineClicked', this.$refs.linkedPipeline); + this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded); }, hideTooltips() { this.$root.$emit('bv::hide::tooltip'); @@ -88,27 +106,48 @@ export default { <template> <li ref="linkedPipeline" + v-gl-tooltip class="linked-pipeline build" + :title="tooltipText" :class="{ 'downstream-pipeline': isDownstream }" data-qa-selector="child_pipeline" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" > - <gl-button - :id="buttonId" - v-gl-tooltip - :title="tooltipText" - class="linked-pipeline-content" - data-qa-selector="linked_pipeline_button" - :class="`js-pipeline-expand-${pipeline.id}`" - :loading="pipeline.isLoading" - @click="onClickLinkedPipeline" + <div + class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1" + :class="{ 'gl-pl-9': parentPipeline }" > - <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" /> - <span class="str-truncated"> {{ downstreamTitle }} • #{{ pipeline.id }} </span> + <div class="gl-display-flex"> + <ci-status + v-if="!pipeline.isLoading" + :status="pipelineStatus" + css-classes="gl-top-0 gl-pr-2" + /> + <div v-else class="gl-pr-2"><gl-loading-icon inline /></div> + <div class="gl-display-flex gl-flex-direction-column gl-w-13"> + <span class="gl-text-truncate"> + {{ downstreamTitle }} + </span> + <div class="gl-text-truncate"> + <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink" + >#{{ pipeline.id }}</gl-link + > + </div> + </div> + </div> <div class="gl-pt-2"> <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span> </div> - </gl-button> + <gl-button + :id="buttonId" + class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" + :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" + :icon="expandedIcon" + data-testid="expandPipelineButton" + data-qa-selector="expand_pipeline_button" + @click="onClickLinkedPipeline" + /> + </div> </li> </template> 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 d82885ff8de..3ad28d88345 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -44,6 +44,9 @@ export default { onDownstreamHovered(jobName) { this.$emit('downstreamHovered', jobName); }, + onPipelineExpandToggle(jobName, expanded) { + this.$emit('pipelineExpandToggle', jobName, expanded); + }, }, }; </script> @@ -65,6 +68,7 @@ export default { :project-id="projectId" @pipelineClicked="onPipelineClick($event, pipeline, index)" @downstreamHovered="onDownstreamHovered" + @pipelineExpandToggle="onPipelineExpandToggle" /> </ul> </div> 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 9de6ba819c2..1453c349f44 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -41,6 +41,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { hasAction() { @@ -86,6 +91,7 @@ export default { v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" css-class-job-name="build-content" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue new file mode 100644 index 00000000000..3cc76425e2a --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue @@ -0,0 +1,76 @@ +<script> +import { GlTab, GlTabs } from '@gitlab/ui'; +import jsYaml from 'js-yaml'; +import PipelineGraph from './pipeline_graph.vue'; +import { preparePipelineGraphData } from '../../utils'; + +export default { + FILE_CONTENT_SELECTOR: '#blob-content', + EMPTY_FILE_SELECTOR: '.nothing-here-block', + + components: { + GlTab, + GlTabs, + PipelineGraph, + }, + props: { + blobData: { + required: true, + type: String, + }, + }, + data() { + return { + selectedTabIndex: 0, + pipelineData: {}, + }; + }, + computed: { + isVisualizationTab() { + return this.selectedTabIndex === 1; + }, + }, + async created() { + if (this.blobData) { + // The blobData in this case represents the gitlab-ci.yml data + const json = await jsYaml.load(this.blobData); + this.pipelineData = preparePipelineGraphData(json); + } + }, + methods: { + // This is used because the blob page still uses haml, and we can't make + // our haml hide the unused section so we resort to a standard query here. + toggleFileContent({ isFileTab }) { + const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR); + const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR); + + const elementToHide = el || emptySection; + + if (!elementToHide) { + return; + } + + // Checking for the current style display prevents user + // from toggling visiblity on and off when clicking on the tab + if (!isFileTab && elementToHide.style.display !== 'none') { + elementToHide.style.display = 'none'; + } + + if (isFileTab && elementToHide.style.display === 'none') { + elementToHide.style.display = 'block'; + } + }, + }, +}; +</script> +<template> + <div> + <div> + <gl-tabs v-model="selectedTabIndex"> + <gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" /> + <gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" /> + </gl-tabs> + </div> + <pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" /> + </div> +</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 new file mode 100644 index 00000000000..19d41b166c3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue @@ -0,0 +1,24 @@ +<script> +import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + components: { + tooltipOnTruncate, + }, + props: { + jobName: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top"> + <div + class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill " + > + {{ jobName }} + </div> + </tooltip-on-truncate> +</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 new file mode 100644 index 00000000000..6a0d3cce1f3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -0,0 +1,57 @@ +<script> +import { isEmpty } from 'lodash'; +import { GlAlert } from '@gitlab/ui'; +import JobPill from './job_pill.vue'; +import StagePill from './stage_pill.vue'; + +export default { + components: { + GlAlert, + JobPill, + StagePill, + }, + props: { + pipelineData: { + required: true, + type: Object, + }, + }, + computed: { + isPipelineDataEmpty() { + return isEmpty(this.pipelineData); + }, + emptyClass() { + return !this.isPipelineDataEmpty ? 'gl-py-7' : ''; + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass"> + <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false"> + {{ __('No content to show') }} + </gl-alert> + <template v-else> + <div + v-for="(stage, index) in pipelineData.stages" + :key="`${stage.name}-${index}`" + class="gl-flex-direction-column" + > + <div + class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5" + :class="{ + 'stage-left-rounded': index === 0, + 'stage-right-rounded': index === pipelineData.stages.length - 1, + }" + > + <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" /> + </div> + <div + class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8" + > + <job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" /> + </div> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue new file mode 100644 index 00000000000..7b2458db725 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue @@ -0,0 +1,35 @@ +<script> +import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + components: { + tooltipOnTruncate, + }, + props: { + stageName: { + type: String, + required: true, + }, + isEmpty: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + emptyClass() { + return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600'; + }, + }, +}; +</script> +<template> + <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top"> + <div + class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill" + :class="emptyClass" + > + {{ stageName }} + </div> + </tooltip-on-truncate> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue index a66bbb7e5ba..d7b6e033bd1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue @@ -1,12 +1,10 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { GlButton } from '@gitlab/ui'; export default { name: 'PipelineNavControls', components: { - LoadingButton, - GlDeprecatedButton, + GlButton, }, props: { newPipelinePath: { @@ -42,25 +40,27 @@ export default { </script> <template> <div class="nav-controls"> - <gl-deprecated-button + <gl-button v-if="newPipelinePath" :href="newPipelinePath" variant="success" + category="primary" class="js-run-pipeline" > {{ s__('Pipelines|Run Pipeline') }} - </gl-deprecated-button> + </gl-button> - <loading-button + <gl-button v-if="resetCachePath" :loading="isResetCacheButtonLoading" - :label="s__('Pipelines|Clear Runner Caches')" class="js-clear-cache" @click="onClickResetCache" - /> + > + {{ s__('Pipelines|Clear Runner Caches') }} + </gl-button> - <gl-deprecated-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint"> + <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint"> {{ s__('Pipelines|CI Lint') }} - </gl-deprecated-button> + </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 f604edd8859..43a54090e18 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,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { isEmpty } from 'lodash'; import { GlLink } from '@gitlab/ui'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 2dfc6485d85..adba86d384b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,5 +1,6 @@ <script> 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'; @@ -9,7 +10,6 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.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 Icon from '~/vue_shared/components/icon.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; import { validateParams } from '../../utils'; import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; @@ -21,7 +21,7 @@ export default { NavigationTabs, NavigationControls, PipelinesFilteredSearch, - Icon, + GlIcon, }, mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()], props: { @@ -285,8 +285,8 @@ export default { v-if="shouldRenderTabs || shouldRenderButtons" class="top-area scrolling-tabs-container inner-page-scroll-tabs" > - <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div> - <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div> + <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div> + <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div> <navigation-tabs v-if="shouldRenderTabs" 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 098efe68b83..97595e5d2ce 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -1,10 +1,9 @@ <script> -import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; 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 Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../../event_hub'; export default { @@ -12,9 +11,9 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, GlCountdown, - GlDeprecatedButton, + GlButton, GlLoadingIcon, }, props: { @@ -83,29 +82,32 @@ export default { type="button" :disabled="isLoading" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" - :title="__('Manual job')" + :title="__('Run manual or delayed jobs')" data-toggle="dropdown" - :aria-label="__('Manual job')" + :aria-label="__('Run manual or delayed jobs')" > - <icon name="play" class="icon-play" /> + <gl-icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"></i> <gl-loading-icon v-if="isLoading" /> </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="action in actions" :key="action.path"> - <gl-deprecated-button + <gl-button + category="tertiary" :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" - class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap" + class="js-pipeline-action-link" @click="onClickAction(action)" > - {{ action.name }} - <span v-if="action.scheduled_at"> - <icon name="clock" /> - <gl-countdown :end-date-string="action.scheduled_at" /> - </span> - </gl-deprecated-button> + <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> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue index 59c066b2683..4a3d134685e 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue @@ -1,14 +1,13 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLink, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { directives: { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, GlLink, }, props: { @@ -29,7 +28,7 @@ export default { data-toggle="dropdown" :aria-label="__('Artifacts')" > - <icon name="download" /> + <gl-icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> <ul class="dropdown-menu dropdown-menu-right"> 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 f25994a7506..1bdb7d18f04 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,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import eventHub from '../../event_hub'; import PipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; @@ -8,8 +9,6 @@ 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 LoadingButton from '~/vue_shared/components/loading_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import { PIPELINES_TABLE } from '../../constants'; /** @@ -27,8 +26,7 @@ export default { PipelineTriggerer, CiBadge, PipelinesTimeago, - LoadingButton, - Icon, + GlButton, }, props: { pipeline: { @@ -274,6 +272,7 @@ export default { <ci-badge :status="pipelineStatus" :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" data-qa-selector="pipeline_commit_status" /> </div> @@ -337,28 +336,30 @@ export default { class="d-md-block" /> - <loading-button + <gl-button v-if="pipeline.flags.retryable" :loading="isRetrying" :disabled="isRetrying" - container-class="js-pipelines-retry-button btn btn-default btn-retry" + class="js-pipelines-retry-button btn-retry" data-qa-selector="pipeline_retry_button" + icon="repeat" + variant="default" + category="secondary" @click="handleRetryClick" - > - <icon name="repeat" /> - </loading-button> + /> - <loading-button + <gl-button v-if="pipeline.flags.cancelable" :loading="isCancelling" :disabled="isCancelling" data-toggle="modal" data-target="#confirmation-modal" - container-class="js-pipelines-cancel-button btn btn-remove" + icon="close" + variant="danger" + category="primary" + class="js-pipelines-cancel-button" @click="handleCancelClick" - > - <icon name="close" /> - </loading-button> + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index d992a4b7752..4045f450104 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -13,18 +13,17 @@ */ import $ from 'jquery'; -import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import eventHub from '../../event_hub'; -import Icon from '~/vue_shared/components/icon.vue'; import JobItem from '../graph/job_item.vue'; import { PIPELINES_TABLE } from '../../constants'; export default { components: { - Icon, + GlIcon, JobItem, GlLoadingIcon, }, @@ -170,7 +169,7 @@ export default { @click="onClickStage" > <span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events"> - <icon :name="borderlessIcon" /> + <gl-icon :name="borderlessIcon" /> </span> </button> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 8a01e1fe3f5..7d13ee582c6 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -1,5 +1,5 @@ <script> -import iconTimerSvg from 'icons/_icon_timer.svg'; +import { GlIcon } from '@gitlab/ui'; import '~/lib/utils/datetime_utility'; import tooltip from '~/vue_shared/directives/tooltip'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -8,6 +8,7 @@ export default { directives: { tooltip, }, + components: { GlIcon }, mixins: [timeagoMixin], props: { finishedTime: { @@ -19,11 +20,6 @@ export default { required: true, }, }, - data() { - return { - iconTimerSvg, - }; - }, computed: { hasDuration() { return this.duration > 0; @@ -59,11 +55,12 @@ export default { <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div> <div class="table-mobile-content"> <p v-if="hasDuration" class="duration"> - <span v-html="iconTimerSvg"> </span> {{ durationFormatted }} + <gl-icon name="timer" class="gl-vertical-align-baseline!" aria-hidden="true" /> + {{ durationFormatted }} </p> <p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block"> - <i class="fa fa-calendar" aria-hidden="true"> </i> + <gl-icon name="calendar" class="gl-vertical-align-baseline!" aria-hidden="true" /> <time v-tooltip 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 bc1d22e2976..c3398e90895 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -55,13 +55,14 @@ export default { <template> <div v-if="isLoading"> - <gl-loading-icon size="lg" class="gl-mt-3 js-loading-spinner" /> + <gl-loading-icon size="lg" class="gl-mt-3" /> </div> <div v-else-if="!isLoading && showTests" ref="container" - class="tests-detail position-relative js-tests-detail" + class="tests-detail position-relative" + data-testid="tests-detail" > <transition name="slide" @@ -85,7 +86,7 @@ export default { <div v-else> <div class="row gl-mt-3"> <div class="col-12"> - <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p> + <p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p> </div> </div> </div> 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 478073e44d1..aa53c5040e8 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,15 +1,12 @@ <script> import { mapGetters } from 'vuex'; -import { GlTooltipDirective, GlFriendlyWrap } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlFriendlyWrap, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; export default { name: 'TestsSuiteTable', components: { - Icon, - SmartVirtualList, + GlIcon, GlFriendlyWrap, }, directives: { @@ -28,8 +25,6 @@ export default { return this.getSuiteTests.length > 0; }, }, - maxShownRows: 30, - typicalRowHeight: 75, wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'], }; </script> @@ -61,66 +56,60 @@ export default { </div> </div> - <smart-virtual-list - :length="getSuiteTests.length" - :remain="$options.maxShownRows" - :size="$options.typicalRowHeight" + <div + v-for="(testCase, index) in getSuiteTests" + :key="index" + class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" > - <div - v-for="(testCase, index) in getSuiteTests" - :key="index" - class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" - > - <div class="table-section section-20 section-wrap"> - <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div> - <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> - <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" /> - </div> + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div> + <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> + <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" /> </div> + </div> - <div class="table-section section-20 section-wrap"> - <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> - <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> - <gl-friendly-wrap - data-testid="caseName" - :symbols="$options.wrapSymbols" - :text="testCase.name" - /> - </div> + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> + <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> + <gl-friendly-wrap + data-testid="caseName" + :symbols="$options.wrapSymbols" + :text="testCase.name" + /> </div> + </div> - <div class="table-section section-10 section-wrap"> - <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-${testCase.status}`" - > - <icon :size="24" :name="testCase.icon" /> - </div> + <div class="table-section section-10 section-wrap"> + <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-${testCase.status}`" + > + <gl-icon :size="24" :name="testCase.icon" /> </div> </div> + </div> - <div class="table-section flex-grow-1"> - <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div> - <div class="table-mobile-content"> - <pre - v-if="testCase.system_output" - class="build-trace build-trace-rounded text-left" - ><code class="bash p-0">{{testCase.system_output}}</code></pre> - </div> + <div class="table-section flex-grow-1"> + <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div> + <div class="table-mobile-content"> + <pre + v-if="testCase.system_output" + class="build-trace build-trace-rounded text-left" + ><code class="bash p-0">{{testCase.system_output}}</code></pre> </div> + </div> - <div class="table-section section-10 section-wrap"> - <div role="rowheader" class="table-mobile-header"> - {{ __('Duration') }} - </div> - <div class="table-mobile-content text-right pr-sm-1"> - {{ testCase.formattedTime }} - </div> + <div class="table-section section-10 section-wrap"> + <div role="rowheader" class="table-mobile-header"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-right pr-sm-1"> + {{ testCase.formattedTime }} </div> </div> - </smart-virtual-list> + </div> </div> <div v-else> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 712ac5eb0e5..d33d4e7dfd0 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -1,15 +1,13 @@ <script> -import { GlDeprecatedButton, GlProgressBar } from '@gitlab/ui'; +import { GlButton, GlProgressBar } from '@gitlab/ui'; import { __ } from '~/locale'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; +import { formattedTime } from '../../stores/test_reports/utils'; export default { name: 'TestSummary', components: { - GlDeprecatedButton, + GlButton, GlProgressBar, - Icon, }, props: { report: { @@ -39,7 +37,7 @@ export default { return 0; }, formattedDuration() { - return formatTime(secondsToMilliseconds(this.report.total_time)); + return formattedTime(this.report.total_time); }, progressBarVariant() { if (this.successPercentage < 33) { @@ -69,14 +67,13 @@ export default { <div> <div class="row"> <div class="col-12 d-flex gl-mt-3 align-items-center"> - <gl-deprecated-button + <gl-button v-if="showBack" - size="sm" + size="small" class="gl-mr-3 js-back-button" + icon="angle-left" @click="onBackClick" - > - <icon name="angle-left" /> - </gl-deprecated-button> + /> <h4>{{ heading }}</h4> </div> 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 e774fe06fbe..5f9c0be3ccc 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 @@ -2,13 +2,11 @@ import { mapGetters } from 'vuex'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; export default { name: 'TestsSummaryTable', components: { GlIcon, - SmartVirtualList, }, directives: { GlTooltip: GlTooltipDirective, @@ -31,8 +29,6 @@ export default { this.$emit('row-click', index); }, }, - maxShownRows: 20, - typicalRowHeight: 55, }; </script> @@ -69,83 +65,77 @@ export default { </div> </div> - <smart-virtual-list - :length="getTestSuites.length" - :remain="$options.maxShownRows" - :size="$options.typicalRowHeight" + <div + v-for="(testSuite, index) in getTestSuites" + :key="index" + role="row" + class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row" + :class="{ + 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error, + }" + @click="tableRowClick(index)" > - <div - v-for="(testSuite, index) in getTestSuites" - :key="index" - role="row" - class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row" - :class="{ - 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error, - }" - @click="tableRowClick(index)" - > - <div class="table-section section-25"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Suite') }} - </div> - <div class="table-mobile-content underline cgray pl-3"> - {{ testSuite.name }} - <gl-icon - v-if="testSuite.suite_error" - ref="suiteErrorIcon" - v-gl-tooltip - name="error" - :title="testSuite.suite_error" - class="vertical-align-middle" - /> - </div> + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Suite') }} </div> + <div class="table-mobile-content underline cgray pl-3"> + {{ testSuite.name }} + <gl-icon + v-if="testSuite.suite_error" + ref="suiteErrorIcon" + v-gl-tooltip + name="error" + :title="testSuite.suite_error" + class="vertical-align-middle" + /> + </div> + </div> - <div class="table-section section-25"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Duration') }} - </div> - <div class="table-mobile-content text-md-left"> - {{ testSuite.formattedTime }} - </div> + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-md-left"> + {{ testSuite.formattedTime }} </div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Failed') }} - </div> - <div class="table-mobile-content">{{ testSuite.failed_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Failed') }} </div> + <div class="table-mobile-content">{{ testSuite.failed_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Errors') }} - </div> - <div class="table-mobile-content">{{ testSuite.error_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Errors') }} </div> + <div class="table-mobile-content">{{ testSuite.error_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Skipped') }} - </div> - <div class="table-mobile-content">{{ testSuite.skipped_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Skipped') }} </div> + <div class="table-mobile-content">{{ testSuite.skipped_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Passed') }} - </div> - <div class="table-mobile-content">{{ testSuite.success_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Passed') }} </div> + <div class="table-mobile-content">{{ testSuite.success_count }}</div> + </div> - <div class="table-section section-10 text-right pr-md-3"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Total') }} - </div> - <div class="table-mobile-content">{{ testSuite.total_count }}</div> + <div class="table-section section-10 text-right pr-md-3"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Total') }} </div> + <div class="table-mobile-content">{{ testSuite.total_count }}</div> </div> - </smart-virtual-list> + </div> </div> <div v-else> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index c57be7c75b0..745f5b886a5 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports'; Vue.use(Translate); +const SELECTORS = { + PIPELINE_DETAILS: '.js-pipeline-details-vue', + PIPELINE_GRAPH: '#js-pipeline-graph-vue', + PIPELINE_HEADER: '#js-pipeline-header-vue', + PIPELINE_TESTS: '#js-pipeline-tests-detail', +}; + const createPipelinesDetailApp = mediator => { + if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) { + return; + } // eslint-disable-next-line no-new new Vue({ - el: '#js-pipeline-graph-vue', + el: SELECTORS.PIPELINE_GRAPH, components: { pipelineGraph, }, @@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => { }; const createPipelineHeaderApp = mediator => { + if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) { + return; + } // eslint-disable-next-line no-new new Vue({ - el: '#js-pipeline-header-vue', + el: SELECTORS.PIPELINE_HEADER, components: { pipelineHeader, }, @@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => { }; const createTestDetails = () => { - const el = document.querySelector('#js-pipeline-tests-detail'); + const el = document.querySelector(SELECTORS.PIPELINE_TESTS); const { summaryEndpoint, suiteEndpoint } = el?.dataset || {}; - const testReportsStore = createTestReportsStore({ summaryEndpoint, suiteEndpoint, @@ -115,7 +127,7 @@ const createTestDetails = () => { }; export default () => { - const { dataset } = document.querySelector('.js-pipeline-details-vue'); + const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); mediator.fetchPipeline(); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js index 6c670806cc4..c123014756d 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, sortTestCases } from './utils'; +import { addIconStatus, formattedTime } from './utils'; export const getTestSuites = state => { const { test_suites: testSuites = [] } = state.testReports; @@ -14,5 +14,5 @@ export const getSelectedSuite = state => export const getSuiteTests = state => { const { test_cases: testCases = [] } = getSelectedSuite(state); - return testCases.sort(sortTestCases).map(addIconStatus); + return testCases.map(addIconStatus); }; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 16fa6935cbe..8f1ac305cda 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -1,5 +1,4 @@ -import { TestStatus } from '~/pipelines/constants'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '../../../locale'; export function iconForTestStatus(status) { switch (status) { @@ -12,25 +11,16 @@ export function iconForTestStatus(status) { } } -export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds)); +export const formattedTime = (seconds = 0) => { + if (seconds < 1) { + const milliseconds = seconds * 1000; + return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) }); + } + return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) }); +}; export const addIconStatus = testCase => ({ ...testCase, icon: iconForTestStatus(testCase.status), formattedTime: formattedTime(testCase.execution_time), }); - -export const sortTestCases = (a, b) => { - if (a.status === b.status) { - return 0; - } - - switch (b.status) { - case TestStatus.SUCCESS: - return -1; - case TestStatus.FAILED: - return 1; - default: - return 0; - } -}; diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 2e08001f6b3..bd53b22784c 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -1,7 +1,49 @@ import { pickBy } from 'lodash'; import { SUPPORTED_FILTER_PARAMETERS } from './constants'; -// eslint-disable-next-line import/prefer-default-export export const validateParams = params => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); }; + +/** + * This function takes a json payload that comes from a yml + * file converted to json through `jsyaml` library. Because we + * naively convert the entire yaml to json, some keys (like `includes`) + * are irrelevant to rendering the graph and must be removed. We also + * restructure the data to have the structure from an API response for the + * pipeline data. + * @param {Object} jsonData + * @returns {Array} - Array of stages containing all jobs + */ +export const preparePipelineGraphData = jsonData => { + const jsonKeys = Object.keys(jsonData); + const jobNames = jsonKeys.filter(job => jsonData[job]?.stage); + + // We merge both the stages from the "stages" key in the yaml and the stage associated + // with each job to show the user both the stages they explicitly defined, and those + // that they added under jobs. We also remove duplicates. + const jobStages = jobNames.map(job => jsonData[job].stage); + const userDefinedStages = jsonData?.stages ?? []; + + // The order is important here. We always show the stages in order they were + // defined in the `stages` key first, and then stages that are under the jobs. + const stages = Array.from(new Set([...userDefinedStages, ...jobStages])); + + const arrayOfJobsByStage = stages.map(val => { + return jobNames.filter(job => { + return jsonData[job].stage === val; + }); + }); + + const pipelineData = stages.map((stage, index) => { + const stageJobs = arrayOfJobsByStage[index]; + return { + name: stage, + groups: stageJobs.map(job => { + return { name: job, jobs: [{ ...jsonData[job] }] }; + }), + }; + }); + + return { stages: pipelineData }; +}; |