diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-11-14 15:35:29 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-11-14 15:35:29 +0000 |
commit | be4abe7719a500852ce4f913d345e9b3e8794dc3 (patch) | |
tree | 03cc5bb43e1cd83a4a5ee6263545e4f532a4c6e8 /app | |
parent | 62287fec4f3dcfce99a1c8e083efb799720d97f2 (diff) | |
download | gitlab-ce-be4abe7719a500852ce4f913d345e9b3e8794dc3.tar.gz |
Stops page reload when changing tabs or pages - uses API requests instead
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 36 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/poll.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/navigation_tabs.vue | 87 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/pipelines.vue | 163 | ||||
-rw-r--r-- | app/views/projects/pipelines/index.html.haml | 6 |
5 files changed, 180 insertions, 120 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 5c4926d6ac8..195e2ca6a78 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -310,6 +310,42 @@ export const setParamInURL = (param, value) => { }; /** + * Given a string of query parameters creates an object. + * + * @example + * `scope=all&page=2` -> { scope: 'all', page: '2'} + * `scope=all` -> { scope: 'all' } + * ``-> {} + * @param {String} query + * @returns {Object} + */ +export const parseQueryStringIntoObject = (query = '') => { + if (query === '') return {}; + + return query + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + Object.assign(acc, { + [val[0]]: decodeURIComponent(val[1]), + }); + return acc; + }, {}); +}; + +export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); + +/** + * Based on the current location and the string parameters provided + * creates a new entry in the history without reloading the page. + * + * @param {String} param + */ +export const historyPushState = (newUrl) => { + window.history.pushState({}, document.title, newUrl); +}; + +/** * Converts permission provided as strings to booleans. * * @param {String} string diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 1485e900945..65a8cf2c891 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -60,7 +60,6 @@ export default class Poll { checkConditions(response) { const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); - if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { this.timeoutID = setTimeout(() => { this.makeRequest(); @@ -102,7 +101,12 @@ export default class Poll { /** * Restarts polling after it has been stoped */ - restart() { + restart(options) { + // update data + if (options && options.data) { + this.options.data = options.data; + } + this.canPoll = true; this.makeRequest(); } diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue index 73f7e3a0cad..07befd23500 100644 --- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue +++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue @@ -2,16 +2,8 @@ export default { name: 'PipelineNavigationTabs', props: { - scope: { - type: String, - required: true, - }, - count: { - type: Object, - required: true, - }, - paths: { - type: Object, + tabs: { + type: Array, required: true, }, }, @@ -23,68 +15,37 @@ // 0 is valid in a badge, but evaluates to false, we need to check for undefined return count !== undefined; }, + + onTabClick(tab) { + this.$emit('onChangeTab', tab.scope); + }, }, }; </script> <template> <ul class="nav-links scrolling-tabs"> <li - class="js-pipelines-tab-all" - :class="{ active: scope === 'all'}"> - <a :href="paths.allPath"> - All - <span - v-if="shouldRenderBadge(count.all)" - 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 - v-if="shouldRenderBadge(count.pending)" - class="badge"> - {{count.pending}} - </span> - </a> - </li> - <li - class="js-pipelines-tab-running" - :class="{ active: scope === 'running'}"> - <a :href="paths.runningPath"> - Running - <span - v-if="shouldRenderBadge(count.running)" - class="badge"> - {{count.running}} - </span> - </a> - </li> - <li - class="js-pipelines-tab-finished" - :class="{ active: scope === 'finished'}"> - <a :href="paths.finishedPath"> - Finished + v-for="(tab, i) in tabs" + :key="i" + :class="{ + active: tab.isActive, + }" + > + <a + role="button" + @click="onTabClick(tab)" + :class="`js-pipelines-tab-${tab.scope}`" + > + {{ tab.name }} + <span - v-if="shouldRenderBadge(count.finished)" - class="badge"> - {{count.finished}} + v-if="shouldRenderBadge(tab.count)" + class="badge" + > + {{tab.count}} </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> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 3da60e88474..cf241c8ffed 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,10 +1,17 @@ <script> + import _ from 'underscore'; import PipelinesService from '../services/pipelines_service'; import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; + import { + convertPermissionToBoolean, + getParameterByName, + historyPushState, + buildUrlWithCurrentLocation, + parseQueryStringIntoObject, + } from '../../lib/utils/common_utils'; export default { props: { @@ -41,27 +48,18 @@ autoDevopsPath: pipelinesData.helpAutoDevopsPath, 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, + scope: getParameterByName('scope') || 'all', + page: getParameterByName('page') || '1', + requestData: {}, }; }, computed: { canCreatePipelineParsed() { return convertPermissionToBoolean(this.canCreatePipeline); }, - scope() { - const scope = getParameterByName('scope'); - return scope === null ? 'all' : scope; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,46 +104,112 @@ 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, - }; - }, - pageParameter() { - return getParameterByName('page') || this.pagenum; - }, - scopeParameter() { - return getParameterByName('scope') || this.apiScope; + + tabs() { + const { count } = this.state; + return [ + { + name: 'All', + scope: 'all', + count: count.all, + isActive: this.scope === 'all', + }, + { + name: 'Pending', + scope: 'pending', + count: count.pending, + isActive: this.scope === 'pending', + }, + { + name: 'Running', + scope: 'running', + count: count.running, + isActive: this.scope === 'running', + }, + { + name: 'Finished', + scope: 'finished', + count: count.finished, + isActive: this.scope === 'finished', + }, + { + name: 'Branches', + scope: 'branches', + isActive: this.scope === 'branches', + }, + { + name: 'Tags', + scope: 'tags', + isActive: this.scope === 'tags', + }, + ]; }, }, created() { this.service = new PipelinesService(this.endpoint); - this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; + this.requestData = { page: this.page, scope: this.scope }; }, methods: { + successCallback(resp) { + return resp.json().then((response) => { + // Because we are polling & the user is interacting verify if the response received + // matches the last request made + if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { + this.store.storeCount(response.count); + this.store.storePagination(resp.headers); + this.setCommonData(response.pipelines); + } + }); + }, /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. + * Handles URL and query parameter changes. + * When the user uses the pagination or the tabs, + * - update URL + * - Make API request to the server with new parameters + * - Update the polling function + * - Update the internal state */ - change(pageNumber) { - const param = setParamInURL('page', pageNumber); + updateContent(parameters) { + // stop polling + this.poll.stop(); + + const queryString = Object.keys(parameters).map((parameter) => { + const value = parameters[parameter]; + // update internal state for UI + this[parameter] = value; + return `${parameter}=${encodeURIComponent(value)}`; + }).join('&'); - gl.utils.visitUrl(param); - return param; + // update polling parameters + this.requestData = parameters; + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + + this.isLoading = true; + // fetch new data + return this.service.getPipelines(this.requestData) + .then((response) => { + this.isLoading = false; + this.successCallback(response); + + // restart polling + this.poll.restart({ data: this.requestData }); + }) + .catch(() => { + this.isLoading = false; + this.errorCallback(); + + // restart polling + this.poll.restart(); + }); }, - successCallback(resp) { - return resp.json().then((response) => { - this.store.storeCount(response.count); - this.store.storePagination(resp.headers); - this.setCommonData(response.pipelines); - }); + onChangeTab(scope) { + this.updateContent({ scope, page: '1' }); + }, + onChangePage(page) { + /* URLS parameters are strings, we need to parse to match types */ + this.updateContent({ scope: this.scope, page: Number(page).toString() }); }, }, }; @@ -154,7 +218,7 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!isLoading && !shouldRenderEmptyState"> + v-if="!shouldRenderEmptyState"> <div class="fade-left"> <i class="fa fa-angle-left" @@ -167,17 +231,17 @@ aria-hidden="true"> </i> </div> + <navigation-tabs - :scope="scope" - :count="state.count" - :paths="paths" + :tabs="tabs" + @onChangeTab="onChangeTab" /> <navigation-controls :new-pipeline-path="newPipelinePath" :has-ci-enabled="hasCiEnabled" :help-page-path="helpPagePath" - :ciLintPath="ciLintPath" + :ci-lint-path="ciLintPath" :can-create-pipeline="canCreatePipelineParsed " /> </div> @@ -188,6 +252,7 @@ label="Loading Pipelines" size="3" v-if="isLoading" + class="prepend-top-20" /> <empty-state @@ -221,8 +286,8 @@ <table-pagination v-if="shouldRenderPagination" - :change="change" - :pageInfo="state.pageInfo" + :change="onChangePage" + :page-info="state.pageInfo" /> </div> </div> diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f8627a3818b..b2e71cff6ce 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -9,12 +9,6 @@ "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "new-pipeline-path" => new_project_pipeline_path(@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 } } |