summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.scss-lint.yml2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js36
-rw-r--r--app/assets/javascripts/lib/utils/poll.js8
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.vue87
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue163
-rw-r--r--app/assets/stylesheets/framework/files.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/pages/environments.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/views/projects/pipelines/index.html.haml6
-rw-r--r--changelogs/unreleased/32098-pipelines-navigation.yml6
-rw-r--r--changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml5
-rw-r--r--changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml5
-rw-r--r--config/prometheus/additional_metrics.yml10
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md4
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb12
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb26
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml8
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js30
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js6
-rw-r--r--spec/javascripts/pipelines/navigation_tabs_spec.js128
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js133
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb9
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