summaryrefslogtreecommitdiff
path: root/app/assets
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 /app/assets
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
Diffstat (limited to 'app/assets')
-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
8 files changed, 356 insertions, 51 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%;