From 184f60a06f828ccbc9264d40e6daa48d60dca629 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Sun, 29 Jan 2017 15:30:04 +0000 Subject: Moves pagination to shared folder Document and remove unused code Declare components in a consistent way; Use " instead of ' to improve consistency; Update documentation; Fix commit author verification to match the use cases; Adds tests for the added components Fix paths in pagination spec Adds tests to pipelines table used in merge requests and commits Use same resource interceptor Fix eslint error --- .../commit/pipelines/pipelines_bundle.js.es6 | 100 +----------- .../commit/pipelines/pipelines_service.js.es6 | 5 + .../commit/pipelines/pipelines_table.js.es6 | 104 +++++++++++++ .../environments/environments_bundle.js.es6 | 2 +- .../environments/vue_resource_interceptor.js.es6 | 12 -- app/assets/javascripts/vue_pagination/index.js.es6 | 148 ------------------ .../vue_pipelines_index/pipelines.js.es6 | 4 +- .../vue_pipelines_index/time_ago.js.es6 | 2 + .../vue_shared/components/pipelines_table.js.es6 | 24 ++- .../components/pipelines_table_row.js.es6 | 102 ++++++++----- .../vue_shared/components/table_pagination.js.es6 | 148 ++++++++++++++++++ .../vue_shared/vue_resource_interceptor.js.es6 | 11 +- app/controllers/projects/commit_controller.rb | 1 - .../projects/merge_requests_controller.rb | 1 - .../projects/merge_requests/_new_submit.html.haml | 6 +- app/views/projects/merge_requests/_show.html.haml | 1 - spec/javascripts/commit/pipelines/mock_data.js.es6 | 90 +++++++++++ .../commit/pipelines/pipelines_spec.js.es6 | 107 +++++++++++++ .../commit/pipelines/pipelines_store_spec.js.es6 | 31 ++++ .../javascripts/fixtures/pipelines_table.html.haml | 2 + .../vue_pagination/pagination_spec.js.es6 | 167 -------------------- .../components/pipelines_table_row_spec.js.es6 | 90 +++++++++++ .../components/pipelines_table_spec.js.es6 | 67 ++++++++ .../components/table_pagination_spec.js.es6 | 168 +++++++++++++++++++++ 24 files changed, 905 insertions(+), 488 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 delete mode 100644 app/assets/javascripts/environments/vue_resource_interceptor.js.es6 delete mode 100644 app/assets/javascripts/vue_pagination/index.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/table_pagination.js.es6 create mode 100644 spec/javascripts/commit/pipelines/mock_data.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_spec.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 create mode 100644 spec/javascripts/fixtures/pipelines_table.html.haml delete mode 100644 spec/javascripts/vue_pagination/pagination_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index a06aad17824..b21d13842a4 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -3,10 +3,6 @@ //= require vue //= require_tree . -//= require vue -//= require vue-resource -//= require vue_shared/vue_resource_interceptor -//= require vue_shared/components/pipelines_table /** * Commits View > Pipelines Tab > Pipelines Table. @@ -14,13 +10,6 @@ * * Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the merge request show view. - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as scope. - * We need a store to make the request and store the received environemnts. - * - * 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. */ $(() => { @@ -28,94 +17,11 @@ $(() => { gl.commits = gl.commits || {}; gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.commits.PipelinesTableView) { - gl.commits.PipelinesTableView.$destroy(true); + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); } - gl.commits.pipelines.PipelinesTableView = new Vue({ - + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ el: document.querySelector('#commit-pipeline-table-view'), - - 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} Props for `pipelines-table-component` - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = gl.commits.pipelines.PipelinesStore.create(); - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = Object.keys(svgsData).reduce((acc, element) => { - acc[element] = svgsData[element]; - return acc; - }, {}); - - return { - endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, - store, - state: store.state, - isLoading: false, - error: 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() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); - - this.isLoading = true; - - return gl.pipelines.pipelinesService.all() - .then(response => response.json()) - .then((json) => { - this.store.store(json); - this.isLoading = false; - this.error = false; - }).catch(() => { - this.error = true; - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); - }); - }, - - template: ` -
-
- -
- -
-

- You don't have any pipelines. -

-
- -
- - -
-
- `, }); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 1e6aa73d9cf..f4ed986b0c5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -32,3 +32,8 @@ class PipelinesService { 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_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 new file mode 100644 index 00000000000..df7a6455eed --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -0,0 +1,104 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +//= require vue +//= require vue-resource +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * + * 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 = gl.commits.pipelines.PipelinesStore.create(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgsData).reduce((acc, element) => { + acc[element] = svgsData[element]; + return acc; + }, {}); + + 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() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + return gl.pipelines.pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.store(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); + }, + + template: ` +
+
+ +
+ +
+

