diff options
Diffstat (limited to 'app/assets/javascripts/pipelines')
6 files changed, 478 insertions, 125 deletions
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 37a6f02d8fd..abcd0c4ecea 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; + import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltipMixin from '../../vue_shared/mixins/tooltip'; export default { props: { @@ -11,53 +11,42 @@ export default { type: String, required: true, }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - cssClass: { type: String, required: true, }, - confirmActionMessage: { type: String, required: false, }, }, - components: { loadingIcon, }, - + mixins: [ + tooltipMixin, + ], data() { return { isLoading: false, }; }, - computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn ${this.cssClass}`; }, }, - methods: { onClick() { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { @@ -66,21 +55,11 @@ export default { this.makeRequest(); } }, - makeRequest() { this.isLoading = true; - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); + $(this.$refs.tooltip).tooltip('destroy'); + eventHub.$emit('postAction', this.endpoint); }, }, }; @@ -95,10 +74,12 @@ export default { :aria-label="title" data-container="body" data-placement="top" + ref="tooltip" :disabled="isLoading"> <i :class="iconClass" - aria-hidden="true" /> + aria-hidden="true"> + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index fed42d23112..01ae07aad65 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,15 +1,9 @@ <script> - import Visibility from 'visibilityjs'; import PipelinesService from '../services/pipelines_service'; - import eventHub from '../event_hub'; - import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; + import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import emptyState from './empty_state.vue'; - import errorState from './error_state.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Poll from '../../lib/utils/poll'; export default { props: { @@ -20,13 +14,12 @@ }, components: { tablePagination, - pipelinesTableComponent, - emptyState, - errorState, navigationTabs, navigationControls, - loadingIcon, }, + mixins: [ + pipelinesMixin, + ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -47,11 +40,6 @@ state: this.store.state, apiScope: 'all', pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, }; }, computed: { @@ -62,9 +50,6 @@ const scope = gl.utils.getParameterByName('scope'); return scope === null ? 'all' : scope; }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,7 +91,6 @@ this.state.pipelines.length && this.state.pageInfo.total > this.state.pageInfo.perPage; }, - hasCiEnabled() { return this.hasCi !== undefined; }, @@ -129,37 +113,7 @@ }, created() { this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - beforeDestroy() { - eventHub.$off('refreshPipelines'); + this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; }, methods: { /** @@ -174,15 +128,6 @@ return param; }, - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, successCallback(resp) { const response = { headers: resp.headers, @@ -190,33 +135,14 @@ }; this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } + this.setCommonData(response.body.pipelines); }, }, }; </script> <template> <div :class="cssClass"> - <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -274,7 +200,6 @@ <pipelines-table-component :pipelines="state.pipelines" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 97b4de26214..a6fc4f04237 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -11,10 +11,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, }, components: { loadingIcon, @@ -31,17 +27,9 @@ $(this.$refs.tooltip).tooltip('destroy'); - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - // eslint-disable-next-line no-new - new Flash('An error occured while making the request.'); - }); + eventHub.$emit('postAction', endpoint); }, + isActionDisabled(action) { if (action.playable === undefined) { return false; diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue new file mode 100644 index 00000000000..5088d92209f --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -0,0 +1,59 @@ +<script> + import pipelinesTableRowComponent from './pipelines_table_row.vue'; + + /** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ + export default { + props: { + pipelines: { + type: Array, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + }, + components: { + pipelinesTableRowComponent, + }, + }; +</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 pipeline-status" + role="rowheader"> + Status + </div> + <div + class="table-section section-15 js-pipeline-info pipeline-info" + role="rowheader"> + Pipeline + </div> + <div + class="table-section section-25 js-pipeline-commit pipeline-commit" + role="rowheader"> + Commit + </div> + <div + class="table-section section-15 js-pipeline-stages pipeline-stages" + role="rowheader"> + Stages + </div> + </div> + <pipelines-table-row-component + v-for="model in pipelines" + :key="model.id" + :pipeline="model" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue new file mode 100644 index 00000000000..c3f1c426d8a --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -0,0 +1,297 @@ +<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'; + +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ +export default { + props: { + pipeline: { + type: Object, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + }, + components: { + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, + ciBadge, + pipelinesTimeago, + }, + computed: { + /** + * 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 he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she 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 + // he/she 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 he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + // 4. If committer is not a GitLab User he/she 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; + }, + + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matches `path` prop sent in the API to `ref_url` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'path') { + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } + + return 0; + }, + + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } + + return ''; + }, + + pipelineStatus() { + if (this.pipeline.details && this.pipeline.details.status) { + return this.pipeline.details.status; + } + return {}; + }, + + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, + }, +}; +</script> +<template> + <div class="commit gl-responsive-table-row"> + <div class="table-section section-10 commit-link"> + <div class="table-mobile-header" + role="rowheader"> + Status + </div> + <div class="table-mobile-content"> + <ci-badge :status="pipelineStatus"/> + </div> + </div> + + <pipeline-url :pipeline="pipeline" /> + + <div class="table-section section-25"> + <div + class="table-mobile-header" + role="rowheader"> + Commit + </div> + <div class="table-mobile-content"> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor"/> + </div> + </div> + + <div class="table-section section-wrap section-15 stage-cell"> + <div + class="table-mobile-header" + role="rowheader"> + Stages + </div> + <div class="table-mobile-content"> + <div class="stage-container dropdown js-mini-pipeline-graph" + v-if="pipeline.details.stages.length > 0" + v-for="stage in pipeline.details.stages"> + <pipeline-stage + :stage="stage" + :update-dropdown="updateGraphDropdown" + /> + </div> + </div> + </div> + + <pipelines-timeago + :duration="pipelineDuration" + :finished-time="pipelineFinishedAt" + /> + + <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="pipeline.details.manual_actions.length" + :actions="pipeline.details.manual_actions" + /> + + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + class="hidden-xs hidden-sm" + :artifacts="pipeline.details.artifacts" + /> + + <async-button-component + v-if="pipeline.flags.retryable" + :endpoint="pipeline.retry_path" + css-class="js-pipelines-retry-button btn-default btn-retry" + title="Retry" + icon="repeat" + /> + + <async-button-component + v-if="pipeline.flags.cancelable" + :endpoint="pipeline.cancel_path" + css-class="js-pipelines-cancel-button btn-remove" + title="Cancel" + icon="remove" + confirm-action-message="Are you sure you want to cancel this pipeline?" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js new file mode 100644 index 00000000000..9adc15e6266 --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -0,0 +1,103 @@ +/* global Flash */ +import '~/flash'; +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; +import emptyState from '../components/empty_state.vue'; +import errorState from '../components/error_state.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import pipelinesTableComponent from '../components/pipelines_table.vue'; +import eventHub from '../event_hub'; + +export default { + components: { + pipelinesTableComponent, + errorState, + emptyState, + loadingIcon, + }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + }, + data() { + return { + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + beforeMount() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: this.requestData ? this.requestData : undefined, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + eventHub.$on('postAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + eventHub.$on('postAction', this.postAction); + }, + destroyed() { + this.poll.stop(); + }, + methods: { + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + setCommonData(pipelines) { + this.store.storePipelines(pipelines); + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + postAction(endpoint) { + this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, +}; |