diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipelines_list')
13 files changed, 426 insertions, 425 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/blank_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/blank_state.vue deleted file mode 100644 index 6c3a4a27606..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_list/blank_state.vue +++ /dev/null @@ -1,30 +0,0 @@ -<script> -export default { - name: 'PipelinesSvgState', - props: { - svgPath: { - type: String, - required: true, - }, - - message: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div class="row empty-state"> - <div class="col-12"> - <div class="svg-content"><img :src="svgPath" /></div> - </div> - - <div class="col-12 text-center"> - <div class="text-content"> - <h4>{{ message }}</h4> - </div> - </div> - </div> -</template> 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 f8107d288d9..c3bcfcb18fb 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -1,7 +1,9 @@ <script> import { GlEmptyState } from '@gitlab/ui'; +import Experiment from '~/experimentation/components/experiment.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import PipelinesCiTemplates from './pipelines_ci_templates.vue'; export default { i18n: { @@ -15,6 +17,8 @@ export default { name: 'PipelinesEmptyState', components: { GlEmptyState, + Experiment, + PipelinesCiTemplates, }, props: { emptyStateSvgPath: { @@ -35,19 +39,26 @@ export default { </script> <template> <div> - <gl-empty-state - v-if="canSetCi" - :title="$options.i18n.title" - :svg-path="emptyStateSvgPath" - :description="$options.i18n.description" - :primary-button-text="$options.i18n.btnText" - :primary-button-link="ciHelpPagePath" - /> - <gl-empty-state - v-else - title="" - :svg-path="emptyStateSvgPath" - :description="$options.i18n.noCiDescription" - /> + <experiment name="pipeline_empty_state_templates"> + <template #control> + <gl-empty-state + v-if="canSetCi" + :title="$options.i18n.title" + :svg-path="emptyStateSvgPath" + :description="$options.i18n.description" + :primary-button-text="$options.i18n.btnText" + :primary-button-link="ciHelpPagePath" + /> + <gl-empty-state + v-else + title="" + :svg-path="emptyStateSvgPath" + :description="$options.i18n.noCiDescription" + /> + </template> + <template #candidate> + <pipelines-ci-templates /> + </template> + </experiment> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/job_item.vue b/app/assets/javascripts/pipelines/components/pipelines_list/job_item.vue new file mode 100644 index 00000000000..670fa398536 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/job_item.vue @@ -0,0 +1,190 @@ +<script> +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; +import { sprintf } from '~/locale'; +import { reportToSentry } from '../../utils'; +import ActionComponent from '../jobs_shared/action_component.vue'; +import JobNameComponent from '../jobs_shared/job_name_component.vue'; + +/** + * Renders the badge for the pipeline graph and the job's dropdown. + * + * The following object should be provided as `job`: + * + * { + * "id": 4256, + * "name": "test", + * "status": { + * "icon": "status_success", + * "text": "passed", + * "label": "passed", + * "group": "success", + * "tooltip": "passed", + * "details_path": "/root/ci-mock/builds/4256", + * "action": { + * "icon": "retry", + * "title": "Retry", + * "path": "/root/ci-mock/builds/4256/retry", + * "method": "post" + * } + * } + * } + */ + +export default { + hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', + components: { + ActionComponent, + JobNameComponent, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [delayedJobMixin], + props: { + job: { + type: Object, + required: true, + }, + cssClassJobName: { + type: String, + required: false, + default: '', + }, + dropdownLength: { + type: Number, + required: false, + default: Infinity, + }, + jobHovered: { + type: String, + required: false, + default: '', + }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, + }, + computed: { + boundary() { + return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; + }, + detailsPath() { + return this.status.details_path; + }, + hasDetails() { + return this.status.has_details; + }, + status() { + return this.job && this.job.status ? this.job.status : {}; + }, + tooltipText() { + const textBuilder = []; + const { name: jobName } = this.job; + + if (jobName) { + textBuilder.push(jobName); + } + + const { tooltip: statusTooltip } = this.status; + if (jobName && statusTooltip) { + textBuilder.push('-'); + } + + if (statusTooltip) { + if (this.isDelayedJob) { + textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime })); + } else { + textBuilder.push(statusTooltip); + } + } + + return textBuilder.join(' '); + }, + /** + * Verifies if the provided job has an action path + * + * @return {Boolean} + */ + 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.relatedDownstreamHovered || this.relatedDownstreamExpanded + ? `${this.$options.hoverClass} ${this.cssClassJobName}` + : this.cssClassJobName; + }, + }, + errorCaptured(err, _vm, info) { + reportToSentry('pipelines_job_item', `pipelines_job_item error: ${err}, info: ${info}`); + }, + methods: { + hideTooltips() { + this.$root.$emit(BV_HIDE_TOOLTIP); + }, + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> +<template> + <div + class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" + data-qa-selector="job_item_container" + > + <gl-link + v-if="hasDetails" + v-gl-tooltip="{ + boundary: 'viewport', + placement: 'bottom', + customClass: 'gl-pointer-events-none', + }" + :href="detailsPath" + :title="tooltipText" + :class="jobClasses" + class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" + data-testid="job-with-link" + @click.stop="hideTooltips" + @mouseout="hideTooltips" + > + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> + </gl-link> + + <div + v-else + v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" + :title="tooltipText" + :class="jobClasses" + class="js-job-component-tooltip non-details-job-component menu-item" + data-testid="job-without-link" + @mouseout="hideTooltips" + > + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> + </div> + + <action-component + v-if="hasAction" + :tooltip-text="status.action.title" + :link="status.action.path" + :action-icon="status.action.icon" + data-qa-selector="action_button" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </div> +</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 cf0849751df..235126fea0c 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue @@ -41,29 +41,29 @@ export default { <template> <div class="nav-controls"> <gl-button - v-if="newPipelinePath" - :href="newPipelinePath" - variant="success" - category="primary" - class="js-run-pipeline" - data-testid="run-pipeline-button" - data-qa-selector="run_pipeline_button" - > - {{ s__('Pipelines|Run Pipeline') }} - </gl-button> - - <gl-button v-if="resetCachePath" :loading="isResetCacheButtonLoading" class="js-clear-cache" data-testid="clear-cache-button" @click="onClickResetCache" > - {{ s__('Pipelines|Clear Runner Caches') }} + {{ s__('Pipelines|Clear runner caches') }} </gl-button> <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint" data-testid="ci-lint-button"> - {{ s__('Pipelines|CI Lint') }} + {{ s__('Pipelines|CI lint') }} + </gl-button> + + <gl-button + v-if="newPipelinePath" + :href="newPipelinePath" + variant="confirm" + category="primary" + class="js-run-pipeline" + data-testid="run-pipeline-button" + data-qa-selector="run_pipeline_button" + > + {{ s__('Pipeline|Run pipeline') }} </gl-button> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue index 05372010d0f..2b33467e948 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -36,7 +36,7 @@ export default { }; </script> <template> - <div data-testid="widget-mini-pipeline-graph"> + <div data-testid="pipeline-mini-graph"> <div v-for="stage in stages" :key="stage.name" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index bdb7dd06620..bf992b84387 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -17,7 +17,7 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import eventHub from '../../event_hub'; -import JobItem from '../graph/job_item.vue'; +import JobItem from './job_item.vue'; export default { components: { @@ -103,7 +103,7 @@ export default { <template> <gl-dropdown ref="dropdown" - v-gl-tooltip.hover + v-gl-tooltip.hover.ds0 data-testid="mini-pipeline-graph-dropdown" :title="stage.title" variant="link" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue index c707b395192..0528e4c147c 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -17,19 +17,11 @@ export default { user() { return this.pipeline.user; }, - classes() { - const triggererClass = 'pipeline-triggerer'; - - if (this.glFeatures.newPipelinesTable) { - return triggererClass; - } - return `table-section section-10 d-none d-md-block ${triggererClass}`; - }, }, }; </script> <template> - <div :class="classes" data-testid="pipeline-triggerer"> + <div class="pipeline-triggerer" data-testid="pipeline-triggerer"> <user-avatar-link v-if="user" :link-href="user.path" 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 0de520a2ca7..d39e120dc6c 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -49,19 +49,11 @@ export default { autoDevopsHelpPath() { return helpPagePath('topics/autodevops/index.md'); }, - classes() { - const tagsClass = 'pipeline-tags'; - - if (this.glFeatures.newPipelinesTable) { - return tagsClass; - } - return `table-section section-10 d-none d-md-block ${tagsClass}`; - }, }, }; </script> <template> - <div :class="classes" data-testid="pipeline-url-table-cell"> + <div class="pipeline-tags" data-testid="pipeline-url-table-cell"> <gl-link :href="pipeline.path" data-testid="pipeline-url-link" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 19d93e7d083..f14a582d731 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; +import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { isEqual } from 'lodash'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { getParameterByName } from '~/lib/utils/common_utils'; @@ -10,7 +10,6 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../ 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 PipelinesFilteredSearch from './pipelines_filtered_search.vue'; @@ -19,13 +18,13 @@ import PipelinesTableComponent from './pipelines_table.vue'; export default { components: { EmptyState, + GlEmptyState, GlIcon, GlLoadingIcon, NavigationTabs, NavigationControls, PipelinesFilteredSearch, PipelinesTableComponent, - SvgBlankState, TablePagination, }, mixins: [PipelinesMixin], @@ -314,6 +313,7 @@ export default { </div> <pipelines-filtered-search + v-if="stateToRender !== $options.stateMap.emptyState" :project-id="projectId" :params="validatedParams" @filterPipelines="filterPipelines" @@ -333,19 +333,19 @@ export default { :can-set-ci="canCreatePipeline" /> - <svg-blank-state + <gl-empty-state v-else-if="stateToRender === $options.stateMap.error" :svg-path="errorStateSvgPath" - :message=" + :title=" s__(`Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team.`) " /> - <svg-blank-state + <gl-empty-state v-else-if="stateToRender === $options.stateMap.emptyTab" :svg-path="noPipelinesSvgPath" - :message="emptyTabMessage" + :title="emptyTabMessage" /> <div v-else-if="stateToRender === $options.stateMap.tableList"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue new file mode 100644 index 00000000000..c2ec8c57fd7 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue @@ -0,0 +1,143 @@ +<script> +import { GlButton, GlCard, GlSprintf } from '@gitlab/ui'; +import ExperimentTracking from '~/experimentation/experiment_tracking'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { s__, sprintf } from '~/locale'; +import { HELLO_WORLD_TEMPLATE_KEY } from '../../constants'; + +export default { + components: { + GlButton, + GlCard, + GlSprintf, + }, + HELLO_WORLD_TEMPLATE_KEY, + i18n: { + cta: s__('Pipelines|Use template'), + testTemplates: { + title: s__('Pipelines|Use a sample CI/CD template'), + subtitle: s__( + 'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.', + ), + helloWorld: { + title: s__('Pipelines|“Hello world” with GitLab CI/CD'), + description: s__( + 'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a simple pipeline that runs a “Hello world” script.', + ), + }, + }, + templates: { + title: s__('Pipelines|Use a CI/CD template'), + subtitle: s__( + "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.", + ), + description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'), + }, + }, + inject: ['addCiYmlPath', 'suggestedCiTemplates'], + data() { + const templates = this.suggestedCiTemplates.map(({ name, logo }) => { + return { + name, + logo, + link: mergeUrlParams({ template: name }, this.addCiYmlPath), + description: sprintf(this.$options.i18n.templates.description, { name }), + }; + }); + + return { + templates, + helloWorldTemplateUrl: mergeUrlParams( + { template: HELLO_WORLD_TEMPLATE_KEY }, + this.addCiYmlPath, + ), + }; + }, + methods: { + trackEvent(template) { + const tracking = new ExperimentTracking('pipeline_empty_state_templates', { + label: template, + }); + tracking.event('template_clicked'); + }, + }, +}; +</script> +<template> + <div> + <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.testTemplates.title }}</h2> + <p class="gl-text-gray-800 gl-mb-6"> + <gl-sprintf :message="$options.i18n.testTemplates.subtitle"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </p> + + <div class="row gl-mb-8"> + <div class="col-lg-3"> + <gl-card> + <div class="gl-flex-direction-row"> + <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div> + <div class="gl-mb-3"> + <strong class="gl-text-gray-800 gl-mb-2">{{ + $options.i18n.testTemplates.helloWorld.title + }}</strong> + </div> + <p class="gl-font-sm">{{ $options.i18n.testTemplates.helloWorld.description }}</p> + </div> + + <gl-button + category="primary" + variant="confirm" + :href="helloWorldTemplateUrl" + data-testid="test-template-link" + @click="trackEvent($options.HELLO_WORLD_TEMPLATE_KEY)" + > + {{ $options.i18n.cta }} + </gl-button> + </gl-card> + </div> + </div> + + <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.templates.title }}</h2> + <p class="gl-text-gray-800 gl-mb-6">{{ $options.i18n.templates.subtitle }}</p> + + <ul class="gl-list-style-none gl-pl-0"> + <li v-for="template in templates" :key="template.name"> + <div + class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-pt-3" + > + <div class="gl-display-flex gl-flex-direction-row gl-align-items-center"> + <img + width="64" + height="64" + :src="template.logo" + class="gl-mr-6" + data-testid="template-logo" + /> + <div class="gl-flex-direction-row"> + <div class="gl-mb-3"> + <strong class="gl-text-gray-800" data-testid="template-name">{{ + template.name + }}</strong> + </div> + <p class="gl-mb-0 gl-font-sm" data-testid="template-description"> + {{ template.description }} + </p> + </div> + </div> + <gl-button + category="primary" + variant="confirm" + :href="template.link" + data-testid="template-link" + @click="trackEvent(template.name)" + > + {{ $options.i18n.cta }} + </gl-button> + </div> + </li> + </ul> + </div> +</template> 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 aa27aa7e50d..47fc7023222 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,7 +1,6 @@ <script> import { GlTable, GlTooltipDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; import PipelineMiniGraph from './pipeline_mini_graph.vue'; import PipelineOperations from './pipeline_operations.vue'; @@ -10,7 +9,6 @@ import PipelineTriggerer from './pipeline_triggerer.vue'; import PipelineUrl from './pipeline_url.vue'; import PipelinesCommit from './pipelines_commit.vue'; import PipelinesStatusBadge from './pipelines_status_badge.vue'; -import PipelinesTableRowComponent from './pipelines_table_row.vue'; import PipelinesTimeago from './time_ago.vue'; const DEFAULT_TD_CLASS = 'gl-p-5!'; @@ -83,7 +81,6 @@ export default { PipelineOperations, PipelinesStatusBadge, PipelineStopModal, - PipelinesTableRowComponent, PipelinesTimeago, PipelineTriggerer, PipelineUrl, @@ -91,7 +88,6 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [glFeatureFlagMixin()], props: { pipelines: { type: Array, @@ -149,41 +145,7 @@ export default { </script> <template> <div class="ci-table"> - <div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 js-pipeline-status" role="rowheader"> - {{ s__('Pipeline|Status') }} - </div> - <div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader"> - {{ s__('Pipeline|Pipeline') }} - </div> - <div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader"> - {{ s__('Pipeline|Triggerer') }} - </div> - <div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader"> - {{ s__('Pipeline|Commit') }} - </div> - <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader"> - {{ s__('Pipeline|Stages') }} - </div> - <div class="table-section section-15" role="rowheader"></div> - <div class="table-section section-20" role="rowheader"> - <slot name="table-header-actions"></slot> - </div> - </div> - <pipelines-table-row-component - v-for="model in pipelines" - :key="model.id" - :pipeline="model" - :pipeline-schedule-url="pipelineScheduleUrl" - :update-graph-dropdown="updateGraphDropdown" - :view-type="viewType" - :canceling-pipeline="cancelingPipeline" - /> - </div> - <gl-table - v-else :fields="$options.fields" :items="pipelines" tbody-tr-class="commit" 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 deleted file mode 100644 index f684a0b0fcd..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ /dev/null @@ -1,269 +0,0 @@ -<script> -import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; -import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; -import CommitComponent from '~/vue_shared/components/commit.vue'; -import eventHub from '../../event_hub'; -import PipelineMiniGraph from './pipeline_mini_graph.vue'; -import PipelineTriggerer from './pipeline_triggerer.vue'; -import PipelineUrl from './pipeline_url.vue'; -import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import PipelinesManualActionsComponent from './pipelines_manual_actions.vue'; -import PipelinesTimeago from './time_ago.vue'; - -export default { - i18n: { - cancelTitle: __('Cancel'), - redeployTitle: __('Retry'), - }, - directives: { - GlTooltip: GlTooltipDirective, - GlModalDirective, - }, - components: { - PipelinesManualActionsComponent, - PipelinesArtifactsComponent, - CommitComponent, - PipelineMiniGraph, - PipelineUrl, - PipelineTriggerer, - CiBadge, - PipelinesTimeago, - GlButton, - }, - props: { - pipeline: { - type: Object, - required: true, - }, - pipelineScheduleUrl: { - type: String, - required: false, - default: '', - }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, - }, - viewType: { - type: String, - required: true, - }, - cancelingPipeline: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - isRetrying: false, - }; - }, - computed: { - actions() { - if (!this.pipeline || !this.pipeline.details) { - return []; - } - const { details } = this.pipeline; - return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; - }, - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * This field needs a lot of verification, because of different possible cases: - * - * 1. person who is an author of a commit might be a GitLab user - * 2. if person who is an author of a commit is a GitLab user, they can have a GitLab avatar - * 3. If GitLab user does not have avatar they might have a Gravatar - * 4. If committer is not a GitLab User they can have a Gravatar - * 5. We do not have consistent API object in this case - * 6. We should improve API and the code - * - * @returns {Object|Undefined} - */ - commitAuthor() { - let commitAuthorInformation; - - if (!this.pipeline || !this.pipeline.commit) { - return null; - } - - // 1. person who is an author of a commit might be a GitLab user - if (this.pipeline.commit.author) { - // 2. if person who is an author of a commit is a GitLab user - // they can have a GitLab avatar - if (this.pipeline.commit.author.avatar_url) { - commitAuthorInformation = this.pipeline.commit.author; - - // 3. If GitLab user does not have avatar, they might have a Gravatar - } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = { - ...this.pipeline.commit.author, - avatar_url: this.pipeline.commit.author_gravatar_url, - }; - } - // 4. If committer is not a GitLab User, they can have a Gravatar - } else { - commitAuthorInformation = { - avatar_url: this.pipeline.commit.author_gravatar_url, - path: `mailto:${this.pipeline.commit.author_email}`, - username: this.pipeline.commit.author_name, - }; - } - - return commitAuthorInformation; - }, - commitTag() { - return this.pipeline?.ref?.tag; - }, - commitRef() { - return this.pipeline?.ref; - }, - commitUrl() { - return this.pipeline?.commit?.commit_path; - }, - commitShortSha() { - return this.pipeline?.commit?.short_id; - }, - commitTitle() { - return this.pipeline?.commit?.title; - }, - pipelineStatus() { - return this.pipeline?.details?.status ?? {}; - }, - hasStages() { - return this.pipeline?.details?.stages?.length > 0; - }, - displayPipelineActions() { - return ( - this.pipeline.flags.retryable || - this.pipeline.flags.cancelable || - this.pipeline.details.manual_actions.length || - this.pipeline.details.artifacts.length - ); - }, - isChildView() { - return this.viewType === 'child'; - }, - isCancelling() { - return this.cancelingPipeline === this.pipeline.id; - }, - }, - watch: { - pipeline() { - this.isRetrying = false; - }, - }, - methods: { - handleCancelClick() { - eventHub.$emit('openConfirmationModal', { - pipeline: this.pipeline, - endpoint: this.pipeline.cancel_path, - }); - }, - handleRetryClick() { - this.isRetrying = true; - eventHub.$emit('retryPipeline', this.pipeline.retry_path); - }, - handlePipelineActionRequestComplete() { - // warn the pipelines table to update - eventHub.$emit('refreshPipelinesTable'); - }, - }, -}; -</script> -<template> - <div class="commit gl-responsive-table-row"> - <div class="table-section section-10 commit-link"> - <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Status') }}</div> - <div class="table-mobile-content"> - <ci-badge - :status="pipelineStatus" - :show-text="!isChildView" - :icon-classes="'gl-vertical-align-middle!'" - data-qa-selector="pipeline_commit_status" - /> - </div> - </div> - - <pipeline-url :pipeline="pipeline" :pipeline-schedule-url="pipelineScheduleUrl" /> - <pipeline-triggerer :pipeline="pipeline" /> - - <div class="table-section section-wrap section-20"> - <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Commit') }}</div> - <div class="table-mobile-content"> - <commit-component - :tag="commitTag" - :commit-ref="commitRef" - :commit-url="commitUrl" - :merge-request-ref="pipeline.merge_request" - :short-sha="commitShortSha" - :title="commitTitle" - :author="commitAuthor" - :show-ref-info="!isChildView" - /> - </div> - </div> - - <div class="table-section section-wrap section-15 stage-cell"> - <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Stages') }}</div> - <div class="table-mobile-content"> - <pipeline-mini-graph - v-if="hasStages" - :stages="pipeline.details.stages" - :update-dropdown="updateGraphDropdown" - @pipelineActionRequestComplete="handlePipelineActionRequestComplete" - /> - </div> - </div> - - <pipelines-timeago class="gl-text-right" :pipeline="pipeline" /> - - <div - v-if="displayPipelineActions" - class="table-section section-20 table-button-footer pipeline-actions" - > - <div class="btn-group table-action-buttons"> - <pipelines-manual-actions-component v-if="actions.length > 0" :actions="actions" /> - - <pipelines-artifacts-component - v-if="pipeline.details.artifacts.length" - :artifacts="pipeline.details.artifacts" - /> - - <gl-button - v-if="pipeline.flags.retryable" - v-gl-tooltip.hover - :aria-label="$options.i18n.redeployTitle" - :title="$options.i18n.redeployTitle" - :disabled="isRetrying" - :loading="isRetrying" - class="js-pipelines-retry-button" - data-qa-selector="pipeline_retry_button" - icon="repeat" - variant="default" - category="secondary" - @click="handleRetryClick" - /> - - <gl-button - v-if="pipeline.flags.cancelable" - v-gl-tooltip.hover - v-gl-modal-directive="'confirmation-modal'" - :aria-label="$options.i18n.cancelTitle" - :title="$options.i18n.cancelTitle" - :loading="isCancelling" - :disabled="isCancelling" - icon="close" - variant="danger" - category="primary" - class="js-pipelines-cancel-button" - @click="handleCancelClick" - /> - </div> - </div> - </div> -</template> 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 543bdf94307..e6b03751350 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -22,6 +22,12 @@ export default { finishedTime() { return this.pipeline?.details?.finished_at; }, + skipped() { + return this.pipeline?.details?.status?.label === 'skipped'; + }, + stuck() { + return this.pipeline.flags.stuck; + }, durationFormatted() { const date = new Date(this.duration * 1000); @@ -42,46 +48,50 @@ export default { return `${hh}:${mm}:${ss}`; }, - legacySectionClass() { - return !this.glFeatures.newPipelinesTable ? 'table-section section-15' : ''; - }, - legacyTableMobileClass() { - return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : ''; - }, showInProgress() { - return !this.duration && !this.finishedTime; + return !this.duration && !this.finishedTime && !this.skipped; + }, + showSkipped() { + return !this.duration && !this.finishedTime && this.skipped; }, }, }; </script> <template> - <div :class="legacySectionClass"> - <div v-if="!glFeatures.newPipelinesTable" class="table-mobile-header" role="rowheader"> - {{ s__('Pipeline|Duration') }} - </div> - <div :class="legacyTableMobileClass"> - <span v-if="showInProgress" data-testid="pipeline-in-progress"> - <gl-icon name="hourglass" class="gl-vertical-align-baseline! gl-mr-2" :size="12" /> - {{ s__('Pipeline|In progress') }} - </span> + <div> + <span v-if="showInProgress" data-testid="pipeline-in-progress"> + <gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" /> + <gl-icon + v-else + name="hourglass" + class="gl-vertical-align-baseline! gl-mr-2" + :size="12" + data-testid="hourglass-icon" + /> + {{ s__('Pipeline|In progress') }} + </span> + + <span v-if="showSkipped" data-testid="pipeline-skipped"> + <gl-icon name="status_skipped_borderless" class="gl-mr-2" :size="16" /> + {{ s__('Pipeline|Skipped') }} + </span> - <p v-if="duration" class="duration"> - <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" /> - {{ durationFormatted }} - </p> + <p v-if="duration" class="duration"> + <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" /> + {{ durationFormatted }} + </p> - <p v-if="finishedTime" class="finished-at d-none d-md-block"> - <gl-icon name="calendar" class="gl-vertical-align-baseline!" :size="12" /> + <p v-if="finishedTime" class="finished-at d-none d-md-block"> + <gl-icon name="calendar" class="gl-vertical-align-baseline!" :size="12" /> - <time - v-gl-tooltip - :title="tooltipTitle(finishedTime)" - data-placement="top" - data-container="body" - > - {{ timeFormatted(finishedTime) }} - </time> - </p> - </div> + <time + v-gl-tooltip + :title="tooltipTitle(finishedTime)" + data-placement="top" + data-container="body" + > + {{ timeFormatted(finishedTime) }} + </time> + </p> </div> </template> |