summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFatih Acet <acetfatih@gmail.com>2017-02-06 21:07:41 +0000
committerFatih Acet <acetfatih@gmail.com>2017-02-06 21:07:41 +0000
commit3a9228739019a7cee7cbcf4dd969e1b671119c93 (patch)
tree3b81ad12705f8719425bbc9b1a84f8bb4dbb63ca
parentee43dcd5d633f19b13a1d914a403686faec2bbca (diff)
parent183ff875f4e78e9cef4cc1d23630909527e334a2 (diff)
downloadgitlab-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
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es62
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es626
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js.es629
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es650
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es6107
-rw-r--r--app/assets/javascripts/dispatcher.js.es65
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es62
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es63
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es611
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es624
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es661
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es68
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6106
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es62
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es611
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es63
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es6 (renamed from app/assets/javascripts/vue_common_component/commit.js.es6)2
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es661
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6234
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es6 (renamed from app/assets/javascripts/vue_pagination/index.js.es6)0
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es623
-rw-r--r--app/controllers/projects/commit_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb19
-rw-r--r--app/views/projects/commit/_pipelines_list.haml40
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml46
-rw-r--r--changelogs/unreleased/fe-commit-mr-pipelines.yml4
-rw-r--r--config/webpack.config.js1
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb43
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js.es690
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js.es6106
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es630
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml2
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js.es6 (renamed from spec/javascripts/vue_common_components/commit_spec.js.es6)2
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es689
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js.es666
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 (renamed from spec/javascripts/vue_pagination/pagination_spec.js.es6)2
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;