+ No pipelines to show +

+
+ +
+ + +
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 3b003f6f661..cd205617a97 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,7 +1,7 @@ //= require vue //= require_tree ./stores/ //= 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/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 deleted file mode 100644 index 605824fa939..00000000000 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ /dev/null @@ -1,148 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign, no-plusplus */ - -((gl) => { - const PAGINATION_UI_BUTTON_LIMIT = 4; - const UI_LIMIT = 6; - const SPREAD = '...'; - const PREV = 'Prev'; - const NEXT = 'Next'; - const FIRST = '<< First'; - const LAST = 'Last >>'; - - gl.VueGlPagination = Vue.extend({ - props: { - - /** - This function will take the information given by the pagination component - And make a new Turbolinks call - - Here is an example `change` method: - - change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); - }, - */ - - change: { - type: Function, - required: true, - }, - - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - let apiScope = gl.utils.getParameterByName('scope'); - - if (!apiScope) apiScope = 'all'; - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages, apiScope); - break; - case NEXT: - this.change(nextPage, apiScope); - break; - case PREV: - this.change(previousPage, apiScope); - break; - case FIRST: - this.change(1, apiScope); - break; - default: - this.change(+text, apiScope); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; - }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) items.push({ title: FIRST }); - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i++) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) items.push({ title: LAST, last: true }); - - return items; - }, - }, - template: ` -
- -
- `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 34d93ce1b7f..c1daf816060 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,7 +1,7 @@ /* global Vue, Turbolinks, gl */ /* eslint-disable no-param-reassign */ -//= require vue_pagination/index +//= require vue_shared/components/table_pagination //= require ./store.js.es6 //= require vue_shared/components/pipelines_table @@ -9,7 +9,7 @@ gl.VuePipelines = Vue.extend({ components: { - glPagination: gl.VueGlPagination, + 'gl-pagination': gl.VueGlPagination, 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, 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..61417b28630 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,8 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +//= require lib/utils/datetime_utility + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4b6bba461d7..9bc1ea65e53 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -4,10 +4,9 @@ //= require ./pipelines_table_row /** - * Pipelines Table Component - * - * Given an array of pipelines, renders a table. + * Pipelines Table Component. * + * Given an array of objects, renders a table. */ (() => { @@ -20,11 +19,11 @@ pipelines: { type: Array, required: true, - default: [], + default: () => ([]), }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack. */ svgs: { type: Object, @@ -41,19 +40,18 @@ - - - - - - + + + + + + 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 index c0ff0c90e4e..375516e3804 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -7,6 +7,12 @@ //= require vue_shared/components/commit //= require vue_pipelines_index/pipeline_actions //= require vue_pipelines_index/time_ago + +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ (() => { window.gl = window.gl || {}; gl.pipelines = gl.pipelines || {}; @@ -21,7 +27,7 @@ }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack; */ svgs: { type: Object, @@ -32,12 +38,10 @@ components: { 'commit-component': gl.CommitComponent, - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - 'vue-stage': gl.VueStage, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - statusScope: gl.VueStatusScope, + 'pipeline-actions': gl.VuePipelineActions, + 'dropdown-stage': gl.VueStage, + 'pipeline-url': gl.VuePipelineUrl, + 'status-scope': gl.VueStatusScope, 'time-ago': gl.VueTimeAgo, }, @@ -46,48 +50,48 @@ * If provided, returns the commit tag. * Needed to render the commit component column. * - * TODO: Document this logic, need to ask @grzesiek and @selfup + * 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() { - if (!this.pipeline.commit) { - return { avatar_url: '', web_url: '', username: '' }; - } + 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) { - return 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 && - this.pipeline.commit.author_gravatar_url && - this.pipeline.commit.author_name && - this.pipeline.commit.author_email) { - return { + 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 undefined; - }, - - /** - * Figure this out! - * Needed to render the commit component column. - */ - 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, - }; + return commitAuthorInformation; }, /** @@ -108,6 +112,9 @@ * 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() { @@ -169,6 +176,17 @@ }, 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()); }, @@ -177,12 +195,12 @@ template: ` - + - + - + `, }); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 new file mode 100644 index 00000000000..605824fa939 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -0,0 +1,148 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign, no-plusplus */ + +((gl) => { + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = 'Prev'; + const NEXT = 'Next'; + const FIRST = '<< First'; + const LAST = 'Last >>'; + + gl.VueGlPagination = Vue.extend({ + props: { + + /** + This function will take the information given by the pagination component + And make a new Turbolinks call + + Here is an example `change` method: + + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + */ + + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + let apiScope = gl.utils.getParameterByName('scope'); + + if (!apiScope) apiScope = 'all'; + + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages, apiScope); + break; + case NEXT: + this.change(nextPage, apiScope); + break; + case PREV: + this.change(previousPage, apiScope); + break; + case FIRST: + this.change(1, apiScope); + break; + default: + this.change(+text, apiScope); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i++) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` +
+ +
+ `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 index 54c2b4ad369..d627fa2b88a 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -1,10 +1,15 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ +/* 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(function (response) { - Vue.activeResources -= 1; + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + + Vue.activeResources--; }); }); 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 deb084c2e91..68f6208c2be 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,7 +218,6 @@ class Projects::MergeRequestsController < 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/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index c1f48837e0e..e00ae629e4b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -44,9 +44,9 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! - = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) + - if @pipelines.any? + #pipelines.pipelines.tab-pane + = 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 8dfe967a937..f131836058b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,6 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! = 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/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..3bcc0d1eb18 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -0,0 +1,107 @@ +/* global pipeline, Vue */ + +//= require 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..46a7df3bb21 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -0,0 +1,31 @@ +//= require vue +//= 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_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 deleted file mode 100644 index efb11211ce2..00000000000 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ /dev/null @@ -1,167 +0,0 @@ -//= require vue -//= require lib/utils/common_utils -//= require vue_pagination/index - -describe('Pagination component', () => { - let component; - - const changeChanges = { - one: '', - two: '', - }; - - const change = (one, two) => { - changeChanges.one = one; - changeChanges.two = two; - }; - - it('should render and start at page 1', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 2, - previousPage: '', - }, - change, - }, - }); - - expect(component.$el.classList).toContain('gl-pagination'); - - component.changePage({ target: { innerText: '1' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the previous page', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 3, - previousPage: 1, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Prev' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the next page', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Next' } }); - - expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the last page', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Last >>' } }); - - expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the first page', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: '<< First' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should do nothing', () => { - setFixtures('
'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 2, - previousPage: '', - }, - change, - }, - }); - - component.changePage({ target: { innerText: '...' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); -}); - -describe('paramHelper', () => { - it('can parse url parameters correctly', () => { - window.history.pushState({}, null, '?scope=all&p=2'); - - const scope = gl.utils.getParameterByName('scope'); - const p = gl.utils.getParameterByName('p'); - - expect(scope).toEqual('all'); - expect(p).toEqual('2'); - }); - - it('returns null if param not in url', () => { - window.history.pushState({}, null, '?p=2'); - - const scope = gl.utils.getParameterByName('scope'); - const p = gl.utils.getParameterByName('p'); - - expect(scope).toEqual(null); - expect(p).toEqual('2'); - }); -}); 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..6825de069e4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -0,0 +1,90 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table_row +//= require commit/pipelines/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..cb1006d44dc --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -0,0 +1,67 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table +//= require commit/pipelines/mock_data +//= require lib/utils/datetime_utility + +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_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 new file mode 100644 index 00000000000..6a0fec43d2e --- /dev/null +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -0,0 +1,168 @@ +//= require vue +//= require lib/utils/common_utils +//= require vue_shared/components/table_pagination +/* global fixture, gl */ + +describe('Pagination component', () => { + let component; + + const changeChanges = { + one: '', + two: '', + }; + + const change = (one, two) => { + changeChanges.one = one; + changeChanges.two = two; + }; + + it('should render and start at page 1', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + expect(component.$el.classList).toContain('gl-pagination'); + + component.changePage({ target: { innerText: '1' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the previous page', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 3, + previousPage: 1, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Prev' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the next page', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Next' } }); + + expect(changeChanges.one).toEqual(5); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the last page', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Last >>' } }); + + expect(changeChanges.one).toEqual(10); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the first page', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: '<< First' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should do nothing', () => { + setFixtures('
'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + component.changePage({ target: { innerText: '...' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); +}); + +describe('paramHelper', () => { + it('can parse url parameters correctly', () => { + window.history.pushState({}, null, '?scope=all&p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual('all'); + expect(p).toEqual('2'); + }); + + it('returns null if param not in url', () => { + window.history.pushState({}, null, '?p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual(null); + expect(p).toEqual('2'); + }); +}); -- cgit v1.2.1
StatusPipelineCommitStagesStatusPipelineCommitStages
-