diff options
24 files changed, 431 insertions, 273 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index a855ef3c6e9..dcd4cac780a 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -241,7 +241,7 @@ linters: # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: - enabled: false + enabled: true # Do not use parent selector references (&) when they would otherwise # be unnecessary. 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/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 28ab9dddc4c..a1475b92c7e 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -18,7 +18,7 @@ export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3} export const highCountTrim = count => (count > 99 ? '99+' : count); /** - * Converst first char to uppercase and replaces undercores with spaces + * Converts first char to uppercase and replaces undercores with spaces * @param {String} string * @requires {String} */ 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/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1247e5e4876..c2a3cd16e67 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -101,13 +101,13 @@ @for $i from 0 through 5 { .legend-box-#{$i} { - background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%); } } @for $i from 1 through 4 { .legend-box-#{$i + 5} { - background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%); } } } @@ -200,13 +200,13 @@ @for $i from 0 through 5 { td.blame-commit-age-#{$i} { - border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%); } } @for $i from 1 through 4 { td.blame-commit-age-#{$i + 5} { - border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%); } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2dafd1ce47c..cb2a237f574 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e; $gl-text-color-secondary: #707070; $gl-text-color-tertiary: #949494; $gl-text-color-quaternary: #d6d6d6; -$gl-text-color-inverted: rgba(255, 255, 255, 1.0); +$gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-green: $green-600; $gl-text-green-hover: $green-700; @@ -486,8 +486,8 @@ $callout-success-color: $green-700; /* * Commit Page */ -$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); -$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); +$commit-max-width-marker-color: rgba(0, 0, 0, 0); +$commit-message-text-area-bg: rgba(0, 0, 0, 0); /* * Common diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 26c5f093c6b..b0795353ec1 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -173,7 +173,7 @@ .prometheus-graph-overlay { fill: none; - opacity: 0.0; + opacity: 0; pointer-events: all; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 14514b2f193..89f93a92f2e 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -7,7 +7,7 @@ .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { - opacity: 1.0; + opacity: 1; filter: alpha(opacity = 100); } } 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 } } diff --git a/changelogs/unreleased/32098-pipelines-navigation.yml b/changelogs/unreleased/32098-pipelines-navigation.yml new file mode 100644 index 00000000000..925c92b6be8 --- /dev/null +++ b/changelogs/unreleased/32098-pipelines-navigation.yml @@ -0,0 +1,6 @@ +--- +title: Stop reloading the page when using pagination and tabs - use API calls - in + Pipelines table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml b/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml new file mode 100644 index 00000000000..ae1eb3aedd5 --- /dev/null +++ b/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml @@ -0,0 +1,5 @@ +--- +title: Don't try to create fork network memberships for forks with a missing source +merge_request: 15366 +author: +type: fixed diff --git a/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml new file mode 100644 index 00000000000..1049e94f312 --- /dev/null +++ b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml @@ -0,0 +1,5 @@ +--- +title: Enable UnnecessaryMantissa in scss-lint +merge_request: 15255 +author: Takuya Noguchi +type: other diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 190eeb59a2c..601a86490d4 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -145,7 +145,7 @@ - container_memory_usage_bytes weight: 1 queries: - - query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024' + - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024' label: Average unit: MB - title: "CPU Utilization" @@ -154,8 +154,6 @@ - container_cpu_usage_seconds_total weight: 1 queries: - - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100' - label: CPU - unit: "%" - series: - - label: cpu + - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100' + label: Average + unit: "%"
\ No newline at end of file diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 518683965e8..a6673fa2a00 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -13,8 +13,8 @@ integration services must be enabled. | Name | Query | | ---- | ----- | -| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 | -| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 | +| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 | +| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 | ## Configuring Prometheus to monitor for Kubernetes node metrics diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb index c88eb9783ed..67a39d28944 100644 --- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb +++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb @@ -51,10 +51,20 @@ module Gitlab FROM projects WHERE forked_project_links.forked_from_project_id = projects.id ) + AND NOT EXISTS ( + SELECT true + FROM forked_project_links AS parent_links + WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id + AND NOT EXISTS ( + SELECT true + FROM projects + WHERE parent_links.forked_from_project_id = projects.id + ) + ) AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} MISSING_MEMBERS - ForkNetworkMember.count_by_sql(count_sql) > 0 + ForkedProjectLink.count_by_sql(count_sql) > 0 end def log(message) diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index fc689bbb486..50f8f13d261 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -56,31 +56,37 @@ describe 'Pipelines', :js do end it 'shows a tab for All pipelines and count' do - expect(page.find('.js-pipelines-tab-all a').text).to include('All') + expect(page.find('.js-pipelines-tab-all').text).to include('All') expect(page.find('.js-pipelines-tab-all .badge').text).to include('1') end it 'shows a tab for Pending pipelines and count' do - expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending') + expect(page.find('.js-pipelines-tab-pending').text).to include('Pending') expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0') end it 'shows a tab for Running pipelines and count' do - expect(page.find('.js-pipelines-tab-running a').text).to include('Running') + expect(page.find('.js-pipelines-tab-running').text).to include('Running') expect(page.find('.js-pipelines-tab-running .badge').text).to include('1') end it 'shows a tab for Finished pipelines and count' do - expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished') + expect(page.find('.js-pipelines-tab-finished').text).to include('Finished') expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0') end it 'shows a tab for Branches' do - expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches') + expect(page.find('.js-pipelines-tab-branches').text).to include('Branches') end it 'shows a tab for Tags' do - expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags') + expect(page.find('.js-pipelines-tab-tags').text).to include('Tags') + end + + it 'updates content when tab is clicked' do + page.find('.js-pipelines-tab-pending').click + wait_for_requests + expect(page).to have_content('No pipelines to show.') end end @@ -396,6 +402,14 @@ describe 'Pipelines', :js do expect(page).to have_selector('.gl-pagination .page', count: 2) end + + it 'should show updated content' do + visit project_pipelines_path(project) + wait_for_requests + page.find('.js-next-button a').click + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end end end diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 97b0c25c923..85ee61f0b54 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -1,16 +1,10 @@ %div #pipelines-list-vue{ data: { endpoint: 'foo', - "css-class" => 'foo', "help-page-path" => 'foo', + "help-auto-devops-path" => 'foo', "empty-state-svg-path" => 'foo', "error-state-svg-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/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a5298be5669..6dad5d6b6bd 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -183,6 +183,36 @@ describe('common_utils', () => { }); }); + describe('historyPushState', () => { + afterEach(() => { + window.history.replaceState({}, null, null); + }); + + it('should call pushState with the correct path', () => { + spyOn(window.history, 'pushState'); + + commonUtils.historyPushState('newpath?page=2'); + + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2'); + }); + }); + + describe('parseQueryStringIntoObject', () => { + it('should return object with query parameters', () => { + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); + expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); + }); + }); + + describe('buildUrlWithCurrentLocation', () => { + it('should build an url with current location and given parameters', () => { + expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); + }); + }); + describe('getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 2aa7011ca51..9b8f68f1676 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -155,7 +155,7 @@ describe('Poll', () => { successCallback: () => { Polling.stop(); setTimeout(() => { - Polling.restart(); + Polling.restart({ data: { page: 4 } }); }, 0); }, errorCallback: callbacks.error, @@ -170,10 +170,10 @@ describe('Poll', () => { Polling.stop(); expect(service.fetch.calls.count()).toEqual(2); - expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); expect(Polling.stop).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); - + expect(Polling.options.data).toEqual({ page: 4 }); done(); }); }); diff --git a/spec/javascripts/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js index 53a88e6322f..f125a2fa189 100644 --- a/spec/javascripts/pipelines/navigation_tabs_spec.js +++ b/spec/javascripts/pipelines/navigation_tabs_spec.js @@ -8,120 +8,48 @@ describe('navigation tabs pipeline component', () => { let data; beforeEach(() => { - data = { - scope: 'all', - count: { - all: 16, - running: 1, - pending: 10, - finished: 0, + data = [ + { + name: 'All', + scope: 'all', + count: 1, + isActive: true, + }, + { + name: 'Pending', + scope: 'pending', + count: 0, + isActive: false, }, - paths: { - allPath: '/gitlab-org/gitlab-ce/pipelines', - pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending', - finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished', - runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running', - branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches', - tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags', + { + name: 'Running', + scope: 'running', + isActive: false, }, - }; + ]; Component = Vue.extend(navigationTabs); + vm = mountComponent(Component, { tabs: data }); }); afterEach(() => { vm.$destroy(); }); - it('should render tabs with correct paths', () => { - vm = mountComponent(Component, data); - - // All - const allTab = vm.$el.querySelector('.js-pipelines-tab-all a'); - expect(allTab.textContent.trim()).toContain('All'); - expect(allTab.getAttribute('href')).toEqual(data.paths.allPath); - - // Pending - const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a'); - expect(pendingTab.textContent.trim()).toContain('Pending'); - expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath); - - // Running - const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a'); - expect(runningTab.textContent.trim()).toContain('Running'); - expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath); - - // Finished - const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a'); - expect(finishedTab.textContent.trim()).toContain('Finished'); - expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath); - - // Branches - const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a'); - expect(branchesTab.textContent.trim()).toContain('Branches'); - - // Tags - const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a'); - expect(tagsTab.textContent.trim()).toContain('Tags'); + it('should render tabs', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(data.length); }); - describe('scope', () => { - it('should render scope provided as active tab', () => { - vm = mountComponent(Component, data); - expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active'); - }); + it('should render active tab', () => { + expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined(); }); - describe('badges', () => { - it('should render provided number', () => { - vm = mountComponent(Component, data); - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(), - ).toContain(data.count.all); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(), - ).toContain(data.count.pending); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(), - ).toContain(data.count.running); - - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(), - ).toContain(data.count.finished); - }); - - it('should not render badge when number is undefined', () => { - vm = mountComponent(Component, { - scope: 'all', - paths: {}, - count: {}, - }); - - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count'), - ).toEqual(null); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge'), - ).toEqual(null); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge'), - ).toEqual(null); + it('should render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1'); + expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0'); + }); - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge'), - ).toEqual(null); - }); + it('should not render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index c30abb2edb0..ff38bc1974d 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -9,26 +10,33 @@ describe('Pipelines', () => { preloadFixtures(jsonFixtureName); let PipelinesComponent; - let pipeline; + let pipelines; + let component; beforeEach(() => { loadFixtures('static/pipelines.html.raw'); - const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); + afterEach(() => { + component.$destroy(); + }); + describe('successfull request', () => { describe('with pipelines', () => { const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipeline), { + next(request.respondWith(JSON.stringify(pipelines), { status: 200, })); }; beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); }); afterEach(() => { @@ -38,18 +46,71 @@ describe('Pipelines', () => { }); 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); + expect( + component.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); done(); }); }); + + it('should render navigation tabs', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + ).toContain('Pending'); + expect( + component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + ).toContain('All'); + expect( + component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + ).toContain('Running'); + expect( + component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + ).toContain('Finished'); + expect( + component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + ).toContain('Branches'); + expect( + component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + ).toContain('Tags'); + done(); + }); + }); + + it('should make an API request when using tabs', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + component.$el.querySelector('.js-pipelines-tab-finished').click(); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + done(); + }); + }); + + describe('with pagination', () => { + it('should make an API request when using pagination', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + // Mock pagination + component.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + Vue.nextTick(() => { + component.$el.querySelector('.js-next-button a').click(); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + + done(); + }); + }); + }); + }); }); describe('without pipelines', () => { @@ -70,15 +131,14 @@ describe('Pipelines', () => { }); it('should render empty state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, }).$mount(); setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); + expect(component.$el.querySelector('.empty-state')).not.toBe(null); done(); }); }); @@ -103,7 +163,7 @@ describe('Pipelines', () => { }); it('should render error state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, @@ -111,9 +171,50 @@ describe('Pipelines', () => { setTimeout(() => { expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }); }); }); + + describe('updateContent', () => { + it('should set given parameters', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + component.updateContent({ scope: 'finished', page: '4' }); + + expect(component.page).toEqual('4'); + expect(component.scope).toEqual('finished'); + expect(component.requestData.scope).toEqual('finished'); + expect(component.requestData.page).toEqual('4'); + }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + + spyOn(component, 'updateContent'); + + component.onChangeTab('running'); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + + spyOn(component, 'updateContent'); + + component.onChangePage(4); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + }); + }); }); diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index 1a4ea2bac48..79d2c071446 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -93,7 +93,14 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat end it 'knows it is finished for this range' do - expect(migration.missing_members?(1, 7)).to be_falsy + expect(migration.missing_members?(1, 8)).to be_falsy + end + + it 'does not miss members for forks of forks for which the root was deleted' do + forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id) + base1.destroy + + expect(migration.missing_members?(7, 10)).to be_falsy end context 'with more forks' do |