diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
19 files changed, 631 insertions, 687 deletions
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue deleted file mode 100644 index 0cdffbde05b..00000000000 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ /dev/null @@ -1,95 +0,0 @@ -<script> - /* eslint-disable no-alert */ - - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, - }, - components: { - loadingIcon, - icon, - }, - props: { - endpoint: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - icon: { - type: String, - required: true, - }, - cssClass: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - type: { - type: String, - required: true, - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - created() { - // We're using eventHub to listen to the modal here instead of - // using props because it would would make the parent components - // much more complex to keep track of the loading state of each button - eventHub.$on('postAction', this.setLoading); - }, - beforeDestroy() { - eventHub.$off('postAction', this.setLoading); - }, - methods: { - onClick() { - eventHub.$emit('openConfirmationModal', { - pipelineId: this.pipelineId, - endpoint: this.endpoint, - type: this.type, - }); - }, - setLoading(endpoint) { - if (endpoint === this.endpoint) { - this.isLoading = true; - } - }, - }, - }; -</script> - -<template> - <button - v-tooltip - type="button" - @click="onClick" - :class="buttonClass" - :title="title" - :aria-label="title" - data-container="body" - data-placement="top" - :disabled="isLoading"> - <icon - :name="icon" - /> - <loading-icon v-if="isLoading" /> - </button> -</template> diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue index 8d3d6223d7b..f3219b8291c 100644 --- a/app/assets/javascripts/pipelines/components/blank_state.vue +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -17,13 +17,13 @@ <template> <div class="row empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content"> <img :src="svgPath" /> </div> </div> - <div class="col-xs-12 text-center"> + <div class="col-12 text-center"> <div class="text-content"> <h4>{{ message }}</h4> </div> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 10ac8c08bed..50c27bed9fd 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -19,13 +19,13 @@ </script> <template> <div class="row empty-state js-empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content svg-250"> <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12"> + <div class="col-12"> <div class="text-content"> <template v-if="canSetCi"> @@ -34,7 +34,7 @@ </h4> <p> - {{ s__(`Pipelines|Continous Integration can help + {{ s__(`Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.`) }} diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index d7effb27bff..1f152ed438d 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,60 +1,99 @@ <script> - import tooltip from '../../../vue_shared/directives/tooltip'; - import icon from '../../../vue_shared/components/icon.vue'; - import { dasherize } from '../../../lib/utils/text_utility'; - /** - * Renders either a cancel, retry or play icon pointing to the given path. - * TODO: Remove UJS from here and use an async request instead. - */ - export default { - components: { - icon, +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; +import { dasherize } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + +/** + * Renders either a cancel, retry or play icon button and handles the post request + * + * Used in: + * - mr widget mini pipeline graph: `mr_widget_pipeline.vue` + * - pipelines table + * - pipelines table in merge request page + * - pipelines table in commit page + * - pipelines detail page in big graph + */ +export default { + components: { + Icon, + }, + + directives: { + tooltip, + }, + + props: { + tooltipText: { + type: String, + required: true, + }, + + link: { + type: String, + required: true, }, - directives: { - tooltip, + actionIcon: { + type: String, + required: true, }, - props: { - tooltipText: { - type: String, - required: true, - }, - - link: { - type: String, - required: true, - }, - - actionMethod: { - type: String, - required: true, - }, - - actionIcon: { - type: String, - required: true, - }, + }, + data() { + return { + isDisabled: false, + }; + }, + + computed: { + cssClass() { + const actionIconDash = dasherize(this.actionIcon); + return `${actionIconDash} js-icon-${actionIconDash}`; }, + }, + methods: { + /** + * The request should not be handled here. + * However due to this component being used in several + * different apps it avoids repetition & complexity. + * + */ + onClickAction() { + $(this.$el).tooltip('hide'); + + this.isDisabled = true; + + axios.post(`${this.link}.json`) + .then(() => { + this.isDisabled = false; + this.$emit('pipelineActionRequestComplete'); + }) + .catch(() => { + this.isDisabled = false; - computed: { - cssClass() { - const actionIconDash = dasherize(this.actionIcon); - return `${actionIconDash} js-icon-${actionIconDash}`; - }, + createFlash(__('An error occurred while making the request.')); + }); }, - }; + }, +}; </script> <template> - <a + <button v-tooltip - :data-method="actionMethod" :title="tooltipText" - :href="link" - class="ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" + :disabled="isDisabled" + type="button" + class="js-ci-action btn btn-blank +btn-transparent ci-action-icon-container ci-action-icon-wrapper" data-container="body" + data-boundary="viewport" + @click="onClickAction" > - <icon :name="actionIcon" /> - </a> + <icon :name="actionIcon"/> + </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue deleted file mode 100644 index 7c4fd65e36f..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ /dev/null @@ -1,53 +0,0 @@ -<script> - import icon from '../../../vue_shared/components/icon.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; - - /** - * Renders either a cancel, retry or play icon pointing to the given path. - * TODO: Remove UJS from here and use an async request instead. - */ - export default { - components: { - icon, - }, - - directives: { - tooltip, - }, - props: { - tooltipText: { - type: String, - required: true, - }, - - link: { - type: String, - required: true, - }, - - actionMethod: { - type: String, - required: true, - }, - - actionIcon: { - type: String, - required: true, - }, - }, - }; -</script> -<template> - <a - v-tooltip - :data-method="actionMethod" - :title="tooltipText" - :href="link" - rel="nofollow" - class="ci-action-icon-wrapper js-ci-status-icon" - data-container="body" - aria-label="Job's action" - > - <icon :name="actionIcon" /> - </a> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index be213c2ee78..e047d10ac93 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,87 +1,95 @@ <script> - import $ from 'jquery'; - import jobNameComponent from './job_name_component.vue'; - import jobComponent from './job_component.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; +import $ from 'jquery'; +import JobNameComponent from './job_name_component.vue'; +import JobComponent from './job_component.vue'; +import tooltip from '../../../vue_shared/directives/tooltip'; - /** - * Renders the dropdown for the pipeline graph. - * - * The following object should be provided as `job`: - * - * { - * "id": 4256, - * "name": "test", - * "status": { - * "icon": "icon_status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } - */ - export default { - directives: { - tooltip, - }, +/** + * Renders the dropdown for the pipeline graph. + * + * The following object should be provided as `job`: + * + * { + * "id": 4256, + * "name": "test", + * "status": { + * "icon": "icon_status_success", + * "text": "passed", + * "label": "passed", + * "group": "success", + * "details_path": "/root/ci-mock/builds/4256", + * "action": { + * "icon": "retry", + * "title": "Retry", + * "path": "/root/ci-mock/builds/4256/retry", + * "method": "post" + * } + * } + * } + */ +export default { + directives: { + tooltip, + }, - components: { - jobComponent, - jobNameComponent, - }, + components: { + JobComponent, + JobNameComponent, + }, - props: { - job: { - type: Object, - required: true, - }, + props: { + job: { + type: Object, + required: true, }, + }, - computed: { - tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; - }, + computed: { + tooltipText() { + return `${this.job.name} - ${this.job.status.label}`; }, + }, - mounted() { - this.stopDropdownClickPropagation(); - }, + mounted() { + this.stopDropdownClickPropagation(); + }, - methods: { - /** - * 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. + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name or the action icon + * the dropdown should not be closed 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. */ - stopDropdownClickPropagation() { - $(this.$el - .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + stopDropdownClickPropagation() { + $( + '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item', + this.$el, + ).on('click', e => { + e.stopPropagation(); + }); + }, + + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); }, - }; + }, +}; </script> <template> - <div class="ci-job-dropdown-container"> + <div class="ci-job-dropdown-container dropdown dropright"> <button v-tooltip + :title="tooltipText" type="button" data-toggle="dropdown" data-container="body" + data-boundary="viewport" + data-display="static" class="dropdown-menu-toggle build-content" - :title="tooltipText"> + > <job-name-component :name="job.name" @@ -98,11 +106,12 @@ <ul> <li v-for="(item, i) in job.jobs" - :key="i"> + :key="i" + > <job-component :job="item" - :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ab84711d4a2..4ec67f6c01b 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,54 +1,57 @@ <script> - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import stageColumnComponent from './stage_column_component.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import StageColumnComponent from './stage_column_component.vue'; - export default { - components: { - stageColumnComponent, - loadingIcon, +export default { + components: { + StageColumnComponent, + LoadingIcon, + }, + props: { + isLoading: { + type: Boolean, + required: true, }, + pipeline: { + type: Object, + required: true, + }, + }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, + computed: { + graph() { + return this.pipeline.details && this.pipeline.details.stages; }, + }, - computed: { - graph() { - return this.pipeline.details && this.pipeline.details.stages; - }, + methods: { + capitalizeStageName(name) { + return name.charAt(0).toUpperCase() + name.slice(1); }, - methods: { - capitalizeStageName(name) { - return name.charAt(0).toUpperCase() + name.slice(1); - }, + isFirstColumn(index) { + return index === 0; + }, - isFirstColumn(index) { - return index === 0; - }, + stageConnectorClass(index, stage) { + let className; - stageConnectorClass(index, stage) { - let className; + // If it's the first stage column and only has one job + if (index === 0 && stage.groups.length === 1) { + className = 'no-margin'; + } else if (index > 0) { + // If it is not the first column + className = 'left-margin'; + } - // If it's the first stage column and only has one job - if (index === 0 && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } + return className; + }, - return className; - }, + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); }, - }; + }, +}; </script> <template> <div class="build-content middle-block js-pipeline-graph"> @@ -70,6 +73,7 @@ :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" + @refreshPipelineGraph="refreshPipelineGraph" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 9b136573135..886e62ab1a7 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,95 +1,90 @@ <script> - import actionComponent from './action_component.vue'; - import dropdownActionComponent from './dropdown_action_component.vue'; - import jobNameComponent from './job_name_component.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; - - /** - * 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": "icon_status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } - */ - - export default { - components: { - actionComponent, - dropdownActionComponent, - jobNameComponent, +import ActionComponent from './action_component.vue'; +import JobNameComponent from './job_name_component.vue'; +import tooltip from '../../../vue_shared/directives/tooltip'; + +/** + * 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": "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 { + components: { + ActionComponent, + JobNameComponent, + }, + directives: { + tooltip, + }, + props: { + job: { + type: Object, + required: true, }, - - directives: { - tooltip, + cssClassJobName: { + type: String, + required: false, + default: '', }, - props: { - job: { - type: Object, - required: true, - }, - - cssClassJobName: { - type: String, - required: false, - default: '', - }, - - isDropdown: { - type: Boolean, - required: false, - default: false, - }, + }, + computed: { + status() { + return this.job && this.job.status ? this.job.status : {}; }, - computed: { - status() { - return this.job && this.job.status ? this.job.status : {}; - }, - - tooltipText() { - const textBuilder = []; + tooltipText() { + const textBuilder = []; - if (this.job.name) { - textBuilder.push(this.job.name); - } + if (this.job.name) { + textBuilder.push(this.job.name); + } - if (this.job.name && this.status.label) { - textBuilder.push('-'); - } + if (this.job.name && this.status.tooltip) { + textBuilder.push('-'); + } - if (this.status.label) { - textBuilder.push(`${this.job.status.label}`); - } + if (this.status.tooltip) { + textBuilder.push(`${this.job.status.tooltip}`); + } - return textBuilder.join(' '); - }, + 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; - }, + /** + * 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; + }, + }, + methods: { + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); }, - }; + }, +}; </script> <template> <div class="ci-job-component"> @@ -100,6 +95,8 @@ :title="tooltipText" :class="cssClassJobName" data-container="body" + data-html="true" + data-boundary="viewport" class="js-pipeline-graph-job-link" > @@ -110,11 +107,12 @@ </a> <div - v-else v-tooltip - class="js-job-component-tooltip" + v-else :title="tooltipText" :class="cssClassJobName" + class="js-job-component-tooltip non-details-job-component" + data-html="true" data-container="body" > @@ -125,19 +123,11 @@ </div> <action-component - v-if="hasAction && !isDropdown" - :tooltip-text="status.action.title" - :link="status.action.path" - :action-icon="status.action.icon" - :action-method="status.action.method" - /> - - <dropdown-action-component - v-if="hasAction && isDropdown" + v-if="hasAction" :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :action-method="status.action.method" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </div> </template> 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 7adcf4017b8..2c728582b7c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,55 +1,59 @@ <script> - import jobComponent from './job_component.vue'; - import dropdownJobComponent from './dropdown_job_component.vue'; +import JobComponent from './job_component.vue'; +import DropdownJobComponent from './dropdown_job_component.vue'; - export default { - components: { - jobComponent, - dropdownJobComponent, +export default { + components: { + JobComponent, + DropdownJobComponent, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - jobs: { - type: Array, - required: true, - }, + jobs: { + type: Array, + required: true, + }, + + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, + stageConnectorClass: { + type: String, + required: false, + default: '', + }, + }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, + methods: { + firstJob(list) { + return list[0]; }, - methods: { - firstJob(list) { - return list[0]; - }, + jobId(job) { + return `ci-badge-${job.name}`; + }, - jobId(job) { - return `ci-badge-${job.name}`; - }, + buildConnnectorClass(index) { + return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + }, - buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; - }, + pipelineActionRequestComplete() { + this.$emit('refreshPipelineGraph'); }, - }; + }, +}; </script> <template> <li - class="stage-column" - :class="stageConnectorClass"> + :class="stageConnectorClass" + class="stage-column"> <div class="stage-name"> {{ title }} </div> @@ -58,9 +62,9 @@ <li v-for="(job, index) in jobs" :key="job.id" - class="build" :class="buildConnnectorClass(index)" :id="jobId(job)" + class="build" > <div class="curve"></div> @@ -69,11 +73,13 @@ v-if="job.size === 1" :job="job" css-class-job-name="build-content" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> <dropdown-job-component v-if="job.size > 1" :job="job" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index e08c2092680..5b212ee8931 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -82,11 +82,11 @@ <ci-header v-if="shouldRenderContent" :status="status" - item-name="Pipeline" :item-id="pipeline.id" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" + item-name="Pipeline" @actionClicked="postAction" /> <loading-icon diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index eba5678e3e5..1fce9f16ee0 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -50,10 +50,10 @@ <loading-button v-if="resetCachePath" - @click="onClickResetCache" :loading="isResetCacheButtonLoading" - class="btn btn-default js-clear-cache" :label="s__('Pipelines|Clear Runner Caches')" + class="btn btn-default js-clear-cache" + @click="onClickResetCache" /> <a diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index ceb4d9ca604..a107e579457 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -46,7 +46,7 @@ }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> + <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -55,10 +55,10 @@ <span>by</span> <user-avatar-link v-if="user" - class="js-pipeline-url-user" :link-href="pipeline.user.path" :img-src="pipeline.user.avatar_url" :tooltip-text="pipeline.user.name" + class="js-pipeline-url-user" /> <span v-if="!user" @@ -67,37 +67,37 @@ </span> <div class="label-container"> <span - v-if="pipeline.flags.latest" v-tooltip - class="js-pipeline-url-latest label label-success" + v-if="pipeline.flags.latest" + class="js-pipeline-url-latest badge badge-success" title="Latest pipeline for this branch"> latest </span> <span - v-if="pipeline.flags.yaml_errors" v-tooltip - class="js-pipeline-url-yaml label label-danger" - :title="pipeline.yaml_errors"> + v-if="pipeline.flags.yaml_errors" + :title="pipeline.yaml_errors" + class="js-pipeline-url-yaml badge badge-danger"> yaml invalid </span> <span - v-if="pipeline.flags.failure_reason" v-tooltip - class="js-pipeline-url-failure label label-danger" - :title="pipeline.failure_reason"> + v-if="pipeline.flags.failure_reason" + :title="pipeline.failure_reason" + class="js-pipeline-url-failure badge badge-danger"> error </span> <a + v-popover="popoverOptions" v-if="pipeline.flags.auto_devops" tabindex="0" - class="js-pipeline-url-autodevops label label-info autodevops-badge" - v-popover="popoverOptions" + class="js-pipeline-url-autodevops badge badge-info autodevops-badge" role="button"> Auto DevOps </a> <span v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck label label-warning"> + class="js-pipeline-url-stuck badge badge-warning"> stuck </span> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index e0a7284124d..b31b4bad7a0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -7,10 +7,7 @@ import TablePagination from '../../vue_shared/components/table_pagination.vue'; import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; import NavigationControls from './nav_controls.vue'; - import { - getParameterByName, - parseQueryStringIntoObject, - } from '../../lib/utils/common_utils'; + import { getParameterByName } from '../../lib/utils/common_utils'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { @@ -19,10 +16,7 @@ NavigationTabs, NavigationControls, }, - mixins: [ - pipelinesMixin, - CIPaginationMixin, - ], + mixins: [pipelinesMixin, CIPaginationMixin], props: { store: { type: Object, @@ -147,25 +141,26 @@ */ shouldRenderTabs() { const { stateMap } = this.$options; - return this.hasMadeRequest && - [ - stateMap.loading, - stateMap.tableList, - stateMap.error, - stateMap.emptyTab, - ].includes(this.stateToRender); + return ( + this.hasMadeRequest && + [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes( + this.stateToRender, + ) + ); }, shouldRenderButtons() { - return (this.newPipelinePath || - this.resetCachePath || - this.ciLintPath) && this.shouldRenderTabs; + return ( + (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs + ); }, shouldRenderPagination() { - return !this.isLoading && + return ( + !this.isLoading && this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage; + this.state.pageInfo.total > this.state.pageInfo.perPage + ); }, emptyTabMessage() { @@ -229,15 +224,13 @@ }, methods: { successCallback(resp) { - return resp.json().then((response) => { - // Because we are polling & the user is interacting verify if the response received - // matches the last request made - if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { - this.store.storeCount(response.count); - this.store.storePagination(resp.headers); - this.setCommonData(response.pipelines); - } - }); + // Because we are polling & the user is interacting verify if the response received + // matches the last request made + if (_.isEqual(resp.config.params, this.requestData)) { + this.store.storeCount(resp.data.count); + this.store.storePagination(resp.headers); + this.setCommonData(resp.data.pipelines); + } }, /** * Handles URL and query parameter changes. @@ -251,8 +244,9 @@ this.updateInternalState(parameters); // fetch new data - return this.service.getPipelines(this.requestData) - .then((response) => { + return this.service + .getPipelines(this.requestData) + .then(response => { this.isLoading = false; this.successCallback(response); @@ -271,13 +265,11 @@ handleResetRunnersCache(endpoint) { this.isResetCacheButtonLoading = true; - this.service.postAction(endpoint) + this.service + .postAction(endpoint) .then(() => { this.isResetCacheButtonLoading = false; - createFlash( - s__('Pipelines|Project cache successfully reset.'), - 'notice', - ); + createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice'); }) .catch(() => { this.isResetCacheButtonLoading = false; @@ -290,8 +282,8 @@ <template> <div class="pipelines-container"> <div - class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="shouldRenderTabs || shouldRenderButtons" + class="top-area scrolling-tabs-container inner-page-scroll-tabs" > <div class="fade-left"> <i @@ -311,8 +303,8 @@ <navigation-tabs v-if="shouldRenderTabs" :tabs="tabs" - @onChangeTab="onChangeTab" scope="pipelines" + @onChangeTab="onChangeTab" /> <navigation-controls @@ -320,8 +312,8 @@ :new-pipeline-path="newPipelinePath" :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" - @resetRunnersCache="handleResetRunnersCache" :is-reset-cache-button-loading="isResetCacheButtonLoading" + @resetRunnersCache="handleResetRunnersCache" /> </div> @@ -355,8 +347,8 @@ /> <div - class="table-holder" v-else-if="stateToRender === $options.stateMap.tableList" + class="table-holder" > <pipelines-table-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 3297af7bde4..5070c253f11 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -44,13 +44,13 @@ <div class="btn-group"> <button v-tooltip + :disabled="isLoading" type="button" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" title="Manual job" data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading" > <icon name="play" @@ -63,17 +63,17 @@ <loading-icon v-if="isLoading" /> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(action, i) in actions" :key="i" > <button + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)" type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" > {{ action.name }} </button> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 1b9e0f917a4..490df47e154 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -37,14 +37,14 @@ > </i> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(artifact, i) in artifacts" :key="i"> <a + :href="artifact.path" rel="nofollow" download - :href="artifact.path" > Download {{ artifact.name }} artifacts </a> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index c9028952ddd..2e777783636 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,7 +1,7 @@ <script> - import modal from '~/vue_shared/components/modal.vue'; + import Modal from '~/vue_shared/components/gl_modal.vue'; import { s__, sprintf } from '~/locale'; - import pipelinesTableRowComponent from './pipelines_table_row.vue'; + import PipelinesTableRowComponent from './pipelines_table_row.vue'; import eventHub from '../event_hub'; /** @@ -11,8 +11,8 @@ */ export default { components: { - pipelinesTableRowComponent, - modal, + PipelinesTableRowComponent, + Modal, }, props: { pipelines: { @@ -37,30 +37,19 @@ return { pipelineId: '', endpoint: '', - type: '', + cancelingPipeline: null, }; }, computed: { modalTitle() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false) : - sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false); + return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + pipelineId: `${this.pipelineId}`, + }, false); }, modalText() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false) : - sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false); - }, - primaryButtonLabel() { - return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline'); + return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false); }, }, created() { @@ -73,10 +62,10 @@ setModalData(data) { this.pipelineId = data.pipelineId; this.endpoint = data.endpoint; - this.type = data.type; }, onSubmit() { eventHub.$emit('postAction', this.endpoint); + this.cancelingPipeline = this.pipelineId; }, }, }; @@ -119,21 +108,18 @@ :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" + :canceling-pipeline="cancelingPipeline" /> + <modal id="confirmation-modal" - :title="modalTitle" - :text="modalText" - kind="danger" - :primary-button-label="primaryButtonLabel" + :header-title-text="modalTitle" + :footer-primary-button-text="s__('Pipeline|Stop pipeline')" + footer-primary-button-variant="danger" @submit="onSubmit" > - <template - slot="body" - slot-scope="props" - > - <p v-html="props.text"></p> - </template> + <span v-html="modalText"></span> </modal> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4cbd67e0372..b2744a30c2a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,15 @@ <script> - /* eslint-disable no-param-reassign */ - import asyncButtonComponent from './async_button.vue'; - import pipelinesActionsComponent from './pipelines_actions.vue'; - import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; - import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; - import pipelineStage from './stage.vue'; - import pipelineUrl from './pipeline_url.vue'; - import pipelinesTimeago from './time_ago.vue'; - import commitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + import PipelinesActionsComponent from './pipelines_actions.vue'; + import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import CiBadge from '../../vue_shared/components/ci_badge_link.vue'; + import PipelineStage from './stage.vue'; + import PipelineUrl from './pipeline_url.vue'; + import 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'; /** * Pipeline table row. @@ -16,14 +18,15 @@ */ export default { components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, + PipelinesActionsComponent, + PipelinesArtifactsComponent, + CommitComponent, + PipelineStage, + PipelineUrl, + CiBadge, + PipelinesTimeago, + LoadingButton, + Icon, }, props: { pipeline: { @@ -43,6 +46,17 @@ type: String, required: true, }, + cancelingPipeline: { + type: String, + required: false, + default: null, + }, + }, + pipelinesTable: PIPELINES_TABLE, + data() { + return { + isRetrying: false, + }; }, computed: { /** @@ -119,8 +133,10 @@ if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { + // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { + // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; @@ -215,6 +231,23 @@ isChildView() { return this.viewType === 'child'; }, + + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, + }, + + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipeline.id, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, }, }; </script> @@ -268,10 +301,11 @@ <div class="table-mobile-content"> <template v-if="pipeline.details.stages.length > 0"> <div - class="stage-container dropdown js-mini-pipeline-graph" v-for="(stage, index) in pipeline.details.stages" - :key="index"> + :key="index" + class="stage-container dropdown js-mini-pipeline-graph"> <pipeline-stage + :type="$options.pipelinesTable" :stage="stage" :update-dropdown="updateGraphDropdown" /> @@ -287,7 +321,8 @@ <div v-if="displayPipelineActions" - class="table-section section-20 table-button-footer pipeline-actions"> + class="table-section section-20 table-button-footer pipeline-actions" + > <div class="btn-group table-action-buttons"> <pipelines-actions-component v-if="pipeline.details.manual_actions.length" @@ -296,33 +331,31 @@ <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" - class="hidden-xs hidden-sm" :artifacts="pipeline.details.artifacts" + class="d-none d-sm-none d-md-block" /> - <async-button-component + <loading-button v-if="pipeline.flags.retryable" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" - :pipeline-id="pipeline.id" - data-toggle="modal" - data-target="#confirmation-modal" - type="retry" - /> + :loading="isRetrying" + :disabled="isRetrying" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + @click="handleRetryClick" + > + <icon name="repeat" /> + </loading-button> - <async-button-component + <loading-button v-if="pipeline.flags.cancelable" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Stop" - icon="close" - :pipeline-id="pipeline.id" + :loading="isCancelling" + :disabled="isCancelling" data-toggle="modal" data-target="#confirmation-modal" - type="stop" - /> + container-class="js-pipelines-cancel-button btn btn-remove" + @click="handleCancelClick" + > + <icon name="close" /> + </loading-button> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 8bc7a1f20b2..b9231c002fd 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,155 +1,180 @@ <script> - import $ from 'jquery'; - - /** - * 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 Flash from '../../flash'; - import icon from '../../vue_shared/components/icon.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - components: { - loadingIcon, - icon, +/** + * 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 $ from 'jquery'; +import { __ } from '../../locale'; +import Flash from '../../flash'; +import axios from '../../lib/utils/axios_utils'; +import eventHub from '../event_hub'; +import Icon from '../../vue_shared/components/icon.vue'; +import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; +import JobComponent from './graph/job_component.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { PIPELINES_TABLE } from '../constants'; + +export default { + components: { + LoadingIcon, + Icon, + JobComponent, + }, + + directives: { + tooltip, + }, + + props: { + stage: { + type: Object, + required: true, }, - directives: { - tooltip, + updateDropdown: { + type: Boolean, + required: false, + default: false, }, - 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: { + dropdownClass() { + return this.dropdownContent.length > 0 + ? 'js-builds-dropdown-container' + : 'js-builds-dropdown-loading'; }, - data() { - return { - isLoading: false, - dropdownContent: '', - }; + triggerButtonClass() { + return `ci-status-icon-${this.stage.status.group}`; }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; - }, + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; + }, + }, - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; - }, + watch: { + updateDropdown() { + if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { + this.fetchJobs(); + } + }, + }, + + updated() { + if (this.dropdownContent.length > 0) { + this.stopDropdownClickPropagation(); + } + }, + + methods: { + onClickStage() { + if (!this.isDropdownOpen()) { + eventHub.$emit('clickedDropdown'); + this.isLoading = true; + this.fetchJobs(); + } + }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; - }, + fetchJobs() { + axios + .get(this.stage.dropdown_path) + .then(({ data }) => { + this.dropdownContent = data.latest_statuses; + this.isLoading = false; + }) + .catch(() => { + this.closeDropdown(); + this.isLoading = false; + + Flash(__('Something went wrong on our end.')); + }); }, - watch: { - updateDropdown() { - if (this.updateDropdown && - this.isDropdownOpen() && - !this.isLoading) { - this.fetchJobs(); - } - }, + /** + * 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. + */ + stopDropdownClickPropagation() { + $( + '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', + this.$el, + ).on('click', e => { + e.stopPropagation(); + }); }, - updated() { - if (this.dropdownContent.length > 0) { - this.stopDropdownClickPropagation(); + closeDropdown() { + if (this.isDropdownOpen()) { + $(this.$refs.dropdown).dropdown('toggle'); } }, - methods: { - onClickStage() { - if (!this.isDropdownOpen()) { - this.isLoading = true; - this.fetchJobs(); - } - }, - - fetchJobs() { - this.$http.get(this.stage.dropdown_path) - .then(response => response.json()) - .then((data) => { - this.dropdownContent = data.html; - this.isLoading = false; - }) - .catch(() => { - this.closeDropdown(); - this.isLoading = false; - - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); - }, - - /** - * 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. - */ - stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, - - closeDropdown() { - if (this.isDropdownOpen()) { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, - - isDropdownOpen() { - return this.$el.classList.contains('open'); - }, + isDropdownOpen() { + return this.$el.classList.contains('open'); + }, + + pipelineActionRequestComplete() { + if (this.type === PIPELINES_TABLE) { + // warn the table to update + eventHub.$emit('refreshPipelinesTable'); + } else { + // close the dropdown in mr widget + $(this.$refs.dropdown).dropdown('toggle'); + } }, - }; + }, +}; </script> <template> <div class="dropdown"> <button v-tooltip + id="stageDropdown" + ref="dropdown" :class="triggerButtonClass" - @click="onClickStage" - class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" :title="stage.title" + class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" data-placement="top" data-toggle="dropdown" + data-display="static" type="button" - id="stageDropdown" aria-haspopup="true" aria-expanded="false" + @click="onClickStage" > <span - aria-hidden="true" :aria-label="stage.title" + aria-hidden="true" > <icon :name="borderlessIcon" /> </span> @@ -167,7 +192,6 @@ > <li - :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" > @@ -175,8 +199,17 @@ <ul v-else - v-html="dropdownContent" > + <li + v-for="job in dropdownContent" + :key="job.id" + > + <job-component + :job="job" + css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> </ul> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index cd54d26c9d3..0a97df2dc18 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -66,8 +66,8 @@ </div> <div class="table-mobile-content"> <p - class="duration" v-if="hasDuration" + class="duration" > <span v-html="iconTimerSvg"> </span> @@ -75,8 +75,8 @@ </p> <p - class="finished-at hidden-xs hidden-sm" v-if="hasFinishedTime" + class="finished-at d-none d-sm-none d-md-block" > <i @@ -87,9 +87,9 @@ <time v-tooltip + :title="tooltipTitle(finishedTime)" data-placement="top" - data-container="body" - :title="tooltipTitle(finishedTime)"> + data-container="body"> {{ timeFormated(finishedTime) }} </time> </p> |