diff options
author | Fatih Acet <acetfatih@gmail.com> | 2017-02-06 21:07:41 +0000 |
---|---|---|
committer | Fatih Acet <acetfatih@gmail.com> | 2017-02-06 21:07:41 +0000 |
commit | 3a9228739019a7cee7cbcf4dd969e1b671119c93 (patch) | |
tree | 3b81ad12705f8719425bbc9b1a84f8bb4dbb63ca | |
parent | ee43dcd5d633f19b13a1d914a403686faec2bbca (diff) | |
parent | 183ff875f4e78e9cef4cc1d23630909527e334a2 (diff) | |
download | gitlab-ce-3a9228739019a7cee7cbcf4dd969e1b671119c93.tar.gz |
Merge branch 'fe-commit-mr-pipelines' into 'master'
Use vue.js Pipelines table in commit and merge request view
Closes #27141
See merge request !8844
44 files changed, 1087 insertions, 278 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index e3241974e59..c345fb6ce14 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -16,7 +16,7 @@ require('./components/board'); require('./components/board_sidebar'); require('./components/new_list_dropdown'); require('./components/modal/index'); -require('./vue_resource_interceptor'); +require('../vue_shared/vue_resource_interceptor'); $(() => { const $boardApp = document.getElementById('board-app'); diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 deleted file mode 100644 index 54c2b4ad369..00000000000 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ -/* global Vue */ - -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next(function (response) { - Vue.activeResources -= 1; - }); -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 new file mode 100644 index 00000000000..fbfec7743c7 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +require('./pipelines_table'); +/** + * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. + */ + +$(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); + } + + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 new file mode 100644 index 00000000000..483b414126a --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -0,0 +1,29 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Uses Vue.Resource + */ +class PipelinesService { + constructor(endpoint) { + this.pipelines = Vue.resource(endpoint); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 new file mode 100644 index 00000000000..f1b41911b73 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -0,0 +1,50 @@ +/* eslint-disable no-underscore-dangle*/ +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + */ + +class PipelinesStore { + constructor() { + this.state = {}; + this.state.pipelines = []; + } + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + + return pipelines; + } + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesStore = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 new file mode 100644 index 00000000000..ce0dbd4d56b --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -0,0 +1,107 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); +require('../../vue_shared/components/pipelines_table'); +require('../../vue_realtime_listener/index'); +require('./pipelines_service'); +require('./pipelines_store'); + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = new gl.commits.pipelines.PipelinesStore(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgsData); + + return { + endpoint: pipelinesTableData.endpoint, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); + + this.isLoading = true; + return pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.storePipelines(json); + this.store.startTimeAgoLoops.call(this, Vue); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); + }); + }, + + template: ` + <div> + <div class="pipelines realtime-loading" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder pipelines" + v-if="!isLoading && state.pipelines.length > 0"> + <pipelines-table-component + :pipelines="state.pipelines" + :svgs="svgs"> + </pipelines-table-component> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index edec21e3b63..70f467d608f 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -159,11 +159,6 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; - case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 521873b14b4..6a3d996f69c 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -4,7 +4,7 @@ window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('../../lib/utils/text_utility'); -require('../../vue_common_component/commit'); +require('../../vue_shared/components/commit'); require('./environment_actions'); require('./environment_external_url'); require('./environment_stop'); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 58f4c6eadb2..05c59d92fd4 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,8 +1,7 @@ window.Vue = require('vue'); - require('./stores/environments_store'); require('./components/environment'); -require('./vue_resource_interceptor'); +require('../vue_shared/vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 deleted file mode 100644 index 406bdbc1c7d..00000000000 --- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* global Vue */ -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); // eslint-disable-line - } - - Vue.activeResources--; // eslint-disable-line - }); -}); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index e3bff2559fd..0ee29a75c62 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -230,5 +230,16 @@ return upperCaseHeaders; }; + + /** + * Transforms a DOMStringMap into a plain object. + * + * @param {DOMStringMap} DOMStringMapObject + * @returns {Object} + */ + w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { + acc[element] = DOMStringMapObject[element]; + return acc; + }, {}); })(window); }).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 7e74bebb81e..107e85f1225 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -61,7 +61,6 @@ require('./flash'); constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; @@ -116,10 +115,6 @@ require('./flash'); $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else { this.expandView(); this.resetViewContainer(); @@ -244,25 +239,6 @@ require('./flash'); }); } - loadPipelines(source) { - if (this.pipelinesLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - this.scrollToElement('#pipelines'); - - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - }, - }); - } - // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index e1bebe0fe5b..e7432afb56e 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,41 +1,36 @@ +/* eslint-disable no-param-reassign */ /* global Vue, VueResource, gl */ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('../vue_common_component/commit'); -require('../vue_pagination/index'); -require('../boards/vue_resource_interceptor'); -require('./status'); -require('./store'); -require('./pipeline_url'); -require('./stage'); -require('./stages'); -require('./pipeline_actions'); -require('./time_ago'); +require('../lib/utils/common_utils'); +require('../vue_shared/vue_resource_interceptor'); require('./pipelines'); -(() => { - const project = document.querySelector('.pipelines'); - const entry = document.querySelector('.vue-pipelines-index'); - const svgs = document.querySelector('.pipeline-svgs'); +$(() => new Vue({ + el: document.querySelector('.vue-pipelines-index'), - if (!entry) return null; - return new Vue({ - el: entry, - data: { + data() { + const project = document.querySelector('.pipelines'); + const svgs = document.querySelector('.pipeline-svgs').dataset; + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgs); + + return { scope: project.dataset.url, store: new gl.PipelineStore(), - svgs: svgs.dataset, - }, - components: { - 'vue-pipelines': gl.VuePipelines, - }, - template: ` - <vue-pipelines - :scope='scope' - :store='store' - :svgs='svgs' - > - </vue-pipelines> - `, - }); -})(); + svgs: svgsObject, + }; + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, +})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 01f8b6519a4..8106934e864 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -50,9 +50,9 @@ <button v-if='artifacts' class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - data-toggle="dropdown" title="Artifacts" data-placement="top" + data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> @@ -81,8 +81,7 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.retry_path' - aria-label="Retry" - > + aria-label="Retry"> <i class="fa fa-repeat" aria-hidden="true"></i> </a> <a @@ -94,8 +93,7 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.cancel_path' - aria-label="Cancel" - > + aria-label="Cancel"> <i class="fa fa-remove" aria-hidden="true"></i> </a> </div> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 194bbae07d9..e47dc6935d6 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,19 +1,19 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +window.Vue = require('vue'); +require('../vue_shared/components/table_pagination'); +require('./store'); +require('../vue_shared/components/pipelines_table'); + ((gl) => { gl.VuePipelines = Vue.extend({ + components: { - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - stages: gl.VueStages, - commit: gl.CommitComponent, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - glPagination: gl.VueGlPagination, - statusScope: gl.VueStatusScope, - timeAgo: gl.VueTimeAgo, + 'gl-pagination': gl.VueGlPagination, + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, + data() { return { pipelines: [], @@ -38,87 +38,29 @@ change(pagenum, apiScope) { gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - ref(pipeline) { - const { ref } = pipeline; - return { name: ref.name, tag: ref.tag, ref_url: ref.path }; - }, - commitTitle(pipeline) { - return pipeline.commit ? pipeline.commit.title : ''; - }, - commitSha(pipeline) { - return pipeline.commit ? pipeline.commit.short_id : ''; - }, - commitUrl(pipeline) { - return pipeline.commit ? pipeline.commit.commit_path : ''; - }, - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, }, template: ` <div> - <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <div class="pipelines realtime-loading" v-if='pageRequest'> <i class="fa fa-spinner fa-spin"></i> </div> - <div class="table-holder" v-if='pipelines.length'> - <table class="table ci-table"> - <thead> - <tr> - <th class="pipeline-status">Status</th> - <th class="pipeline-info">Pipeline</th> - <th class="pipeline-commit">Commit</th> - <th class="pipeline-stages">Stages</th> - <th class="pipeline-date"></th> - <th class="pipeline-actions hidden-xs"></th> - </tr> - </thead> - <tbody> - <tr class="commit" v-for='pipeline in pipelines'> - <status-scope - :pipeline='pipeline' - :match='match' - :svgs='svgs' - > - </status-scope> - <pipeline-url :pipeline='pipeline'></pipeline-url> - <td> - <commit - :commit-icon-svg='svgs.commitIconSvg' - :author='author(pipeline)' - :tag="pipeline.ref.tag" - :title='commitTitle(pipeline)' - :commit-ref='ref(pipeline)' - :short-sha='commitSha(pipeline)' - :commit-url='commitUrl(pipeline)' - > - </commit> - </td> - <stages - :pipeline='pipeline' - :svgs='svgs' - :match='match' - > - </stages> - <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> - <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> - </tr> - </tbody> - </table> + + <div class="blank-state blank-state-no-icon" + v-if="!pageRequest && pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> </div> - <div class="pipelines realtime-loading" v-if='pageRequest'> - <i class="fa fa-spinner fa-spin"></i> + + <div class="table-holder" v-if='!pageRequest && pipelines.length'> + <pipelines-table-component + :pipelines='pipelines' + :svgs='svgs'> + </pipelines-table-component> </div> + <gl-pagination - v-if='pageInfo.total > pageInfo.perPage' + v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage' :pagenum='pagenum' :change='change' :count='count.all' diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 496df9aaced..8cc417a9966 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -15,7 +15,7 @@ required: true, }, svgs: { - type: DOMStringMap, + type: Object, required: true, }, match: { diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 deleted file mode 100644 index cb176b3f0c6..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStages = Vue.extend({ - components: { - 'vue-stage': gl.VueStage, - }, - props: ['pipeline', 'svgs', 'match'], - template: ` - <td class="stage-cell"> - <div - class="stage-container dropdown js-mini-pipeline-graph" - v-for='stage in pipeline.details.stages' - > - <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> - </div> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 0f5ce2a9274..0ee21f00fdc 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -20,6 +20,7 @@ require('../vue_realtime_listener'); gl.PipelineStore = class { fetchDataLoop(Vue, pageNum, url, apiScope) { + this.pageRequest = true; const updatePipelineNums = (count) => { const { all } = count; const running = count.running_or_pending; @@ -41,16 +42,18 @@ require('../vue_realtime_listener'); this.pageRequest = false; }, () => { this.pageRequest = false; - return new Flash('Something went wrong on our end.'); + return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); }); goFetch(); const startTimeLoops = () => { this.timeLoopInterval = setInterval(() => { - this.$children - .filter(e => e.$options._componentTag === 'time-ago') - .forEach(e => e.changeTime()); + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); }, 10000); }; diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 655110feba1..3598da11573 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -1,6 +1,9 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +window.Vue = require('vue'); +require('../lib/utils/datetime_utility'); + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index 4adad7bea31..7f7c18ddeb1 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,7 +1,5 @@ /* global Vue */ -window.Vue = require('vue'); - (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 new file mode 100644 index 00000000000..4bdaef31ee9 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -0,0 +1,61 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +require('./pipelines_table_row'); +/** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ + +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { + + props: { + pipelines: { + type: Array, + required: true, + default: () => ([]), + }, + + /** + * TODO: Remove this when we have webpack. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, + }, + + template: ` + <table class="table ci-table"> + <thead> + <tr> + <th class="js-pipeline-status pipeline-status">Status</th> + <th class="js-pipeline-info pipeline-info">Pipeline</th> + <th class="js-pipeline-commit pipeline-commit">Commit</th> + <th class="js-pipeline-stages pipeline-stages">Stages</th> + <th class="js-pipeline-date pipeline-date"></th> + <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <template v-for="model in pipelines" + v-bind:model="model"> + <tr is="pipelines-table-row-component" + :pipeline="model" + :svgs="svgs"></tr> + </template> + </tbody> + </table> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 new file mode 100644 index 00000000000..c819f0dd7cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -0,0 +1,234 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +require('../../vue_pipelines_index/status'); +require('../../vue_pipelines_index/pipeline_url'); +require('../../vue_pipelines_index/stage'); +require('../../vue_pipelines_index/pipeline_actions'); +require('../../vue_pipelines_index/time_ago'); +require('./commit'); +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', { + + props: { + pipeline: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * TODO: Remove this when we have webpack; + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + 'pipeline-actions': gl.VuePipelineActions, + 'dropdown-stage': gl.VueStage, + 'pipeline-url': gl.VuePipelineUrl, + 'status-scope': gl.VueStatusScope, + 'time-ago': gl.VueTimeAgo, + }, + + 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; + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline && + this.pipeline.commit && + 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 + if (this.pipeline && + this.pipeline.commit) { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + web_url: `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. + * + * Matched `url` prop sent in the API to `path` 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 === 'url') { + accumulator.path = 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; + }, + }, + + methods: { + /** + * FIXME: This should not be in this component but in the components that + * need this function. + * + * Used to render SVGs in the following components: + * - status-scope + * - dropdown-stage + * + * @param {String} string + * @return {String} + */ + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + template: ` + <tr class="commit"> + <status-scope + :pipeline="pipeline" + :svgs="svgs" + :match="match"> + </status-scope> + + <pipeline-url :pipeline="pipeline"></pipeline-url> + + <td> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="svgs.commitIconSvg"> + </commit-component> + </td> + + <td class="stage-cell"> + <div class="stage-container dropdown js-mini-pipeline-graph" + v-if="pipeline.details.stages.length > 0" + v-for="stage in pipeline.details.stages"> + <dropdown-stage + :stage="stage" + :svgs="svgs" + :match="match"> + </dropdown-stage> + </div> + </td> + + <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago> + + <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions> + </tr> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index 67c6cb73761..67c6cb73761 100644 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..d3229f9f730 --- /dev/null +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -0,0 +1,23 @@ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, +no-param-reassign, no-plusplus */ +/* global Vue */ + +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + + Vue.activeResources--; + }); +}); + +Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); +}); diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b5a7078a3a1..f880a9862c6 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6eb542e4bd8..38a1946a71e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -216,19 +216,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do - render json: { - html: view_to_html_string('projects/merge_requests/show/_pipelines'), - pipelines: PipelineSerializer - .new(project: @project, user: @current_user) - .with_pagination(request, response) - .represent(@pipelines) - } + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) end end end def new - define_new_vars + respond_to do |format| + format.html { define_new_vars } + format.json do + render json: { pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) } + end + end end def new_diffs diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 1164627fa11..aae2cb8a04b 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +1,25 @@ -%div - - if pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .table-holder.pipelines - %table.table.ci-table.js-pipeline-table - %thead - %th.pipeline-status Status - %th.pipeline-info Pipeline - %th.pipeline-commit Commit - %th.pipeline-stages Stages - %th.pipeline-date - %th.pipeline-actions - = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false +#commit-pipeline-table-view{ data: { endpoint: endpoint } } +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } + +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 89968cf4e0d..ac93eac41ac 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,4 +2,4 @@ = render 'commit_box' = render 'ci_menu' -= render 'pipelines_list', pipelines: @pipelines += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d3c013b3f21..38259faf62f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -46,7 +46,7 @@ -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines" + = render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params)) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b46c4a13cc4..83250443bea 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,8 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - -# This tab is always loaded via AJAX + - if @pipelines.any? + = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml index afe3f3430c6..cbe534abedb 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -1 +1 @@ -= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f776734556a..81e393d7626 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,31 +36,27 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - - if @pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), - } } + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } .vue-pipelines-index diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml new file mode 100644 index 00000000000..b5cc6bbf8b6 --- /dev/null +++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Use vue.js Pipelines table in commit and merge request view +merge_request: 8844 +author: diff --git a/config/webpack.config.js b/config/webpack.config.js index a156756f9ff..953ae463c86 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -19,6 +19,7 @@ var config = { boards: './boards/boards_bundle.js', boards_test: './boards/test_utils/simulate_drag.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', + commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e019541e74f..d18e8c37901 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -22,23 +22,35 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } + before { fork_project.team << [user, :master] } - before do - fork_project.team << [user, :master] + context 'when rendering HTML response' do + it 'renders new merge request widget template' do + submit_new_merge_request + + expect(response).to be_success + end end - it 'renders it' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + context 'when rendering JSON response' do + it 'renders JSON including serialized pipelines' do + submit_new_merge_request(format: :json) - expect(response).to be_success + expect(json_response).to have_key('pipelines') + expect(response).to be_ok + end end end + + def submit_new_merge_request(format: :html) + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' }, + format: format + end end shared_examples "loads labels" do |action| @@ -689,15 +701,8 @@ describe Projects::MergeRequestsController do format: :json end - it 'responds with a rendered HTML partial' do - expect(response) - .to render_template('projects/merge_requests/show/_pipelines') - expect(json_response).to have_key 'html' - end - it 'responds with serialized pipelines' do - expect(json_response).to have_key 'pipelines' - expect(json_response['pipelines']).not_to be_empty + expect(json_response).not_to be_empty end end end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 33f1c323af1..268d420c594 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit pipelines' do +feature 'project commit pipelines', js: true do given(:project) { create(:project) } background do diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6 new file mode 100644 index 00000000000..5f0f26a013c --- /dev/null +++ b/spec/javascripts/commit/pipelines/mock_data.js.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-vars */ +const pipeline = { + id: 73, + user: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + path: '/root/review-app/pipelines/73', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73', + }, + duration: null, + finished_at: '2017-01-25T00:00:17.130Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73#build', + }, + path: '/root/review-app/pipelines/73#build', + dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build', + }], + artifacts: [], + manual_actions: [ + { + name: 'stop_review', + path: '/root/review-app/builds/1463/play', + }, + { + name: 'name', + path: '/root/review-app/builds/1490/play', + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: + { + name: 'master', + path: '/root/review-app/tree/master', + tag: false, + branch: true, + }, + commit: { + id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4', + short_id: 'fbd79f04', + title: 'Update .gitlab-ci.yml', + author_name: 'Administrator', + author_email: 'admin@example.com', + created_at: '2017-01-16T12:13:57.000-05:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + message: 'Update .gitlab-ci.yml', + author: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + }, + retry_path: '/root/review-app/pipelines/73/retry', + created_at: '2017-01-16T17:13:59.800Z', + updated_at: '2017-01-25T00:00:17.132Z', +}; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 new file mode 100644 index 00000000000..c2b61632827 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -0,0 +1,106 @@ +/* global pipeline, Vue */ + +require('vue-resource'); +require('flash'); +require('~/commit/pipelines/pipelines_store'); +require('~/commit/pipelines/pipelines_service'); +require('~/commit/pipelines/pipelines_table'); +require('~vue_shared/vue_resource_interceptor'); +require('./mock_data'); + +describe('Pipelines table in Commits and Merge requests', () => { + preloadFixtures('pipelines_table'); + + beforeEach(() => { + loadFixtures('pipelines_table'); + }); + + describe('successfull request', () => { + describe('without pipelines', () => { + const pipelinesEmptyResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesEmptyResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesEmptyResponse, + ); + }); + + it('should render the empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 1); + }); + }); + + describe('with pipelines', () => { + const pipelinesResponse = (request, next) => { + next(request.respondWith(JSON.stringify([pipeline]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesResponse, + ); + }); + + it('should render a table with the received pipelines', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const pipelinesErrorResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesErrorResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesErrorResponse, + ); + }); + + it('should render empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 new file mode 100644 index 00000000000..a5a16544ffb --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -0,0 +1,30 @@ +require('~commit/pipelines/pipelines_store'); + +describe('Store', () => { + const store = gl.commits.pipelines.PipelinesStore; + + beforeEach(() => { + store.create(); + }); + + it('should start with a blank state', () => { + expect(store.state.pipelines.length).toBe(0); + }); + + it('should store an array of pipelines', () => { + const pipelines = [ + { + id: '1', + name: 'pipeline', + }, + { + id: '2', + name: 'pipeline_2', + }, + ]; + + store.store(pipelines); + + expect(store.state.pipelines.length).toBe(pipelines.length); + }); +}); diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml new file mode 100644 index 00000000000..fbe4a434f76 --- /dev/null +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -0,0 +1,2 @@ +#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } +.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6 index bbd914de4ea..84d169a3fb9 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/vue_common_component/commit'); +require('~/vue_shared/components/committ'); describe('Commit component', () => { let props; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 new file mode 100644 index 00000000000..e83a1749e82 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -0,0 +1,89 @@ +/* global pipeline */ + +require('~vue_shared/components/pipelines_table_row'); +require('./mock_data'); + +describe('Pipelines Table Row', () => { + let component; + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + + component = new gl.pipelines.PipelinesTableRowComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipeline, + svgs: {}, + }, + }); + }); + + it('should render a table row', () => { + expect(component.$el).toEqual('TR'); + }); + + describe('status column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td.commit-link a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render status text', () => { + expect( + component.$el.querySelector('td.commit-link a').textContent, + ).toContain(pipeline.details.status.text); + }); + }); + + describe('information column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render pipeline ID', () => { + expect( + component.$el.querySelector('td:nth-child(2) a > span').textContent, + ).toEqual(`#${pipeline.id}`); + }); + + describe('when a user is provided', () => { + it('should render user information', () => { + expect( + component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + ).toEqual(pipeline.user.web_url); + + expect( + component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + ).toEqual(pipeline.user.name); + }); + }); + }); + + describe('commit column', () => { + it('should render link to commit', () => { + expect( + component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), + ).toEqual(pipeline.commit.commit_path); + }); + }); + + describe('stages column', () => { + it('should render an icon for each stage', () => { + expect( + component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + ).toEqual(pipeline.details.stages.length); + }); + }); + + describe('actions column', () => { + it('should render the provided actions', () => { + expect( + component.$el.querySelectorAll('td:nth-child(6) ul li').length, + ).toEqual(pipeline.details.manual_actions.length); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 new file mode 100644 index 00000000000..adc9ea904cc --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -0,0 +1,66 @@ +/* global pipeline */ + +require('~vue_shared/components/pipelines_table'); +require('~lib/utils/datetime_utility'); +require('./mock_data'); + +describe('Pipelines Table', () => { + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + describe('table', () => { + let component; + beforeEach(() => { + component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + }); + + it('should render a table', () => { + expect(component.$el).toEqual('TABLE'); + }); + + it('should render table head with correct columns', () => { + expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); + expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); + expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); + expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); + expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); + expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + }); + }); + + describe('without data', () => { + it('should render an empty table', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + }); + }); + + describe('with data', () => { + it('should render rows', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [pipeline], + svgs: {}, + }, + }); + + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index 8935c474ee5..e84f0dcfe67 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -1,5 +1,5 @@ require('~/lib/utils/common_utils'); -require('~/vue_pagination/index'); +require('~/vue_shared/components/table_pagination'); describe('Pagination component', () => { let component; |