diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/pipelines/components/pipelines_list | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipelines_list')
15 files changed, 768 insertions, 423 deletions
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 8a656bb47f4..f8107d288d9 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -1,23 +1,22 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; export default { i18n: { - 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'), + title: s__('Pipelines|Build with confidence'), + description: 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.`), + btnText: s__('Pipelines|Get started with CI/CD'), + noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'), }, name: 'PipelinesEmptyState', components: { - GlButton, + GlEmptyState, }, props: { - helpPagePath: { - type: String, - required: true, - }, emptyStateSvgPath: { type: String, required: true, @@ -27,40 +26,28 @@ export default { required: true, }, }, + computed: { + ciHelpPagePath() { + return helpPagePath('ci/quick_start/index.md'); + }, + }, }; </script> <template> - <div class="row empty-state js-empty-state"> - <div class="col-12"> - <div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div> - </div> - - <div class="col-12"> - <div class="text-content"> - <template v-if="canSetCi"> - <h4 data-testid="header-text" class="gl-text-center"> - {{ s__('Pipelines|Build with confidence') }} - </h4> - <p data-testid="info-text"> - {{ $options.i18n.infoMessage }} - </p> - - <div class="gl-text-center"> - <gl-button - :href="helpPagePath" - variant="info" - category="primary" - data-testid="get-started-pipelines" - > - {{ $options.i18n.buttonMessage }} - </gl-button> - </div> - </template> - - <p v-else class="gl-text-center"> - {{ s__('Pipelines|This project is not currently set up to run pipelines.') }} - </p> - </div> - </div> + <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" + /> </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 new file mode 100644 index 00000000000..05372010d0f --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -0,0 +1,54 @@ +<script> +import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; +/** + * Renders the pipeline mini graph. + */ +export default { + components: { + PipelineStage, + }, + props: { + stages: { + type: Array, + required: true, + }, + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, + stagesClass: { + type: [Array, Object, String], + required: false, + default: '', + }, + isMergeTrain: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + onPipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> +<template> + <div data-testid="widget-mini-pipeline-graph"> + <div + v-for="stage in stages" + :key="stage.name" + :class="stagesClass" + class="stage-container dropdown" + > + <pipeline-stage + :stage="stage" + :update-dropdown="updateDropdown" + :is-merge-train="isMergeTrain" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue new file mode 100644 index 00000000000..81eeead2171 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -0,0 +1,119 @@ +<script> +import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import eventHub from '../../event_hub'; +import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import PipelinesManualActions from './pipelines_manual_actions.vue'; + +export default { + i18n: { + cancelTitle: __('Cancel'), + redeployTitle: __('Retry'), + }, + directives: { + GlTooltip: GlTooltipDirective, + GlModalDirective, + }, + components: { + GlButton, + PipelinesManualActions, + PipelinesArtifactsComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + cancelingPipeline: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + isRetrying: false, + }; + }, + computed: { + displayPipelineActions() { + return ( + this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length + ); + }, + actions() { + if (!this.pipeline || !this.pipeline.details) { + return []; + } + const { details } = this.pipeline; + return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; + }, + 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); + }, + }, +}; +</script> + +<template> + <div v-if="displayPipelineActions" class="gl-text-right"> + <div class="btn-group"> + <pipelines-manual-actions 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> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue new file mode 100644 index 00000000000..bdb7dd06620 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -0,0 +1,152 @@ +<script> +/** + * Renders each stage of the pipeline mini graph. + * + * Given the provided endpoint will make a request to + * fetch the dropdown data when the stage is clicked. + * + * Request is made inside this component to make it reusable between: + * 1. Pipelines main table + * 2. Pipelines table in commit and Merge request views + * 3. Merge request widget + * 4. Commit widget + */ + +import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +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'; + +export default { + components: { + GlIcon, + GlLoadingIcon, + GlDropdown, + JobItem, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + stage: { + type: Object, + required: true, + }, + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, + isMergeTrain: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isLoading: false, + dropdownContent: [], + }; + }, + computed: { + 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) { + this.fetchJobs(); + } + }, + }, + methods: { + onShowDropdown() { + eventHub.$emit('clickedDropdown'); + this.isLoading = true; + this.fetchJobs(); + }, + fetchJobs() { + axios + .get(this.stage.dropdown_path) + .then(({ data }) => { + this.dropdownContent = data.latest_statuses; + this.isLoading = false; + }) + .catch(() => { + this.$refs.dropdown.hide(); + this.isLoading = false; + + Flash(__('Something went wrong on our end.')); + }); + }, + isDropdownOpen() { + return this.$el.classList.contains('show'); + }, + pipelineActionRequestComplete() { + // close the dropdown in MR widget + this.$refs.dropdown.hide(); + + // warn the pipelines table to update + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="dropdown" + v-gl-tooltip.hover + data-testid="mini-pipeline-graph-dropdown" + :title="stage.title" + variant="link" + :lazy="true" + :popper-opts="{ placement: 'bottom' }" + :toggle-class="['mini-pipeline-graph-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" + data-testid="mini-pipeline-graph-dropdown-menu-list" + > + <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> + <template v-if="isMergeTrain"> + <li class="gl-new-dropdown-divider" role="presentation"> + <hr role="separator" aria-orientation="horizontal" class="dropdown-divider" /> + </li> + <li> + <div + class="gl-display-flex gl-align-items-center" + data-testid="warning-message-merge-trains" + > + <div class="menu-item gl-font-sm gl-text-gray-300!"> + {{ s__('Pipeline|Merge train pipeline jobs can not be retried') }} + </div> + </div> + </li> + </template> + </ul> + </gl-dropdown> +</template> 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 6ac60727f23..c707b395192 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -1,10 +1,12 @@ <script> import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { UserAvatarLink, }, + mixins: [glFeatureFlagMixin()], props: { pipeline: { type: Object, @@ -15,11 +17,19 @@ 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="table-section section-10 d-none d-md-block pipeline-triggerer"> + <div :class="classes" 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 823ada133d2..0de520a2ca7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,5 +1,7 @@ <script> import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { SCHEDULE_ORIGIN } from '../../constants'; export default { @@ -12,6 +14,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], inject: { targetProjectFullPath: { default: '', @@ -26,10 +29,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, }, computed: { user() { @@ -44,11 +43,25 @@ export default { this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`, ); }, + autoDevopsTagId() { + return `pipeline-url-autodevops-${this.pipeline.id}`; + }, + 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="table-section section-10 d-none d-md-block pipeline-tags"> + <div :class="classes" data-testid="pipeline-url-table-cell"> <gl-link :href="pipeline.path" data-testid="pipeline-url-link" @@ -103,38 +116,43 @@ export default { data-testid="pipeline-url-failure" >{{ __('error') }}</gl-badge > - <gl-link - v-if="pipeline.flags.auto_devops" - :id="`pipeline-url-autodevops-${pipeline.id}`" - tabindex="0" - data-testid="pipeline-url-autodevops" - role="button" - ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link - > - <gl-popover - :target="`pipeline-url-autodevops-${pipeline.id}`" - triggers="focus" - placement="top" - > - <template #title> - <div class="gl-font-weight-normal gl-line-height-normal"> - <gl-sprintf - :message=" - __( - 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', - ) - " - > - <template #strong="{ content }"> - <b>{{ content }}</b> - </template> - </gl-sprintf> - </div> - </template> - <gl-link :href="autoDevopsHelpPath" target="_blank" rel="noopener noreferrer nofollow">{{ - __('Learn more about Auto DevOps') - }}</gl-link> - </gl-popover> + <template v-if="pipeline.flags.auto_devops"> + <gl-link + :id="autoDevopsTagId" + tabindex="0" + data-testid="pipeline-url-autodevops" + role="button" + > + <gl-badge variant="info" size="sm"> + {{ __('Auto DevOps') }} + </gl-badge> + </gl-link> + <gl-popover :target="autoDevopsTagId" triggers="focus" placement="top"> + <template #title> + <div class="gl-font-weight-normal gl-line-height-normal"> + <gl-sprintf + :message=" + __( + 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', + ) + " + > + <template #strong="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + </div> + </template> + <gl-link + :href="autoDevopsHelpPath" + data-testid="pipeline-url-autodevops-link" + target="_blank" + > + {{ __('Learn more about Auto DevOps') }} + </gl-link> + </gl-popover> + </template> + <gl-badge v-if="pipeline.flags.stuck" variant="warning" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 48009a9fcb8..19d93e7d083 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -52,10 +52,6 @@ export default { required: false, default: '', }, - helpPagePath: { - type: String, - required: true, - }, emptyStateSvgPath: { type: String, required: true, @@ -68,10 +64,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, hasGitlabCi: { type: Boolean, required: true, @@ -337,7 +329,6 @@ export default { <empty-state v-else-if="stateToRender === $options.stateMap.emptyState" - :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" :can-set-ci="canCreatePipeline" /> @@ -362,7 +353,6 @@ export default { :pipelines="state.pipelines" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> </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 b13460b4c68..9c3990f82df 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue @@ -31,6 +31,8 @@ export default { :text="$options.translations.artifacts" :aria-label="$options.translations.artifacts" icon="download" + right + lazy text-sr-only > <gl-dropdown-item diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue new file mode 100644 index 00000000000..cc676883c1d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue @@ -0,0 +1,85 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CommitComponent from '~/vue_shared/components/commit.vue'; + +export default { + components: { + CommitComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + 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; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <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" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue index 6890cbb9bed..b94f1a42039 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue @@ -82,6 +82,7 @@ export default { :loading="isLoading" data-testid="pipelines-manual-actions-dropdown" right + lazy icon="play" > <gl-dropdown-item diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue new file mode 100644 index 00000000000..cc3c8d522b3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -0,0 +1,37 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; + +export default { + components: { + CiBadge, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + pipelineStatus() { + return this.pipeline?.details?.status ?? {}; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <ci-badge + :status="pipelineStatus" + :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" + data-qa-selector="pipeline_commit_status" + /> +</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 24c67184e56..aa27aa7e50d 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,22 +1,97 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +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'; import PipelineStopModal from './pipeline_stop_modal.vue'; +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!'; +const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; +const DEFAULT_TH_CLASSES = + 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!'; -/** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ export default { + fields: [ + { + key: 'status', + label: s__('Pipeline|Status'), + thClass: DEFAULT_TH_CLASSES, + columnClass: 'gl-w-10p', + tdClass: DEFAULT_TD_CLASS, + thAttr: { 'data-testid': 'status-th' }, + }, + { + key: 'pipeline', + label: s__('Pipeline|Pipeline'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'pipeline-th' }, + }, + { + key: 'triggerer', + label: s__('Pipeline|Triggerer'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'triggerer-th' }, + }, + { + key: 'commit', + label: s__('Pipeline|Commit'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'commit-th' }, + }, + { + key: 'stages', + label: s__('Pipeline|Stages'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'stages-th' }, + }, + { + key: 'timeago', + label: s__('Pipeline|Duration'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'timeago-th' }, + }, + { + key: 'actions', + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'actions-th' }, + }, + ], components: { - PipelinesTableRowComponent, + GlTable, + PipelinesCommit, + PipelineMiniGraph, + PipelineOperations, + PipelinesStatusBadge, PipelineStopModal, + PipelinesTableRowComponent, + PipelinesTimeago, + PipelineTriggerer, + PipelineUrl, }, directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], props: { pipelines: { type: Array, @@ -32,10 +107,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -70,42 +141,103 @@ export default { eventHub.$emit('postAction', this.endpoint); this.cancelingPipeline = this.pipelineId; }, + onPipelineActionRequestComplete() { + eventHub.$emit('refreshPipelinesTable'); + }, }, }; </script> <template> <div class="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 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> - <pipelines-table-row-component - v-for="model in pipelines" - :key="model.id" - :pipeline="model" - :pipeline-schedule-url="pipelineScheduleUrl" - :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" - :view-type="viewType" - :canceling-pipeline="cancelingPipeline" - /> + + <gl-table + v-else + :fields="$options.fields" + :items="pipelines" + tbody-tr-class="commit" + :tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }" + stacked="lg" + fixed + > + <template #head(actions)> + <span class="gl-display-block gl-lg-display-none!">{{ s__('Pipeline|Actions') }}</span> + <slot name="table-header-actions"></slot> + </template> + + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(status)="{ item }"> + <pipelines-status-badge :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-url :pipeline="item" :pipeline-schedule-url="pipelineScheduleUrl" /> + </template> + + <template #cell(triggerer)="{ item }"> + <pipeline-triggerer :pipeline="item" /> + </template> + + <template #cell(commit)="{ item }"> + <pipelines-commit :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(stages)="{ item }"> + <div class="stage-cell"> + <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> + <div></div> + <pipeline-mini-graph + v-if="item.details && item.details.stages && item.details.stages.length > 0" + :stages="item.details.stages" + :update-dropdown="updateGraphDropdown" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + /> + </div> + </template> + + <template #cell(timeago)="{ item }"> + <pipelines-timeago :pipeline="item" /> + </template> + + <template #cell(actions)="{ item }"> + <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> + </template> + </gl-table> + <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" /> </div> </template> 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 572abe2a24a..f684a0b0fcd 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 @@ -3,13 +3,12 @@ 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 { PIPELINES_TABLE } from '../../constants'; 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 PipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import PipelineStage from './stage.vue'; +import PipelinesManualActionsComponent from './pipelines_manual_actions.vue'; import PipelinesTimeago from './time_ago.vue'; export default { @@ -22,10 +21,10 @@ export default { GlModalDirective, }, components: { - PipelinesActionsComponent, + PipelinesManualActionsComponent, PipelinesArtifactsComponent, CommitComponent, - PipelineStage, + PipelineMiniGraph, PipelineUrl, PipelineTriggerer, CiBadge, @@ -47,10 +46,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -61,7 +56,6 @@ export default { default: null, }, }, - pipelinesTable: PIPELINES_TABLE, data() { return { isRetrying: false, @@ -137,15 +131,12 @@ export default { commitTitle() { return this.pipeline?.commit?.title; }, - pipelineDuration() { - return this.pipeline?.details?.duration ?? 0; - }, - pipelineFinishedAt() { - return this.pipeline?.details?.finished_at ?? ''; - }, pipelineStatus() { return this.pipeline?.details?.status ?? {}; }, + hasStages() { + return this.pipeline?.details?.stages?.length > 0; + }, displayPipelineActions() { return ( this.pipeline.flags.retryable || @@ -177,6 +168,10 @@ export default { this.isRetrying = true; eventHub.$emit('retryPipeline', this.pipeline.retry_path); }, + handlePipelineActionRequestComplete() { + // warn the pipelines table to update + eventHub.$emit('refreshPipelinesTable'); + }, }, }; </script> @@ -194,11 +189,7 @@ export default { </div> </div> - <pipeline-url - :pipeline="pipeline" - :pipeline-schedule-url="pipelineScheduleUrl" - :auto-devops-help-path="autoDevopsHelpPath" - /> + <pipeline-url :pipeline="pipeline" :pipeline-schedule-url="pipelineScheduleUrl" /> <pipeline-triggerer :pipeline="pipeline" /> <div class="table-section section-wrap section-20"> @@ -220,35 +211,23 @@ export default { <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"> - <template v-if="pipeline.details.stages.length > 0"> - <div - v-for="(stage, index) in pipeline.details.stages" - :key="index" - class="stage-container dropdown" - data-testid="widget-mini-pipeline-graph" - > - <pipeline-stage - :type="$options.pipelinesTable" - :stage="stage" - :update-dropdown="updateGraphDropdown" - /> - </div> - </template> + <pipeline-mini-graph + v-if="hasStages" + :stages="pipeline.details.stages" + :update-dropdown="updateGraphDropdown" + @pipelineActionRequestComplete="handlePipelineActionRequestComplete" + /> </div> </div> - <pipelines-timeago - class="gl-text-right" - :duration="pipelineDuration" - :finished-time="pipelineFinishedAt" - /> + <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-actions-component v-if="actions.length > 0" :actions="actions" /> + <pipelines-manual-actions-component v-if="actions.length > 0" :actions="actions" /> <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue deleted file mode 100644 index f5dfb9e72d5..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ /dev/null @@ -1,234 +0,0 @@ -<script> -/** - * Renders each stage of the pipeline mini graph. - * - * Given the provided endpoint will make a request to - * fetch the dropdown data when the stage is clicked. - * - * Request is made inside this component to make it reusable between: - * 1. Pipelines main table - * 2. Pipelines table in commit and Merge request views - * 3. Merge request widget - * 4. Commit widget - */ -import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import $ from 'jquery'; -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'; - -export default { - components: { - GlIcon, - GlLoadingIcon, - GlDropdown, - JobItem, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [glFeatureFlagsMixin()], - props: { - stage: { - type: Object, - required: true, - }, - - updateDropdown: { - type: Boolean, - required: false, - default: false, - }, - - type: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - isLoading: false, - dropdownContent: [], - }; - }, - computed: { - 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) { - this.fetchJobs(); - } - }, - }, - updated() { - 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'); - this.isLoading = true; - this.fetchJobs(); - } - }, - fetchJobs() { - axios - .get(this.stage.dropdown_path) - .then(({ data }) => { - this.dropdownContent = data.latest_statuses; - this.isLoading = false; - }) - .catch(() => { - 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, - * so we stop propagation of the click event inside the dropdown. - * - * 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() { - $( - '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', - this.$el, - ).on('click', (e) => { - 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 { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, - }, -}; -</script> - -<template> - <div class="dropdown"> - <gl-dropdown - v-if="isCiMiniPipelineGlDropdown" - ref="stageGlDropdown" - v-gl-tooltip.hover - data-testid="mini-pipeline-graph-dropdown" - :title="stage.title" - 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" - data-testid="mini-pipeline-graph-dropdown-menu-list" - > - <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> - </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/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 5548a1021f5..543bdf94307 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,6 @@ <script> import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { @@ -7,23 +8,19 @@ export default { GlTooltip: GlTooltipDirective, }, components: { GlIcon }, - mixins: [timeagoMixin], + mixins: [timeagoMixin, glFeatureFlagMixin()], props: { - finishedTime: { - type: String, - required: true, - }, - duration: { - type: Number, + pipeline: { + type: Object, required: true, }, }, computed: { - hasDuration() { - return this.duration > 0; + duration() { + return this.pipeline?.details?.duration; }, - hasFinishedTime() { - return this.finishedTime !== ''; + finishedTime() { + return this.pipeline?.details?.finished_at; }, durationFormatted() { const date = new Date(this.duration * 1000); @@ -45,20 +42,36 @@ 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; + }, }, }; </script> <template> - <div class="table-section section-15"> - <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div> - <div class="table-mobile-content"> - <p v-if="hasDuration" class="duration"> - <gl-icon name="timer" class="gl-vertical-align-baseline!" /> + <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> + + <p v-if="duration" class="duration"> + <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" /> {{ durationFormatted }} </p> - <p v-if="hasFinishedTime" class="finished-at d-none d-md-block"> - <gl-icon name="calendar" class="gl-vertical-align-baseline!" /> + <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 |