summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfredo Sumaran <alfredo@gitlab.com>2017-03-24 01:12:44 +0000
committerAlfredo Sumaran <alfredo@gitlab.com>2017-03-24 01:12:44 +0000
commit453d755ae4c83cbdd0a6542aca7028f4e1521679 (patch)
treefa202ad1115e38a52c8797925d86befb4973c09c
parent856edee368c8c6a93821bff97eb0bb08b3e15bf9 (diff)
parenta5f17beb43c792618cd0e8612d8e00bb8edb6942 (diff)
downloadgitlab-ce-453d755ae4c83cbdd0a6542aca7028f4e1521679.tar.gz
Merge branch '27574-pipelines-empty-state' into 'master'
Pipelines empty state Closes #27574 See merge request !9978
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/empty_state.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/error_state.js19
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js68
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js8
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js191
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss5
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/pipelines/index.html.haml60
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_empty.svg1
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_failed.svg1
-rw-r--r--changelogs/unreleased/27574-pipelines-empty-state.yml4
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml14
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml3
-rw-r--r--spec/javascripts/vue_pipelines_index/empty_state_spec.js38
-rw-r--r--spec/javascripts/vue_pipelines_index/error_state_spec.js23
-rw-r--r--spec/javascripts/vue_pipelines_index/mock_data.js107
-rw-r--r--spec/javascripts/vue_pipelines_index/nav_controls_spec.js93
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_spec.js114
22 files changed, 772 insertions, 103 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 360fb39dc9c..a20e5bc3b1b 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,10 +1,10 @@
-/* eslint-disable no-new*/
-/* global Flash */
import Vue from 'vue';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub';
+import EmptyState from '../../vue_pipelines_index/components/empty_state';
+import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
@@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('pipelines-table', {
components: {
'pipelines-table-component': PipelinesTableComponent,
+ 'error-state': ErrorState,
+ 'empty-state': EmptyState,
},
/**
@@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', {
return {
endpoint: pipelinesTableData.endpoint,
+ helpPagePath: pipelinesTableData.helpPagePath,
store,
state: store.state,
isLoading: false,
+ hasError: false,
};
},
+ computed: {
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
+
+ shouldRenderEmptyState() {
+ return !this.state.pipelines.length && !this.isLoading;
+ },
+ },
+
/**
* When the component is about to be mounted, tell the service to fetch the data
*
@@ -80,8 +94,8 @@ export default Vue.component('pipelines-table', {
this.isLoading = false;
})
.catch(() => {
+ this.hasError = true;
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
},
@@ -92,12 +106,11 @@ export default Vue.component('pipelines-table', {
<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>
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
<div class="table-holder"
v-if="!isLoading && state.pipelines.length > 0">
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
new file mode 100644
index 00000000000..56b4858f4b4
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
@@ -0,0 +1,33 @@
+import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
+
+export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesEmptyStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>Build with confidence</h4>
+ <p>
+ Continous Integration can help catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver code to your product environment.
+ </p>
+ <a :href="helpPagePath" class="btn btn-info">
+ Get started with Pipelines
+ </a>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
new file mode 100644
index 00000000000..e5d228bddf8
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
@@ -0,0 +1,19 @@
+import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
+
+export default {
+ template: `
+ <div class="row empty-state js-pipelines-error-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesErrorStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>The API failed to fetch the pipelines.</h4>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
new file mode 100644
index 00000000000..6aa10531034
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
@@ -0,0 +1,52 @@
+export default {
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: true,
+ },
+
+ hasCiEnabled: {
+ type: Boolean,
+ required: true,
+ },
+
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: true,
+ },
+
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="nav-controls">
+ <a
+ v-if="canCreatePipeline"
+ :href="newPipelinePath"
+ class="btn btn-create">
+ Run Pipeline
+ </a>
+
+ <a
+ v-if="!hasCiEnabled"
+ :href="helpPagePath"
+ class="btn btn-info">
+ Get started with Pipelines
+ </a>
+
+ <a
+ :href="ciLintPath"
+ class="btn btn-default">
+ CI Lint
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
new file mode 100644
index 00000000000..b4480bd98c7
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
@@ -0,0 +1,68 @@
+export default {
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+
+ count: {
+ type: Object,
+ required: true,
+ },
+
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ template: `
+ <ul class="nav-links">
+ <li
+ class="js-pipelines-tab-all"
+ :class="{ 'active': scope === 'all'}">
+ <a :href="paths.allPath">
+ All
+ <span class="badge js-totalbuilds-count">
+ {{count.all}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-pending"
+ :class="{ 'active': scope === 'pending'}">
+ <a :href="paths.pendingPath">
+ Pending
+ <span class="badge">
+ {{count.pending}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-running"
+ :class="{ 'active': scope === 'running'}">
+ <a :href="paths.runningPath">
+ Running
+ <span class="badge">
+ {{count.running}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-finished"
+ :class="{ 'active': scope === 'finished'}">
+ <a :href="paths.finishedPath">
+ Finished
+ <span class="badge">
+ {{count.finished}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-branches"
+ :class="{ 'active': scope === 'branches'}">
+ <a :href="paths.branchesPath">Branches</a>
+ </li>
+ <li class="js-pipelines-tab-tags"
+ :class="{ 'active': scope === 'tags'}">
+ <a :href="paths.tagsPath">Tags</a>
+ </li>
+ </ul>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index 104154a715b..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
- el: document.querySelector('.vue-pipelines-index'),
+ el: document.querySelector('#pipelines-list-vue'),
data() {
- const project = document.querySelector('.pipelines');
const store = new PipelinesStore();
return {
store,
- endpoint: project.dataset.url,
};
},
components: {
'vue-pipelines': PipelinesComponent,
},
template: `
- <vue-pipelines
- :endpoint="endpoint"
- :store="store" />
+ <vue-pipelines :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index f389e5e4950..48f0e9036e8 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,19 +1,15 @@
-/* global Flash */
-/* eslint-disable no-new */
-import '~/flash';
import Vue from 'vue';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
+import EmptyState from './components/empty_state';
+import ErrorState from './components/error_state';
+import NavigationTabs from './components/navigation_tabs';
+import NavigationControls from './components/nav_controls';
export default {
props: {
- endpoint: {
- type: String,
- required: true,
- },
-
store: {
type: Object,
required: true,
@@ -23,17 +19,109 @@ export default {
components: {
'gl-pagination': TablePaginationComponent,
'pipelines-table-component': PipelinesTableComponent,
+ 'empty-state': EmptyState,
+ 'error-state': ErrorState,
+ 'navigation-tabs': NavigationTabs,
+ 'navigation-controls': NavigationControls,
},
data() {
+ const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
+
return {
+ endpoint: pipelinesData.endpoint,
+ cssClass: pipelinesData.cssClass,
+ helpPagePath: pipelinesData.helpPagePath,
+ newPipelinePath: pipelinesData.newPipelinePath,
+ canCreatePipeline: pipelinesData.canCreatePipeline,
+ allPath: pipelinesData.allPath,
+ pendingPath: pipelinesData.pendingPath,
+ runningPath: pipelinesData.runningPath,
+ finishedPath: pipelinesData.finishedPath,
+ branchesPath: pipelinesData.branchesPath,
+ tagsPath: pipelinesData.tagsPath,
+ hasCi: pipelinesData.hasCi,
+ ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
- pageRequest: false,
+ isLoading: false,
+ hasError: false,
};
},
+ computed: {
+ canCreatePipelineParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ },
+
+ scope() {
+ const scope = gl.utils.getParameterByName('scope');
+ return scope === null ? 'all' : scope;
+ },
+
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
+
+ /**
+ * The empty state should only be rendered when the request is made to fetch all pipelines
+ * and none is returned.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderEmptyState() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ (this.scope === 'all' || this.scope === null);
+ },
+
+ /**
+ * When a specific scope does not have pipelines we render a message.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderNoPipelinesMessage() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ this.scope !== 'all' &&
+ this.scope !== null;
+ },
+
+ shouldRenderTable() {
+ return !this.hasError &&
+ !this.isLoading && this.state.pipelines.length;
+ },
+
+ /**
+ * Pagination should only be rendered when there is more than one page.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderPagination() {
+ return !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage;
+ },
+
+ hasCiEnabled() {
+ return this.hasCi !== undefined;
+ },
+
+ paths() {
+ return {
+ allPath: this.allPath,
+ pendingPath: this.pendingPath,
+ finishedPath: this.finishedPath,
+ runningPath: this.runningPath,
+ branchesPath: this.branchesPath,
+ tagsPath: this.tagsPath,
+ };
+ },
+ },
+
created() {
this.service = new PipelinesService(this.endpoint);
@@ -69,7 +157,7 @@ export default {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
- this.pageRequest = true;
+ this.isLoading = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
@@ -81,41 +169,72 @@ export default {
this.store.storePagination(response.headers);
})
.then(() => {
- this.pageRequest = false;
+ this.isLoading = false;
})
.catch(() => {
- this.pageRequest = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ this.hasError = true;
+ this.isLoading = false;
});
},
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if="pageRequest">
- <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </div>
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
+ template: `
+ <div :class="cssClass">
+
+ <div
+ class="top-area"
+ v-if="!isLoading && !shouldRenderEmptyState">
+ <navigation-tabs
+ :scope="scope"
+ :count="state.count"
+ :paths="paths" />
+
+ <navigation-controls
+ :new-pipeline-path="newPipelinePath"
+ :has-ci-enabled="hasCiEnabled"
+ :help-page-path="helpPagePath"
+ :ciLintPath="ciLintPath"
+ :can-create-pipeline="canCreatePipelineParsed " />
</div>
- <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
- <pipelines-table-component
- :pipelines="state.pipelines"
- :service="service"/>
+ <div class="content-list pipelines">
+
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </div>
+
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="shouldRenderNoPipelinesMessage">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
+
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
+ </div>
+
+ <gl-pagination
+ v-if="shouldRenderPagination"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"/>
</div>
-
- <gl-pagination
- v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
- :pagenum="pagenum"
- :change="change"
- :count="state.count.all"
- :pageInfo="state.pageInfo"
- >
- </gl-pagination>
</div>
`,
};
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 772f05feb12..a20db153d09 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -2,6 +2,7 @@
.realtime-loading {
font-size: 40px;
text-align: center;
+ margin: 0 auto;
}
.stage {
@@ -13,6 +14,10 @@
white-space: nowrap;
}
+ .empty-state {
+ margin: 5% auto 0;
+ }
+
.table-holder {
width: 100%;
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index da5a676274f..09e3a775d1c 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,6 +1,7 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5d59ce06612..3d73284699f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,53 +2,19 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
- = link_to project_pipelines_path(@project) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@pipelines_count)
-
- %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
- = link_to project_pipelines_path(@project, scope: :pending) do
- Pending
- %span.badge
- = number_with_delimiter(@pending_count)
-
- %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
- = link_to project_pipelines_path(@project, scope: :running) do
- Running
- %span.badge.js-running-count
- = number_with_delimiter(@running_count)
-
- %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
- = link_to project_pipelines_path(@project, scope: :finished) do
- Finished
- %span.badge
- = number_with_delimiter(@finished_count)
-
- %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
- = link_to project_pipelines_path(@project, scope: :branches) do
- Branches
-
- %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
- = link_to project_pipelines_path(@project, scope: :tags) do
- Tags
-
- .nav-controls
- - if can? current_user, :create_pipeline, @project
- = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
- Run pipeline
-
- - unless @repository.gitlab_ci_yml
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
-
- = 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) } }
- .vue-pipelines-index
+#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json),
+ "css-class" => container_class,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
+ "new-pipeline-path" => new_namespace_project_pipeline_path(@project.namespace, @project),
+ "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
+ "all-path" => project_pipelines_path(@project),
+ "pending-path" => project_pipelines_path(@project, scope: :pending),
+ "running-path" => project_pipelines_path(@project, scope: :running),
+ "finished-path" => project_pipelines_path(@project, scope: :finished),
+ "branches-path" => project_pipelines_path(@project, scope: :branches),
+ "tags-path" => project_pipelines_path(@project, scope: :tags),
+ "has-ci" => @repository.gitlab_ci_yml,
+ "ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg
new file mode 100644
index 00000000000..8119d5bebe0
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg
new file mode 100644
index 00000000000..7dbabf7e4ef
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg> \ No newline at end of file
diff --git a/changelogs/unreleased/27574-pipelines-empty-state.yml b/changelogs/unreleased/27574-pipelines-empty-state.yml
new file mode 100644
index 00000000000..c26ea97205f
--- /dev/null
+++ b/changelogs/unreleased/27574-pipelines-empty-state.yml
@@ -0,0 +1,4 @@
+---
+title: Adds empty and error state to pipelines
+merge_request:
+author:
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 162056671e0..2272b19bc8f 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -442,7 +442,7 @@ describe 'Pipelines', :feature, :js do
context 'when project is public' do
let(:project) { create(:project, :public) }
- it { expect(page).to have_content 'No pipelines to show' }
+ it { expect(page).to have_content 'Build with confidence' }
it { expect(page).to have_http_status(:success) }
end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 75efcc06585..bc2e092db65 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -33,7 +33,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ expect(component.$el.querySelector('.empty-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 1);
});
@@ -63,6 +64,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
@@ -92,7 +94,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
new file mode 100644
index 00000000000..418a38a0e2e
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -0,0 +1,14 @@
+%div
+ #pipelines-list-vue{ data: { endpoint: 'foo',
+ "css-class" => 'foo',
+ "help-page-path" => 'foo',
+ "new-pipeline-path" => 'foo',
+ "can-create-pipeline" => 'true',
+ "all-path" => 'foo',
+ "pending-path" => 'foo',
+ "running-path" => 'foo',
+ "finished-path" => 'foo',
+ "branches-path" => 'foo',
+ "tags-path" => 'foo',
+ "has-ci" => 'foo',
+ "ci-lint-path" => 'foo' } }
diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml
index fbe4a434f76..ad1682704bb 100644
--- a/spec/javascripts/fixtures/pipelines_table.html.haml
+++ b/spec/javascripts/fixtures/pipelines_table.html.haml
@@ -1,2 +1 @@
-#commit-pipeline-table-view{ data: { endpoint: "endpoint" } }
-.pipeline-svgs{ data: { "commit_icon_svg": "svg"} }
+#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } }
diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js
new file mode 100644
index 00000000000..733337168dc
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import emptyStateComp from '~/vue_pipelines_index/components/empty_state';
+
+describe('Pipelines Empty State', () => {
+ let component;
+ let EmptyStateComponent;
+
+ beforeEach(() => {
+ EmptyStateComponent = Vue.extend(emptyStateComp);
+
+ component = new EmptyStateComponent({
+ propsData: {
+ helpPagePath: 'foo',
+ },
+ }).$mount();
+ });
+
+ it('should render empty state SVG', () => {
+ expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
+ });
+
+ it('should render emtpy state information', () => {
+ expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
+
+ expect(
+ component.$el.querySelector('p').textContent,
+ ).toContain('Continous Integration can help catch bugs by running your tests automatically');
+
+ expect(
+ component.$el.querySelector('p').textContent,
+ ).toContain('Continuous Deployment can help you deliver code to your product environment');
+ });
+
+ it('should render a link with provided help path', () => {
+ expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js
new file mode 100644
index 00000000000..524e018b1fa
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import errorStateComp from '~/vue_pipelines_index/components/error_state';
+
+describe('Pipelines Error State', () => {
+ let component;
+ let ErrorStateComponent;
+
+ beforeEach(() => {
+ ErrorStateComponent = Vue.extend(errorStateComp);
+
+ component = new ErrorStateComponent().$mount();
+ });
+
+ it('should render error state SVG', () => {
+ expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
+ });
+
+ it('should render emtpy state information', () => {
+ expect(
+ component.$el.querySelector('h4').textContent,
+ ).toContain('The API failed to fetch the pipelines');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/vue_pipelines_index/mock_data.js
new file mode 100644
index 00000000000..2365a662b9f
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/mock_data.js
@@ -0,0 +1,107 @@
+export default {
+ pipelines: [{
+ id: 115,
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ path: '/root/review-app/pipelines/115',
+ details: {
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/115',
+ },
+ duration: null,
+ finished_at: '2017-03-17T19:00:15.996Z',
+ 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/115#build',
+ },
+ path: '/root/review-app/pipelines/115#build',
+ dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build',
+ },
+ {
+ name: 'review',
+ title: 'review: skipped',
+ status: {
+ icon: 'icon_status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/115#review',
+ },
+ path: '/root/review-app/pipelines/115#review',
+ dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review',
+ }],
+ artifacts: [],
+ manual_actions: [{
+ name: 'stop_review',
+ path: '/root/review-app/builds/3766/play',
+ }],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ },
+ ref: {
+ name: 'thisisabranch',
+ path: '/root/review-app/tree/thisisabranch',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ short_id: '9e87f876',
+ title: 'Update README.md',
+ created_at: '2017-03-15T22:58:28.000+00:00',
+ parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'],
+ message: 'Update README.md',
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-03-15T22:58:28.000+00:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-03-15T22:58:28.000+00:00',
+ author: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
+ },
+ retry_path: '/root/review-app/pipelines/115/retry',
+ created_at: '2017-03-15T22:58:33.436Z',
+ updated_at: '2017-03-17T19:00:15.997Z',
+ }],
+ count: {
+ all: 52,
+ running: 0,
+ pending: 0,
+ finished: 52,
+ },
+};
diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js
new file mode 100644
index 00000000000..659c4854a56
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import navControlsComp from '~/vue_pipelines_index/components/nav_controls';
+
+describe('Pipelines Nav Controls', () => {
+ let NavControlsComponent;
+
+ beforeEach(() => {
+ NavControlsComponent = Vue.extend(navControlsComp);
+ });
+
+ it('should render link to create a new pipeline', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline');
+ expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath);
+ });
+
+ it('should not render link to create pipeline if no permission is provided', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: false,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-create')).toEqual(null);
+ });
+
+ it('should render link for CI lint', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint');
+ expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath);
+ });
+
+ it('should render link to help page when CI is not enabled', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: false,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
+ });
+
+ it('should not render link to help page when CI is enabled', () => {
+ const mockData = {
+ newPipelinePath: 'foo',
+ hasCiEnabled: true,
+ helpPagePath: 'foo',
+ ciLintPath: 'foo',
+ canCreatePipeline: true,
+ };
+
+ const component = new NavControlsComponent({
+ propsData: mockData,
+ }).$mount();
+
+ expect(component.$el.querySelector('.btn-info')).toEqual(null);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_spec.js
new file mode 100644
index 00000000000..725f6cb2d7a
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import pipelinesComp from '~/vue_pipelines_index/pipelines';
+import Store from '~/vue_pipelines_index/stores/pipelines_store';
+import pipelinesData from './mock_data';
+
+describe('Pipelines', () => {
+ preloadFixtures('static/pipelines.html.raw');
+
+ let PipelinesComponent;
+
+ beforeEach(() => {
+ loadFixtures('static/pipelines.html.raw');
+
+ PipelinesComponent = Vue.extend(pipelinesComp);
+ });
+
+ describe('successfull request', () => {
+ describe('with pipelines', () => {
+ const pipelinesInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(pipelinesData), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('should render table', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.table-holder')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+
+ describe('without pipelines', () => {
+ const emptyInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(emptyInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.empty-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const errorInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(errorInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('should render error state', (done) => {
+ const component = new PipelinesComponent({
+ propsData: {
+ store: new Store(),
+ },
+ }).$mount();
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
+ expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ done();
+ });
+ });
+ });
+});