diff options
author | Alfredo Sumaran <alfredo@gitlab.com> | 2017-03-24 01:12:44 +0000 |
---|---|---|
committer | Alfredo Sumaran <alfredo@gitlab.com> | 2017-03-24 01:12:44 +0000 |
commit | 453d755ae4c83cbdd0a6542aca7028f4e1521679 (patch) | |
tree | fa202ad1115e38a52c8797925d86befb4973c09c /app/assets | |
parent | 856edee368c8c6a93821bff97eb0bb08b3e15bf9 (diff) | |
parent | a5f17beb43c792618cd0e8612d8e00bb8edb6942 (diff) | |
download | gitlab-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')
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%; |