summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/api.js27
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue14
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js24
-rw-r--r--app/assets/javascripts/releases/list/components/app.vue20
-rw-r--r--app/assets/javascripts/releases/list/store/actions.js13
-rw-r--r--app/assets/javascripts/releases/list/store/mutations.js6
-rw-r--r--app/assets/javascripts/releases/list/store/state.js1
-rw-r--r--app/assets/javascripts/users_select.js138
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb3
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb27
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/finders/pipelines_finder.rb2
-rw-r--r--app/models/concerns/issuable.rb12
-rw-r--r--changelogs/unreleased/13768-fix-redo-icn.yml2
-rw-r--r--changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml5
-rw-r--r--changelogs/unreleased/feat-ui-releases-pagination.yml5
-rw-r--r--changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml5
-rw-r--r--changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml5
-rw-r--r--db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb18
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md18
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/ci/yaml/README.md1
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md37
-rw-r--r--lib/gitlab/analytics/cycle_analytics/data_collector.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb4
-rw-r--r--lib/gitlab/ci/config/entry/default.rb9
-rw-r--r--lib/gitlab/ci/config/entry/job.rb6
-rw-r--r--lib/gitlab/ci/config/entry/timeout.rb20
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb31
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js1
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_jump_to_next_button_spec.js1
-rw-r--r--spec/frontend/notes/components/discussion_keyboard_navigator_spec.js9
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js1
-rw-r--r--spec/javascripts/notes/components/discussion_counter_spec.js35
-rw-r--r--spec/javascripts/releases/list/components/app_spec.js41
-rw-r--r--spec/javascripts/releases/list/store/actions_spec.js49
-rw-r--r--spec/javascripts/releases/list/store/mutations_spec.js12
-rw-r--r--spec/javascripts/releases/mock_data.js18
-rw-r--r--spec/lib/gitlab/ci/config/entry/default_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb251
-rw-r--r--spec/models/concerns/issuable_spec.rb73
49 files changed, 734 insertions, 249 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index aee9990bc0b..6ec77186298 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,6 +5,8 @@ import { joinPaths } from './lib/utils/url_utility';
import flash from '~/flash';
import { __ } from '~/locale';
+const DEFAULT_PER_PAGE = 20;
+
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
@@ -66,7 +68,7 @@ const Api = {
params: Object.assign(
{
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
options,
),
@@ -90,7 +92,7 @@ const Api = {
.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
})
.then(({ data }) => callback(data));
@@ -101,7 +103,7 @@ const Api = {
const url = Api.buildUrl(Api.projectsPath);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
simple: true,
};
@@ -126,7 +128,7 @@ const Api = {
.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
...options,
},
})
@@ -235,7 +237,7 @@ const Api = {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
@@ -325,7 +327,7 @@ const Api = {
params: Object.assign(
{
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
},
options,
),
@@ -355,7 +357,7 @@ const Api = {
const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
const defaults = {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
@@ -371,7 +373,7 @@ const Api = {
return axios.get(url, {
params: {
search: query,
- per_page: 20,
+ per_page: DEFAULT_PER_PAGE,
...options,
},
});
@@ -403,10 +405,15 @@ const Api = {
return axios.post(url);
},
- releases(id) {
+ releases(id, options = {}) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
- return axios.get(url);
+ return axios.get(url, {
+ params: {
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
+ });
},
release(projectPath, tagName) {
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d7ffa0abb79..98f1f385e9b 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -19,6 +19,7 @@ export default {
'resolvableDiscussionsCount',
'firstUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
+ 'getDiscussion',
]),
isLoggedIn() {
return this.getUserData.id;
@@ -40,9 +41,10 @@ export default {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
const diffTab = window.mrTabs.currentAction === 'diffs';
- const discussionId = this.firstUnresolvedDiscussionId(diffTab);
-
- this.jumpToDiscussion(discussionId);
+ const discussionId =
+ this.firstUnresolvedDiscussionId(diffTab) || this.firstUnresolvedDiscussionId();
+ const firstDiscussion = this.getDiscussion(discussionId);
+ this.jumpToDiscussion(firstDiscussion);
},
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
index 7fbfe8eebb2..7d742fbfeee 100644
--- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -19,7 +19,11 @@ export default {
};
},
computed: {
- ...mapGetters(['nextUnresolvedDiscussionId', 'previousUnresolvedDiscussionId']),
+ ...mapGetters([
+ 'nextUnresolvedDiscussionId',
+ 'previousUnresolvedDiscussionId',
+ 'getDiscussion',
+ ]),
},
mounted() {
Mousetrap.bind('n', () => this.jumpToNextDiscussion());
@@ -33,14 +37,14 @@ export default {
...mapActions(['expandDiscussion']),
jumpToNextDiscussion() {
const nextId = this.nextUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
-
- this.jumpToDiscussion(nextId);
+ const nextDiscussion = this.getDiscussion(nextId);
+ this.jumpToDiscussion(nextDiscussion);
this.currentDiscussionId = nextId;
},
jumpToPreviousDiscussion() {
const prevId = this.previousUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
-
- this.jumpToDiscussion(prevId);
+ const prevDiscussion = this.getDiscussion(prevId);
+ this.jumpToDiscussion(prevDiscussion);
this.currentDiscussionId = prevId;
},
},
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 47ec740b63a..62d401d4911 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -84,6 +84,7 @@ export default {
'hasUnresolvedDiscussions',
'showJumpToNextDiscussion',
'getUserData',
+ 'getDiscussion',
]),
currentUser() {
return this.getUserData;
@@ -221,8 +222,9 @@ export default {
this.discussion.id,
this.discussionsByDiffOrder,
);
+ const nextDiscussion = this.getDiscussion(nextId);
- this.jumpToDiscussion(nextId);
+ this.jumpToDiscussion(nextDiscussion);
},
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 3d89d907777..94ca01e44cc 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -35,20 +35,26 @@ export default {
return false;
},
- jumpToDiscussion(id) {
+
+ switchToDiscussionsTabAndJumpTo(id) {
+ window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
+ setTimeout(() => this.discussionJump(id), 0);
+ });
+
+ window.mrTabs.tabShown('show');
+ },
+
+ jumpToDiscussion(discussion) {
+ const { id, diff_discussion: isDiffDiscussion } = discussion;
if (id) {
const activeTab = window.mrTabs.currentAction;
- if (activeTab === 'diffs') {
+ if (activeTab === 'diffs' && isDiffDiscussion) {
this.diffsJump(id);
- } else if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
- setTimeout(() => this.discussionJump(id), 0);
- });
-
- window.mrTabs.tabShown('show');
- } else {
+ } else if (activeTab === 'show') {
this.discussionJump(id);
+ } else {
+ this.switchToDiscussionsTabAndJumpTo(id);
}
}
},
diff --git a/app/assets/javascripts/releases/list/components/app.vue b/app/assets/javascripts/releases/list/components/app.vue
index 5a06c4fec58..a414b3ccd4e 100644
--- a/app/assets/javascripts/releases/list/components/app.vue
+++ b/app/assets/javascripts/releases/list/components/app.vue
@@ -1,6 +1,12 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui';
+import {
+ getParameterByName,
+ historyPushState,
+ buildUrlWithCurrentLocation,
+} from '~/lib/utils/common_utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleaseBlock from './release_block.vue';
export default {
@@ -9,6 +15,7 @@ export default {
GlSkeletonLoading,
GlEmptyState,
ReleaseBlock,
+ TablePagination,
},
props: {
projectId: {
@@ -25,7 +32,7 @@ export default {
},
},
computed: {
- ...mapState(['isLoading', 'releases', 'hasError']),
+ ...mapState(['isLoading', 'releases', 'hasError', 'pageInfo']),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
@@ -34,10 +41,17 @@ export default {
},
},
created() {
- this.fetchReleases(this.projectId);
+ this.fetchReleases({
+ page: getParameterByName('page'),
+ projectId: this.projectId,
+ });
},
methods: {
...mapActions(['fetchReleases']),
+ onChangePage(page) {
+ historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
+ this.fetchReleases({ page, projectId: this.projectId });
+ },
},
};
</script>
@@ -67,6 +81,8 @@ export default {
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
</div>
+
+ <table-pagination v-if="!isLoading" :change="onChangePage" :page-info="pageInfo" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/releases/list/store/actions.js b/app/assets/javascripts/releases/list/store/actions.js
index e0a922d5ef6..b15fb69226f 100644
--- a/app/assets/javascripts/releases/list/store/actions.js
+++ b/app/assets/javascripts/releases/list/store/actions.js
@@ -2,6 +2,7 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
@@ -16,17 +17,19 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
*
* @param {String} projectId
*/
-export const fetchReleases = ({ dispatch }, projectId) => {
+export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
dispatch('requestReleases');
api
- .releases(projectId)
- .then(({ data }) => dispatch('receiveReleasesSuccess', data))
+ .releases(projectId, { page })
+ .then(response => dispatch('receiveReleasesSuccess', response))
.catch(() => dispatch('receiveReleasesError'));
};
-export const receiveReleasesSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_RELEASES_SUCCESS, data);
+export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
+ const pageInfo = parseIntPagination(normalizeHeaders(headers));
+ commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo });
+};
export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
diff --git a/app/assets/javascripts/releases/list/store/mutations.js b/app/assets/javascripts/releases/list/store/mutations.js
index b97dc6cb0ab..99fc096264a 100644
--- a/app/assets/javascripts/releases/list/store/mutations.js
+++ b/app/assets/javascripts/releases/list/store/mutations.js
@@ -13,13 +13,15 @@ export default {
* Sets isLoading to false.
* Sets hasError to false.
* Sets the received data
+ * Sets the received pagination information
* @param {Object} state
- * @param {Object} data
+ * @param {Object} resp
*/
- [types.RECEIVE_RELEASES_SUCCESS](state, data) {
+ [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) {
state.hasError = false;
state.isLoading = false;
state.releases = data;
+ state.pageInfo = pageInfo;
},
/**
diff --git a/app/assets/javascripts/releases/list/store/state.js b/app/assets/javascripts/releases/list/store/state.js
index bf25e651c99..c251f56c9c5 100644
--- a/app/assets/javascripts/releases/list/store/state.js
+++ b/app/assets/javascripts/releases/list/store/state.js
@@ -2,4 +2,5 @@ export default () => ({
isLoading: false,
hasError: false,
releases: [],
+ pageInfo: {},
});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index da1a7c290f8..57fbb88ca2e 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
+/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -13,7 +13,7 @@ import { parseBoolean } from './lib/utils/common_utils';
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) {
- var $els;
+ const $els = $(els || '.js-user-search');
this.users = this.users.bind(this);
this.user = this.user.bind(this);
this.usersPath = '/autocomplete/users.json';
@@ -28,36 +28,11 @@ function UsersSelect(currentUser, els, options = {}) {
const { handleClick } = options;
- $els = $(els);
-
- if (!els) {
- $els = $('.js-user-search');
- }
-
$els.each(
(function(_this) {
return function(i, dropdown) {
- var options = {};
- var $block,
- $collapsedSidebar,
- $dropdown,
- $loading,
- $selectbox,
- $value,
- abilityName,
- assignTo,
- assigneeTemplate,
- collapsedAssigneeTemplate,
- defaultLabel,
- defaultNullUser,
- firstUser,
- issueURL,
- selectedId,
- selectedIdDefault,
- showAnyUser,
- showNullUser,
- showMenuAbove;
- $dropdown = $(dropdown);
+ const options = {};
+ const $dropdown = $(dropdown);
options.projectId = $dropdown.data('projectId');
options.groupId = $dropdown.data('groupId');
options.showCurrentUser = $dropdown.data('currentUser');
@@ -65,22 +40,25 @@ function UsersSelect(currentUser, els, options = {}) {
options.todoStateFilter = $dropdown.data('todoStateFilter');
options.iid = $dropdown.data('iid');
options.issuableType = $dropdown.data('issuableType');
- showNullUser = $dropdown.data('nullUser');
- defaultNullUser = $dropdown.data('nullUserDefault');
- showMenuAbove = $dropdown.data('showMenuAbove');
- showAnyUser = $dropdown.data('anyUser');
- firstUser = $dropdown.data('firstUser');
+ const showNullUser = $dropdown.data('nullUser');
+ const defaultNullUser = $dropdown.data('nullUserDefault');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showAnyUser = $dropdown.data('anyUser');
+ const firstUser = $dropdown.data('firstUser');
options.authorId = $dropdown.data('authorId');
- defaultLabel = $dropdown.data('defaultLabel');
- issueURL = $dropdown.data('issueUpdate');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- abilityName = $dropdown.data('abilityName');
- $value = $block.find('.value');
- $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- $loading = $block.find('.block-loading').fadeOut();
- selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
- selectedId = $dropdown.data('selected');
+ const defaultLabel = $dropdown.data('defaultLabel');
+ const issueURL = $dropdown.data('issueUpdate');
+ const $selectbox = $dropdown.closest('.selectbox');
+ let $block = $selectbox.closest('.block');
+ const abilityName = $dropdown.data('abilityName');
+ let $value = $block.find('.value');
+ const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ const $loading = $block.find('.block-loading').fadeOut();
+ const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
+ let selectedId = $dropdown.data('selected');
+ let assignTo;
+ let assigneeTemplate;
+ let collapsedAssigneeTemplate;
if (selectedId === undefined) {
selectedId = selectedIdDefault;
@@ -207,15 +185,15 @@ function UsersSelect(currentUser, els, options = {}) {
});
assignTo = function(selected) {
- var data;
- data = {};
+ const data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return axios.put(issueURL, data).then(({ data }) => {
- var user, tooltipTitle;
+ let user = {};
+ let tooltipTitle = user.name;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -471,10 +449,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
- var isIssueIndex, isMRIndex, page, selected;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
if (
$dropdown.hasClass('js-filter-bulk-update') ||
$dropdown.hasClass('js-issuable-form-dropdown')
@@ -501,7 +478,7 @@ function UsersSelect(currentUser, els, options = {}) {
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if (!$dropdown.hasClass('js-multiselect')) {
- selected = $dropdown
+ const selected = $dropdown
.closest('.selectbox')
.find(`input[name='${$dropdown.data('fieldName')}']`)
.val();
@@ -544,9 +521,8 @@ function UsersSelect(currentUser, els, options = {}) {
},
updateLabel: $dropdown.data('dropdownTitle'),
renderRow(user) {
- var avatar, img, username;
- username = user.username ? `@${user.username}` : '';
- avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
+ const username = user.username ? `@${user.username}` : '';
+ const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
let selected = false;
@@ -565,7 +541,7 @@ function UsersSelect(currentUser, els, options = {}) {
selected = user.id === selectedId;
}
- img = '';
+ let img = '';
if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(
user.name,
@@ -586,35 +562,34 @@ function UsersSelect(currentUser, els, options = {}) {
$('.ajax-users-select').each(
(function(_this) {
return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
+ const options = {};
options.skipLdap = $(select).hasClass('skip_ldap');
options.projectId = $(select).data('projectId');
options.groupId = $(select).data('groupId');
options.showCurrentUser = $(select).data('currentUser');
options.authorId = $(select).data('authorId');
options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
return $(select).select2({
placeholder: __('Search for a user'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
return _this.users(query.term, options, users => {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
+ let name;
+ const data = {
results: users,
};
if (query.term.length === 0) {
if (firstUser) {
// Move current user to the front of the list
- ref = data.results;
+ const ref = data.results;
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
data.results.unshift(obj);
@@ -623,7 +598,7 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
if (showNullUser) {
- nullUser = {
+ const nullUser = {
name: s__('UsersSelect|Unassigned'),
id: 0,
};
@@ -634,7 +609,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (name === true) {
name = s__('UsersSelect|Any User');
}
- anyUser = {
+ const anyUser = {
name,
id: null,
};
@@ -646,8 +621,8 @@ function UsersSelect(currentUser, els, options = {}) {
data.results.length === 0 &&
query.term.match(/^[^@]+@[^@]+$/)
) {
- var trimmed = query.term.trim();
- emailUser = {
+ const trimmed = query.term.trim();
+ const emailUser = {
name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
username: trimmed,
id: trimmed,
@@ -659,18 +634,15 @@ function UsersSelect(currentUser, els, options = {}) {
});
},
initSelection() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.initSelection.apply(_this, args);
},
formatResult() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args);
},
formatSelection() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: 'ajax-users-dropdown',
@@ -687,10 +659,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
UsersSelect.prototype.initSelection = function(element, callback) {
- var id, nullUser;
- id = $(element).val();
+ const id = $(element).val();
if (id === '0') {
- nullUser = {
+ const nullUser = {
name: s__('UsersSelect|Unassigned'),
};
return callback(nullUser);
@@ -700,11 +671,9 @@ UsersSelect.prototype.initSelection = function(element, callback) {
};
UsersSelect.prototype.formatResult = function(user) {
- var avatar;
+ let avatar = gon.default_avatar_url;
if (user.avatar_url) {
avatar = user.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
}
return `
<div class='user-result'>
@@ -732,8 +701,7 @@ UsersSelect.prototype.user = function(user_id, callback) {
return false;
}
- var url;
- url = this.buildUrl(this.userPath);
+ let url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id);
return axios.get(url).then(({ data }) => {
callback(data);
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 1645af695be..a78d803927c 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -38,7 +38,8 @@ module CycleAnalyticsParams
end
def to_utc_time(field)
- Date.parse(field).to_time.utc
+ date = field.is_a?(Date) ? field : Date.parse(field)
+ date.to_time.utc
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 009765702ab..5cbfabebe39 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -44,7 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
- push_frontend_feature_flag(:release_search_filter, project)
+ push_frontend_feature_flag(:release_search_filter, project, default_enabled: true)
end
respond_to :html
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 766ec1e33f3..566a7ed46ca 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -24,7 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
- push_frontend_feature_flag(:release_search_filter, @project)
+ push_frontend_feature_flag(:release_search_filter, @project, default_enabled: true)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index dfddd32d7df..e3ea81d5564 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -13,6 +13,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
+# release_tag: string
# author_id: integer
# author_username: string
# assignee_id: integer or 'None' or 'Any'
@@ -59,6 +60,7 @@ class IssuableFinder
author_username
label_name
milestone_title
+ release_tag
my_reaction_emoji
search
in
@@ -126,6 +128,7 @@ class IssuableFinder
items = by_non_archived(items)
items = by_iids(items)
items = by_milestone(items)
+ items = by_release(items)
items = by_label(items)
by_my_reaction_emoji(items)
end
@@ -364,6 +367,10 @@ class IssuableFinder
end
end
+ def releases?
+ params[:release_tag].present?
+ end
+
private
def force_cte?
@@ -570,6 +577,18 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_release(items)
+ return items unless releases?
+
+ if filter_by_no_release?
+ items.without_release
+ elsif filter_by_any_release?
+ items.any_release
+ else
+ items.with_release(params[:release_tag], params[:project_id])
+ end
+ end
+
def filter_by_no_milestone?
# Accepts `No Milestone` for compatibility
params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
@@ -588,6 +607,14 @@ class IssuableFinder
params[:milestone_title] == Milestone::Started.name
end
+ def filter_by_no_release?
+ params[:release_tag].to_s.downcase == FILTER_NONE
+ end
+
+ def filter_by_any_release?
+ params[:release_tag].to_s.downcase == FILTER_ANY
+ end
+
def by_label(items)
return items unless labels?
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 1c9c7ec68d0..275a01330bf 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -12,6 +12,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
+# release_tag: string
# author_id: integer
# assignee_id: integer
# search: string
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index f5aadc42ff0..092a805f275 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -3,7 +3,7 @@
class PipelinesFinder
attr_reader :project, :pipelines, :params, :current_user
- ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
+ ALLOWED_INDEXED_COLUMNS = %w[id status ref updated_at user_id].freeze
def initialize(project, current_user, params = {})
@project = project
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 71338fedbe9..205bf4a5a26 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -99,6 +99,8 @@ module Issuable
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
+ scope :any_release, -> { joins_milestone_releases }
+ scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
@@ -120,6 +122,16 @@ module Issuable
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
+ scope :without_release, -> do
+ joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
+ .where('milestone_releases.release_id IS NULL')
+ end
+
+ scope :joins_milestone_releases, -> do
+ joins("JOIN milestone_releases ON issues.milestone_id = milestone_releases.milestone_id
+ JOIN releases ON milestone_releases.release_id = releases.id").distinct
+ end
+
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) }
scope :join_project, -> { joins(:project) }
diff --git a/changelogs/unreleased/13768-fix-redo-icn.yml b/changelogs/unreleased/13768-fix-redo-icn.yml
index 3ef194bc4b0..ddac33ef6f5 100644
--- a/changelogs/unreleased/13768-fix-redo-icn.yml
+++ b/changelogs/unreleased/13768-fix-redo-icn.yml
@@ -1,5 +1,5 @@
---
-title: Replacing incorrect icon for Retry in Pipeline list page
+title: Replacing incorrect icon in security dashboard.
merge_request: 20510
author:
type: changed
diff --git a/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml b/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml
new file mode 100644
index 00000000000..0648adc96bf
--- /dev/null
+++ b/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml
@@ -0,0 +1,5 @@
+---
+title: Allow order_by updated_at in Pipelines API
+merge_request: 19886
+author:
+type: added
diff --git a/changelogs/unreleased/feat-ui-releases-pagination.yml b/changelogs/unreleased/feat-ui-releases-pagination.yml
new file mode 100644
index 00000000000..8f6efe8ca01
--- /dev/null
+++ b/changelogs/unreleased/feat-ui-releases-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Implement pagination for project releases page
+merge_request: 19912
+author: Fabio Huser
+type: added
diff --git a/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml b/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml
new file mode 100644
index 00000000000..a5881b6e187
--- /dev/null
+++ b/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml
@@ -0,0 +1,5 @@
+---
+title: Add worker attributes to Sidekiq metrics
+merge_request: 20292
+author:
+type: other
diff --git a/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml b/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml
new file mode 100644
index 00000000000..4dedf4e8d1d
--- /dev/null
+++ b/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure next unresolved discussion button takes user to the right place
+merge_request: 20620
+author:
+type: fixed
diff --git a/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb b/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb
new file mode 100644
index 00000000000..566bb16ac65
--- /dev/null
+++ b/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnCiPipelinesUpdatedAt < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_COLUMNS = [:project_id, :status, :updated_at]
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:ci_pipelines, INDEX_COLUMNS)
+ end
+
+ def down
+ remove_concurrent_index(:ci_pipelines, INDEX_COLUMNS)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7b21db5b098..5c22ef8fe4b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -851,6 +851,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do
t.index ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha"
t.index ["project_id", "source"], name: "index_ci_pipelines_on_project_id_and_source"
t.index ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source"
+ t.index ["project_id", "status", "updated_at"], name: "index_ci_pipelines_on_project_id_and_status_and_updated_at"
t.index ["project_id"], name: "index_ci_pipelines_on_project_id"
t.index ["status"], name: "index_ci_pipelines_on_status"
t.index ["user_id"], name: "index_ci_pipelines_on_user_id"
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
index 0b5ddfd03ee..e15f91ebab2 100644
--- a/doc/administration/operations/extra_sidekiq_processes.md
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -126,12 +126,26 @@ queues will use three threads in total.
## Limiting concurrency
-To limit the concurrency of the Sidekiq processes:
+To limit the concurrency of the Sidekiq process:
1. Edit `/etc/gitlab/gitlab.rb` and add:
```ruby
- sidekiq_cluster['concurrency'] = 25
+ sidekiq['concurrency'] = 25
+ ```
+
+1. Save the file and reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+To limit the max concurrency of the Sidekiq cluster processes:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq_cluster['max_concurrency'] = 25
```
1. Save the file and reconfigure GitLab for the changes to take effect:
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 90a4f8d6e26..97dc316cc96 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -18,7 +18,7 @@ GET /projects/:id/pipelines
| `yaml_errors`| boolean | no | Returns pipelines with invalid configurations |
| `name`| string | no | The name of the user who triggered pipelines |
| `username`| string | no | The username of the user who triggered pipelines |
-| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, or `user_id` (default: `id`) |
+| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, `updated_at` or `user_id` (default: `id`) |
| `sort` | string | no | Sort pipelines in `asc` or `desc` order (default: `desc`) |
```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 73e976a6145..ca04bbd0444 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -135,6 +135,7 @@ The following job parameters can be defined inside a `default:` block:
- [`before_script`](#before_script-and-after_script)
- [`after_script`](#before_script-and-after_script)
- [`cache`](#cache)
+- [`timeout`](#timeout)
- [`interruptible`](#interruptible)
In the following example, the `ruby:2.5` image is set as the default for all
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
index 07ef8bca972..c73368fbbd2 100644
--- a/doc/user/project/clusters/add_remove_clusters.md
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -206,9 +206,46 @@ GitLab supports:
Before creating your first cluster on Amazon EKS with GitLab's integration,
make sure the following requirements are met:
+- Enable the `create_eks_clusters` feature flag for your GitLab instance.
- An [Amazon Web Services](https://aws.amazon.com/) account is set up and you are able to log in.
- You have permissions to manage IAM resources.
+#### Enable the `create_eks_clusters` feature flag **(CORE ONLY)**
+
+NOTE: **Note:**
+If you are running a self-managed instance, EKS cluster creation will not be available
+unless the feature flag `create_eks_clusters` is enabled. This can be done from the Rails console
+by instance administrators.
+
+Use these commands to start the Rails console:
+
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+Then run the following command to enable the feature flag:
+
+```
+Feature.enable(:create_eks_clusters)
+```
+
+You can also enable the feature flag only for specific projects with:
+
+```
+Feature.enable(:create_eks_clusters, Project.find_by_full_path('my_group/my_project'))
+```
+
+Run the following command to disable the feature flag:
+
+```
+Feature.disable(:create_eks_clusters)
+```
+
##### Additional requirements for self-managed instances
If you are using a self-managed GitLab instance, GitLab must first
diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
index 05b16672912..5eca364a697 100644
--- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
@@ -42,3 +42,5 @@ module Gitlab
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::DataCollector.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::DataCollector')
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb
index 34c726b2254..29a2d55df1a 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb
@@ -9,11 +9,11 @@ module Gitlab
end
def zero_interval
- Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
+ Arel::Nodes::NamedFunction.new('CAST', [Arel.sql("'0' AS INTERVAL")])
end
def round_duration_to_seconds
- Arel::Nodes::Extract.new(duration, :epoch)
+ Arel::Nodes::NamedFunction.new('ROUND', [Arel::Nodes::Extract.new(duration, :epoch)])
end
def duration
diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb
index 83127bde6e4..646f06a60a9 100644
--- a/lib/gitlab/ci/config/entry/default.rb
+++ b/lib/gitlab/ci/config/entry/default.rb
@@ -14,7 +14,8 @@ module Gitlab
include ::Gitlab::Config::Entry::Inheritable
ALLOWED_KEYS = %i[before_script image services
- after_script cache interruptible].freeze
+ after_script cache interruptible
+ timeout].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -44,7 +45,11 @@ module Gitlab
description: 'Set jobs interruptible default value.',
inherit: false
- helpers :before_script, :image, :services, :after_script, :cache, :interruptible
+ entry :timeout, Entry::Timeout,
+ description: 'Set jobs default timeout.',
+ inherit: false
+
+ helpers :before_script, :image, :services, :after_script, :cache, :interruptible, :timeout
private
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index c75ae87a985..a109265f2a7 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -46,8 +46,6 @@ module Gitlab
message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
}
- validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
-
validates :dependencies, array_of_strings: true
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
@@ -103,6 +101,10 @@ module Gitlab
description: 'Set jobs interruptible value.',
inherit: true
+ entry :timeout, Entry::Timeout,
+ description: 'Timeout duration of this job.',
+ inherit: true
+
entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.',
default: Entry::Policy::DEFAULT_ONLY,
diff --git a/lib/gitlab/ci/config/entry/timeout.rb b/lib/gitlab/ci/config/entry/timeout.rb
new file mode 100644
index 00000000000..0bffa9340de
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/timeout.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents the interrutible value.
+ #
+ class Timeout < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index bd819843bd4..7bfb0d54d80 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -7,14 +7,17 @@ module Gitlab
# timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
+ TRUE_LABEL = "yes"
+ FALSE_LABEL = "no"
+
def initialize
@metrics = init_metrics
@metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
end
- def call(_worker, job, queue)
- labels = create_labels(queue)
+ def call(worker, job, queue)
+ labels = create_labels(worker.class, queue)
queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
@metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
@@ -42,7 +45,7 @@ module Gitlab
@metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
# job_status: done, fail match the job_status attribute in structured logging
- labels[:job_status] = job_succeeded ? :done : :fail
+ labels[:job_status] = job_succeeded ? "done" : "fail"
@metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
@metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time)
end
@@ -62,10 +65,24 @@ module Gitlab
}
end
- def create_labels(queue)
- {
- queue: queue
- }
+ def create_labels(worker_class, queue)
+ labels = { queue: queue.to_s, latency_sensitive: FALSE_LABEL, external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" }
+ return labels unless worker_class.include? WorkerAttributes
+
+ labels[:latency_sensitive] = bool_as_label(worker_class.latency_sensitive_worker?)
+ labels[:external_dependencies] = bool_as_label(worker_class.worker_has_external_dependencies?)
+
+ feature_category = worker_class.get_feature_category
+ labels[:feature_category] = feature_category.to_s
+
+ resource_boundary = worker_class.get_worker_resource_boundary
+ labels[:boundary] = resource_boundary == :unknown ? "" : resource_boundary.to_s
+
+ labels
+ end
+
+ def bool_as_label(value)
+ value ? TRUE_LABEL : FALSE_LABEL
end
def get_thread_cputime
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 45b99b71e06..475ea4f0f7d 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -38,6 +38,7 @@ describe('issue_comment_form component', () => {
},
store,
sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 91f9dab2530..3ccfea121b0 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -37,6 +37,8 @@ describe('DiscussionActions', () => {
shouldShowJumpToNextDiscussion: true,
...props,
},
+ sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
index fd439ba46bd..ed173eacfab 100644
--- a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
+++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
@@ -7,6 +7,7 @@ describe('JumpToNextDiscussionButton', () => {
beforeEach(() => {
wrapper = shallowMount(JumpToNextDiscussionButton, {
sync: false,
+ attachToDocument: true,
});
});
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
index 8881bedf3cc..b38cfa8fb4a 100644
--- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
@@ -37,6 +37,7 @@ describe('notes/components/discussion_keyboard_navigator', () => {
isDiff ? NEXT_DIFF_ID : NEXT_ID;
notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) =>
isDiff ? PREV_DIFF_ID : PREV_ID;
+ notes.getters.getDiscussion = () => id => ({ id });
storeOptions = {
modules: {
@@ -63,14 +64,18 @@ describe('notes/components/discussion_keyboard_navigator', () => {
it('calls jumpToNextDiscussion when pressing `n`', () => {
Mousetrap.trigger('n');
- expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedNextId);
+ expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(
+ expect.objectContaining({ id: expectedNextId }),
+ );
expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId);
});
it('calls jumpToPreviousDiscussion when pressing `p`', () => {
Mousetrap.trigger('p');
- expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedPrevId);
+ expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(
+ expect.objectContaining({ id: expectedPrevId }),
+ );
expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId);
});
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index f77236b14bc..5ab26d742ca 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -36,6 +36,7 @@ describe('DiscussionNotes', () => {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
},
sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js
index fecc0d604b1..2ad9428dd6f 100644
--- a/spec/javascripts/notes/components/discussion_counter_spec.js
+++ b/spec/javascripts/notes/components/discussion_counter_spec.js
@@ -27,6 +27,8 @@ describe('DiscussionCounter component', () => {
describe('methods', () => {
describe('jumpToFirstUnresolvedDiscussion', () => {
it('expands unresolved discussion', () => {
+ window.mrTabs.currentAction = 'show';
+
spyOn(vm, 'expandDiscussion').and.stub();
const discussions = [
{
@@ -47,14 +49,39 @@ describe('DiscussionCounter component', () => {
...store.state,
discussions,
});
- setFixtures(`
- <div class="discussion" data-discussion-id="${firstDiscussionId}"></div>
- `);
-
vm.jumpToFirstUnresolvedDiscussion();
expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId });
});
+
+ it('jumps to first unresolved discussion from diff tab if all diff discussions are resolved', () => {
+ window.mrTabs.currentAction = 'diff';
+ spyOn(vm, 'switchToDiscussionsTabAndJumpTo').and.stub();
+
+ const unresolvedId = discussionMock.id + 1;
+ const discussions = [
+ {
+ ...discussionMock,
+ id: discussionMock.id,
+ diff_discussion: true,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ resolved: true,
+ },
+ {
+ ...discussionMock,
+ id: unresolvedId,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }],
+ resolved: false,
+ },
+ ];
+ store.replaceState({
+ ...store.state,
+ discussions,
+ });
+ vm.jumpToFirstUnresolvedDiscussion();
+
+ expect(vm.switchToDiscussionsTabAndJumpTo).toHaveBeenCalledWith(unresolvedId);
+ });
});
});
});
diff --git a/spec/javascripts/releases/list/components/app_spec.js b/spec/javascripts/releases/list/components/app_spec.js
index 471c442e497..994488581d7 100644
--- a/spec/javascripts/releases/list/components/app_spec.js
+++ b/spec/javascripts/releases/list/components/app_spec.js
@@ -1,15 +1,22 @@
import Vue from 'vue';
+import _ from 'underscore';
import app from '~/releases/list/components/app.vue';
import createStore from '~/releases/list/store';
import api from '~/api';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
-import { releases } from '../../mock_data';
+import {
+ pageInfoHeadersWithoutPagination,
+ pageInfoHeadersWithPagination,
+ release,
+ releases,
+} from '../../mock_data';
describe('Releases App ', () => {
const Component = Vue.extend(app);
let store;
let vm;
+ let releasesPagination;
const props = {
projectId: 'gitlab-ce',
@@ -19,6 +26,7 @@ describe('Releases App ', () => {
beforeEach(() => {
store = createStore();
+ releasesPagination = _.range(21).map(index => ({ ...release, tag_name: `${index}.00` }));
});
afterEach(() => {
@@ -28,7 +36,7 @@ describe('Releases App ', () => {
describe('while loading', () => {
beforeEach(() => {
- spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
vm = mountComponentWithStore(Component, { props, store });
});
@@ -36,6 +44,7 @@ describe('Releases App ', () => {
expect(vm.$el.querySelector('.js-loading')).not.toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
setTimeout(() => {
done();
@@ -45,7 +54,9 @@ describe('Releases App ', () => {
describe('with successful request', () => {
beforeEach(() => {
- spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
+ spyOn(api, 'releases').and.returnValue(
+ Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }),
+ );
vm = mountComponentWithStore(Component, { props, store });
});
@@ -54,6 +65,27 @@ describe('Releases App ', () => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with successful request and pagination', () => {
+ beforeEach(() => {
+ spyOn(api, 'releases').and.returnValue(
+ Promise.resolve({ data: releasesPagination, headers: pageInfoHeadersWithPagination }),
+ );
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ it('renders success state', done => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-loading')).toBeNull();
+ expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
+ expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
+ expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
done();
}, 0);
@@ -62,7 +94,7 @@ describe('Releases App ', () => {
describe('with empty request', () => {
beforeEach(() => {
- spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
+ spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
vm = mountComponentWithStore(Component, { props, store });
});
@@ -71,6 +103,7 @@ describe('Releases App ', () => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
}, 0);
diff --git a/spec/javascripts/releases/list/store/actions_spec.js b/spec/javascripts/releases/list/store/actions_spec.js
index 8e78a631a5f..c4b49c39e28 100644
--- a/spec/javascripts/releases/list/store/actions_spec.js
+++ b/spec/javascripts/releases/list/store/actions_spec.js
@@ -7,14 +7,17 @@ import {
import state from '~/releases/list/store/state';
import * as types from '~/releases/list/store/mutation_types';
import api from '~/api';
+import { parseIntPagination } from '~/lib/utils/common_utils';
import testAction from 'spec/helpers/vuex_action_helper';
-import { releases } from '../../mock_data';
+import { pageInfoHeadersWithoutPagination, releases } from '../../mock_data';
describe('Releases State actions', () => {
let mockedState;
+ let pageInfo;
beforeEach(() => {
mockedState = state();
+ pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
});
describe('requestReleases', () => {
@@ -25,12 +28,16 @@ describe('Releases State actions', () => {
describe('fetchReleases', () => {
describe('success', () => {
- it('dispatches requestReleases and receiveReleasesSuccess ', done => {
- spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
+ it('dispatches requestReleases and receiveReleasesSuccess', done => {
+ spyOn(api, 'releases').and.callFake((id, options) => {
+ expect(id).toEqual(1);
+ expect(options.page).toEqual('1');
+ return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
+ });
testAction(
fetchReleases,
- releases,
+ { projectId: 1 },
mockedState,
[],
[
@@ -38,7 +45,31 @@ describe('Releases State actions', () => {
type: 'requestReleases',
},
{
- payload: releases,
+ payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
+ type: 'receiveReleasesSuccess',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
+ spyOn(api, 'releases').and.callFake((_, options) => {
+ expect(options.page).toEqual('2');
+ return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
+ });
+
+ testAction(
+ fetchReleases,
+ { page: '2', projectId: 1 },
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
@@ -48,12 +79,12 @@ describe('Releases State actions', () => {
});
describe('error', () => {
- it('dispatches requestReleases and receiveReleasesError ', done => {
+ it('dispatches requestReleases and receiveReleasesError', done => {
spyOn(api, 'releases').and.returnValue(Promise.reject());
testAction(
fetchReleases,
- null,
+ { projectId: null },
mockedState,
[],
[
@@ -74,9 +105,9 @@ describe('Releases State actions', () => {
it('should commit RECEIVE_RELEASES_SUCCESS mutation', done => {
testAction(
receiveReleasesSuccess,
- releases,
+ { data: releases, headers: pageInfoHeadersWithoutPagination },
mockedState,
- [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: releases }],
+ [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: { pageInfo, data: releases } }],
[],
done,
);
diff --git a/spec/javascripts/releases/list/store/mutations_spec.js b/spec/javascripts/releases/list/store/mutations_spec.js
index d2577891495..d756c69d53b 100644
--- a/spec/javascripts/releases/list/store/mutations_spec.js
+++ b/spec/javascripts/releases/list/store/mutations_spec.js
@@ -1,13 +1,16 @@
import state from '~/releases/list/store/state';
import mutations from '~/releases/list/store/mutations';
import * as types from '~/releases/list/store/mutation_types';
-import { releases } from '../../mock_data';
+import { parseIntPagination } from '~/lib/utils/common_utils';
+import { pageInfoHeadersWithoutPagination, releases } from '../../mock_data';
describe('Releases Store Mutations', () => {
let stateCopy;
+ let pageInfo;
beforeEach(() => {
stateCopy = state();
+ pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
});
describe('REQUEST_RELEASES', () => {
@@ -20,7 +23,7 @@ describe('Releases Store Mutations', () => {
describe('RECEIVE_RELEASES_SUCCESS', () => {
beforeEach(() => {
- mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, releases);
+ mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, { pageInfo, data: releases });
});
it('sets is loading to false', () => {
@@ -34,6 +37,10 @@ describe('Releases Store Mutations', () => {
it('sets data', () => {
expect(stateCopy.releases).toEqual(releases);
});
+
+ it('sets pageInfo', () => {
+ expect(stateCopy.pageInfo).toEqual(pageInfo);
+ });
});
describe('RECEIVE_RELEASES_ERROR', () => {
@@ -42,6 +49,7 @@ describe('Releases Store Mutations', () => {
expect(stateCopy.isLoading).toEqual(false);
expect(stateCopy.releases).toEqual([]);
+ expect(stateCopy.pageInfo).toEqual({});
});
});
});
diff --git a/spec/javascripts/releases/mock_data.js b/spec/javascripts/releases/mock_data.js
index 7197eb7bca8..72875dff172 100644
--- a/spec/javascripts/releases/mock_data.js
+++ b/spec/javascripts/releases/mock_data.js
@@ -1,3 +1,21 @@
+export const pageInfoHeadersWithoutPagination = {
+ 'X-NEXT-PAGE': '',
+ 'X-PAGE': '1',
+ 'X-PER-PAGE': '20',
+ 'X-PREV-PAGE': '',
+ 'X-TOTAL': '19',
+ 'X-TOTAL-PAGES': '1',
+};
+
+export const pageInfoHeadersWithPagination = {
+ 'X-NEXT-PAGE': '2',
+ 'X-PAGE': '1',
+ 'X-PER-PAGE': '20',
+ 'X-PREV-PAGE': '',
+ 'X-TOTAL': '21',
+ 'X-TOTAL-PAGES': '2',
+};
+
export const release = {
name: 'Bionic Beaver',
tag_name: '18.04',
diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb
index dad4f408e50..0366a63ef05 100644
--- a/spec/lib/gitlab/ci/config/entry/default_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb
@@ -26,7 +26,8 @@ describe Gitlab::Ci::Config::Entry::Default do
it 'contains the expected node names' do
expect(described_class.nodes.keys)
.to match_array(%i[before_script image services
- after_script cache interruptible])
+ after_script cache interruptible
+ timeout])
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index fe83171c57a..b0e08e49d78 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
image services only except rules needs variables artifacts
- environment coverage retry interruptible]
+ environment coverage retry interruptible timeout]
end
it { is_expected.to match_array result }
@@ -417,21 +417,21 @@ describe Gitlab::Ci::Config::Entry::Job do
context 'when timeout value is not correct' do
context 'when it is higher than instance wide timeout' do
- let(:config) { { timeout: '3 months' } }
+ let(:config) { { timeout: '3 months', script: 'test' } }
it 'returns error about value too high' do
expect(entry).not_to be_valid
expect(entry.errors)
- .to include "job timeout should not exceed the limit"
+ .to include "timeout config should not exceed the limit"
end
end
context 'when it is not a duration' do
- let(:config) { { timeout: 100 } }
+ let(:config) { { timeout: 100, script: 'test' } }
it 'returns error about wrong value' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job timeout should be a duration'
+ expect(entry.errors).to include 'timeout config should be a duration'
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 4b1c7483b11..66f6402b9a2 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1375,7 +1375,7 @@ module Gitlab
end
it 'raises an error for invalid number' do
- expect { builds }.to raise_error('jobs:deploy_to_production timeout should be a duration')
+ expect { builds }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:deploy_to_production:timeout config should be a duration')
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
index 0d8cff3a295..36c6f377bde 100644
--- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
@@ -3,106 +3,201 @@
require 'fast_spec_helper'
describe Gitlab::SidekiqMiddleware::Metrics do
- let(:middleware) { described_class.new }
- let(:concurrency_metric) { double('concurrency metric') }
-
- let(:queue_duration_seconds) { double('queue duration seconds metric') }
- let(:completion_seconds_metric) { double('completion seconds metric') }
- let(:user_execution_seconds_metric) { double('user execution seconds metric') }
- let(:failed_total_metric) { double('failed total metric') }
- let(:retried_total_metric) { double('retried total metric') }
- let(:running_jobs_metric) { double('running jobs metric') }
-
- before do
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds)
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
- allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
- allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
- allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
- allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
-
- allow(concurrency_metric).to receive(:set)
- end
+ context "with worker attribution" do
+ subject { described_class.new }
- describe '#initialize' do
- it 'sets general metrics' do
- expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+ let(:queue) { :test }
+ let(:worker_class) { worker.class }
+ let(:job) { {} }
+ let(:job_status) { :done }
+ let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
+ let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", latency_sensitive: "no" } }
+
+ shared_examples "a metrics middleware" do
+ context "with mocked prometheus" do
+ let(:concurrency_metric) { double('concurrency metric') }
+
+ let(:queue_duration_seconds) { double('queue duration seconds metric') }
+ let(:completion_seconds_metric) { double('completion seconds metric') }
+ let(:user_execution_seconds_metric) { double('user execution seconds metric') }
+ let(:failed_total_metric) { double('failed total metric') }
+ let(:retried_total_metric) { double('retried total metric') }
+ let(:running_jobs_metric) { double('running jobs metric') }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
+ allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
+ allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
+
+ allow(concurrency_metric).to receive(:set)
+ end
+
+ describe '#initialize' do
+ it 'sets concurrency metrics' do
+ expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+
+ subject
+ end
+ end
+
+ describe '#call' do
+ let(:thread_cputime_before) { 1 }
+ let(:thread_cputime_after) { 2 }
+ let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before }
+
+ let(:monotonic_time_before) { 11 }
+ let(:monotonic_time_after) { 20 }
+ let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
+
+ let(:queue_duration_for_job) { 0.01 }
+
+ before do
+ allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+ allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
+
+ expect(running_jobs_metric).to receive(:increment).with(labels, 1)
+ expect(running_jobs_metric).to receive(:increment).with(labels, -1)
+
+ expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job
+ expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration)
+ expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration)
+ end
+
+ it 'yields block' do
+ expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
+ end
+
+ it 'sets queue specific metrics' do
+ subject.call(worker, job, :test) { nil }
+ end
+
+ context 'when job_duration is not available' do
+ let(:queue_duration_for_job) { nil }
+
+ it 'does not set the queue_duration_seconds histogram' do
+ expect(queue_duration_seconds).not_to receive(:observe)
+
+ subject.call(worker, job, :test) { nil }
+ end
+ end
+
+ context 'when error is raised' do
+ let(:job_status) { :fail }
+
+ it 'sets sidekiq_jobs_failed_total and reraises' do
+ expect(failed_total_metric).to receive(:increment).with(labels, 1)
+
+ expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
+ end
+ end
+
+ context 'when job is retried' do
+ let(:job) { { 'retry_count' => 1 } }
+
+ it 'sets sidekiq_jobs_retried_total metric' do
+ expect(retried_total_metric).to receive(:increment)
+
+ subject.call(worker, job, :test) { nil }
+ end
+ end
+ end
+ end
- middleware
- end
- end
+ context "with prometheus integrated" do
+ describe '#call' do
+ it 'yields block' do
+ expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
+ end
- it 'ignore user execution when measured 0' do
- allow(completion_seconds_metric).to receive(:observe)
+ context 'when error is raised' do
+ let(:job_status) { :fail }
- expect(user_execution_seconds_metric).not_to receive(:observe)
- end
+ it 'sets sidekiq_jobs_failed_total and reraises' do
+ expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
+ end
+ end
+ end
+ end
+ end
- describe '#call' do
- let(:worker) { double(:worker) }
+ context "when workers are not attributed" do
+ class TestNonAttributedWorker
+ include Sidekiq::Worker
+ end
+ let(:worker) { TestNonAttributedWorker.new }
+ let(:labels) { default_labels }
- let(:job) { {} }
- let(:job_status) { :done }
- let(:labels) { { queue: :test } }
- let(:labels_with_job_status) { { queue: :test, job_status: job_status } }
+ it_behaves_like "a metrics middleware"
+ end
- let(:thread_cputime_before) { 1 }
- let(:thread_cputime_after) { 2 }
- let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before }
+ context "when workers are attributed" do
+ def create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, category)
+ Class.new do
+ include Sidekiq::Worker
+ include WorkerAttributes
+
+ latency_sensitive_worker! if latency_sensitive
+ worker_has_external_dependencies! if external_dependencies
+ worker_resource_boundary resource_boundary unless resource_boundary == :unknown
+ feature_category category unless category.nil?
+ end
+ end
- let(:monotonic_time_before) { 11 }
- let(:monotonic_time_after) { 20 }
- let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
+ let(:latency_sensitive) { false }
+ let(:external_dependencies) { false }
+ let(:resource_boundary) { :unknown }
+ let(:feature_category) { nil }
+ let(:worker_class) { create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, feature_category) }
+ let(:worker) { worker_class.new }
- let(:queue_duration_for_job) { 0.01 }
+ context "latency sensitive" do
+ let(:latency_sensitive) { true }
+ let(:labels) { default_labels.merge(latency_sensitive: "yes") }
- before do
- allow(middleware).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
- allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
- allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
+ it_behaves_like "a metrics middleware"
+ end
- expect(running_jobs_metric).to receive(:increment).with(labels, 1)
- expect(running_jobs_metric).to receive(:increment).with(labels, -1)
+ context "external dependencies" do
+ let(:external_dependencies) { true }
+ let(:labels) { default_labels.merge(external_dependencies: "yes") }
- expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job
- expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration)
- expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration)
- end
+ it_behaves_like "a metrics middleware"
+ end
- it 'yields block' do
- expect { |b| middleware.call(worker, job, :test, &b) }.to yield_control.once
- end
+ context "cpu boundary" do
+ let(:resource_boundary) { :cpu }
+ let(:labels) { default_labels.merge(boundary: "cpu") }
- it 'sets queue specific metrics' do
- middleware.call(worker, job, :test) { nil }
- end
+ it_behaves_like "a metrics middleware"
+ end
- context 'when job_duration is not available' do
- let(:queue_duration_for_job) { nil }
+ context "memory boundary" do
+ let(:resource_boundary) { :memory }
+ let(:labels) { default_labels.merge(boundary: "memory") }
- it 'does not set the queue_duration_seconds histogram' do
- middleware.call(worker, job, :test) { nil }
+ it_behaves_like "a metrics middleware"
end
- end
- context 'when job is retried' do
- let(:job) { { 'retry_count' => 1 } }
+ context "feature category" do
+ let(:feature_category) { :authentication }
+ let(:labels) { default_labels.merge(feature_category: "authentication") }
- it 'sets sidekiq_jobs_retried_total metric' do
- expect(retried_total_metric).to receive(:increment)
-
- middleware.call(worker, job, :test) { nil }
+ it_behaves_like "a metrics middleware"
end
- end
-
- context 'when error is raised' do
- let(:job_status) { :fail }
- it 'sets sidekiq_jobs_failed_total and reraises' do
- expect(failed_total_metric).to receive(:increment).with(labels, 1)
+ context "combined" do
+ let(:latency_sensitive) { true }
+ let(:external_dependencies) { true }
+ let(:resource_boundary) { :cpu }
+ let(:feature_category) { :authentication }
+ let(:labels) { default_labels.merge(latency_sensitive: "yes", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") }
- expect { middleware.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
+ it_behaves_like "a metrics middleware"
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index f7bef9e71e2..4a6a9026f77 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -852,4 +852,77 @@ describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
+
+ describe 'release scopes' do
+ let_it_be(:project) { create(:project) }
+
+ let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
+ let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
+ let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
+ let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
+
+ let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
+ let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
+ let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
+ let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
+ let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
+ let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
+
+ let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
+ let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
+ let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
+ let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
+ let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
+ let_it_be(:issue_6) { create(:issue, project: project) }
+
+ let_it_be(:items) { Issue.all }
+
+ describe '#without_release' do
+ it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
+ expect(items.without_release).to contain_exactly(issue_5, issue_6)
+ end
+ end
+
+ describe '#any_release' do
+ it 'returns all issues tied to a release' do
+ expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
+ end
+ end
+
+ describe '#with_release' do
+ it 'returns the issues tied a specfic release' do
+ expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
+ end
+
+ context 'when a release has a milestone with one issue and another one with no issue' do
+ it 'returns that one issue' do
+ expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
+ end
+
+ context 'when the milestone with no issue is added as a filter' do
+ it 'returns an empty list' do
+ expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
+ end
+ end
+
+ context 'when the milestone with the issue is added as a filter' do
+ it 'returns this issue' do
+ expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
+ end
+ end
+ end
+
+ context 'when there is no issue under a specific release' do
+ it 'returns no issue' do
+ expect(items.with_release('v4.0', project.id)).to be_empty
+ end
+ end
+
+ context 'when a non-existent release tag is passed in' do
+ it 'returns no issue' do
+ expect(items.with_release('v999.0', project.id)).to be_empty
+ end
+ end
+ end
+ end
end