Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-26 12:09:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-26 12:09:24 +0300
commit26bba9525deb5e9d05fd29cf5b286e7a65d1c791 (patch)
tree5c4b2157f5c1fc49f6720339a32fcb69df2e43bb
parent2d40635435ab225e16494b559ae030da660e91e8 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/access_tokens/index.js16
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue13
-rw-r--r--app/assets/javascripts/boards/mixins/board_new_issue.js6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js4
-rw-r--r--app/assets/javascripts/ensure_data.js56
-rw-r--r--app/assets/javascripts/lib/utils/forms.js94
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/views/shared/access_tokens/_form.html.haml4
-rw-r--r--app/workers/concerns/worker_attributes.rb33
-rw-r--r--changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml5
-rw-r--r--changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml5
-rw-r--r--config/feature_flags/development/migrate_delayed_project_removal.yml8
-rw-r--r--doc/development/usage_ping/dictionary.md28
-rw-r--r--lib/gitlab/instrumentation_helper.rb1
-rw-r--r--lib/gitlab/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/usage_data_queries.rb8
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/pipeline/show.rb20
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb112
-rw-r--r--scripts/review_apps/base-config.yaml8
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb1
-rw-r--r--spec/features/boards/boards_spec.rb94
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/boards/multi_select_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb24
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb2
-rw-r--r--spec/features/boards/sidebar_due_date_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb2
-rw-r--r--spec/features/boards/sidebar_milestones_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/boards/sidebar_subscription_spec.rb2
-rw-r--r--spec/features/boards/sidebar_time_tracking_spec.rb2
-rw-r--r--spec/features/boards/sub_group_project_spec.rb3
-rw-r--r--spec/frontend/access_tokens/index_spec.js20
-rw-r--r--spec/frontend/boards/stores/actions_spec.js7
-rw-r--r--spec/frontend/lib/utils/forms_spec.js163
-rw-r--r--spec/frontend/vue_shared/components/ensure_data_spec.js145
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb12
-rw-r--r--spec/support/helpers/next_instance_of.rb15
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb74
43 files changed, 890 insertions, 122 deletions
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 43d56295f78..7f5f0403de6 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -1,20 +1,10 @@
import Vue from 'vue';
import createFlash from '~/flash';
+import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
-const getInputAttrs = (el) => {
- const input = el.querySelector('input');
-
- return {
- id: input.id,
- name: input.name,
- value: input.value,
- placeholder: input.placeholder,
- };
-};
-
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
@@ -22,7 +12,7 @@ export const initExpiresAtField = () => {
return null;
}
- const inputAttrs = getInputAttrs(el);
+ const { expiresAt: inputAttrs } = parseRailsFormFields(el);
return new Vue({
el,
@@ -43,7 +33,7 @@ export const initProjectsField = () => {
return null;
}
- const inputAttrs = getInputAttrs(el);
+ const { projects: inputAttrs } = parseRailsFormFields(el);
if (window.gon.features.personalAccessTokensScopedToProjects) {
return new Promise((resolve) => {
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 6ccaec4a633..ca66ad6934a 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -328,6 +328,7 @@ export default {
<div
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
+ data-testid="issue-count-badge"
:class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader,
'gl-p-0': list.collapsed,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index a81c28733cd..944eb25bf7e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -2,8 +2,8 @@
import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
@@ -17,8 +17,8 @@ export default {
ProjectSelect,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
- inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
+ mixins: [BoardNewIssueMixin],
+ inject: ['groupId'],
props: {
list: {
type: Object,
@@ -53,14 +53,11 @@ export default {
submit(e) {
e.preventDefault();
+ const { title } = this;
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
- const weight = this.weightFeatureAvailable ? this.boardWeight : undefined;
-
- const { title } = this;
-
eventHub.$emit(`scroll-board-list-${this.list.id}`);
return this.addListNewIssue({
@@ -70,7 +67,7 @@ export default {
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.selectedProject.fullPath,
- weight: weight >= 0 ? weight : null,
+ ...this.extraIssueInput(),
},
list: this.list,
}).then(() => {
diff --git a/app/assets/javascripts/boards/mixins/board_new_issue.js b/app/assets/javascripts/boards/mixins/board_new_issue.js
new file mode 100644
index 00000000000..d4b74544735
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/board_new_issue.js
@@ -0,0 +1,6 @@
+export default {
+ // EE-only
+ methods: {
+ extraIssueInput: () => {},
+ },
+};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index d1a6ce9c153..bf67328bbe6 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -327,8 +327,8 @@ export default {
commit(types.RESET_ISSUES);
},
- moveItem: ({ dispatch }) => {
- dispatch('moveIssue');
+ moveItem: ({ dispatch }, payload) => {
+ dispatch('moveIssue', payload);
},
moveIssue: (
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
new file mode 100644
index 00000000000..5b4d1afc9d0
--- /dev/null
+++ b/app/assets/javascripts/ensure_data.js
@@ -0,0 +1,56 @@
+import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
+import { GlEmptyState } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { __ } from '~/locale';
+
+const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
+const ERROR_FETCHING_DATA_DESCRIPTION = __(
+ 'Please try and refresh the page. If the problem persists please contact support.',
+);
+
+/**
+ * This function takes a Component and extends it with data from the `parseData` function.
+ * The data will be made available through `props` and `proivde`.
+ * If the `parseData` throws, the `GlEmptyState` will be returned.
+ * @param {Component} Component a component to render
+ * @param {Object} options
+ * @param {Function} options.parseData a function to parse `data`
+ * @param {Object} options.data an object to pass to `parseData`
+ * @param {Boolean} options.shouldLog to tell whether to log any thrown error by `parseData` to Sentry
+ * @param {Object} options.props to override passed `props` data
+ * @param {Object} options.provide to override passed `provide` data
+ * @param {*} ...options the remaining options will be passed as properties to `createElement`
+ * @return {Component} a Vue component to render, either the GlEmptyState or the extended Component
+ */
+export default function ensureData(Component, options = {}) {
+ const { parseData, data, shouldLog = false, props, provide, ...rest } = options;
+ try {
+ const parsedData = parseData(data);
+ return {
+ provide: { ...parsedData, ...provide },
+ render(createElement) {
+ return createElement(Component, {
+ props: { ...parsedData, ...props },
+ ...rest,
+ });
+ },
+ };
+ } catch (error) {
+ if (shouldLog) {
+ Sentry.captureException(error);
+ }
+
+ return {
+ functional: true,
+ render(createElement) {
+ return createElement(GlEmptyState, {
+ props: {
+ title: ERROR_FETCHING_DATA_HEADER,
+ description: ERROR_FETCHING_DATA_DESCRIPTION,
+ svgPath: `data:image/svg+xml;utf8,${encodeURIComponent(emptySvg)}`,
+ },
+ });
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index 52e1323412d..b58aef15dda 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -1,3 +1,5 @@
+import { convertToCamelCase } from '~/lib/utils/text_utility';
+
export const serializeFormEntries = (entries) =>
entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {});
@@ -51,3 +53,95 @@ export const serializeFormObject = (form) =>
return acc;
}, []),
);
+
+/**
+ * Parse inputs of HTML forms generated by Rails.
+ *
+ * This can be helpful when mounting Vue components within Rails forms.
+ *
+ * If called with an HTML element like:
+ *
+ * ```html
+ * <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
+ * <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contactInfoPhone">
+ * <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests">
+ * <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests">
+ * ```
+ *
+ * It will return an object like:
+ *
+ * ```javascript
+ * {
+ * contactInfoEmail: {
+ * name: 'user[contact_info][email]',
+ * id: 'user_contact_info_email',
+ * value: 'foo@bar.com',
+ * placeholder: 'Email',
+ * },
+ * contactInfoPhone: {
+ * name: 'user[contact_info][phone]',
+ * id: 'user_contact_info_phone',
+ * value: '(123) 456-7890',
+ * placeholder: 'Phone',
+ * },
+ * interests: [
+ * {
+ * name: 'user[interests][]',
+ * id: 'user_interests_vue',
+ * value: 'Vue',
+ * checked: true,
+ * },
+ * {
+ * name: 'user[interests][]',
+ * id: 'user_interests_graphql',
+ * value: 'GraphQL',
+ * checked: false,
+ * },
+ * ],
+ * }
+ * ```
+ *
+ * @param {HTMLInputElement} mountEl
+ * @returns {Object} object with form fields data.
+ */
+export const parseRailsFormFields = (mountEl) => {
+ if (!mountEl) {
+ throw new TypeError('`mountEl` argument is required');
+ }
+
+ const inputs = mountEl.querySelectorAll('[name]');
+
+ return [...inputs].reduce((accumulator, input) => {
+ const fieldName = input.dataset.jsName;
+
+ if (!fieldName) {
+ return accumulator;
+ }
+
+ const fieldNameCamelCase = convertToCamelCase(fieldName);
+ const { id, placeholder, name, value, type, checked } = input;
+ const attributes = {
+ name,
+ id,
+ value,
+ ...(placeholder && { placeholder }),
+ };
+
+ // Store radio buttons and checkboxes as an array so they can be
+ // looped through and rendered in Vue
+ if (['radio', 'checkbox'].includes(type)) {
+ return {
+ ...accumulator,
+ [fieldNameCamelCase]: [
+ ...(accumulator[fieldNameCamelCase] || []),
+ { ...attributes, checked },
+ ],
+ };
+ }
+
+ return {
+ ...accumulator,
+ [fieldNameCamelCase]: attributes,
+ };
+ }, {});
+};
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 69a609b6fd8..5a8090fc4bd 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:add_issues_button)
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index 9709ad8428e..88c24a27497 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -23,7 +23,7 @@
= render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
.js-access-tokens-expires-at
- = f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off'
+ = f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
.form-group
= f.label :scopes, _('Scopes'), class: 'label-bold'
@@ -31,7 +31,7 @@
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
- %input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' }
+ %input{ type: 'hidden', name: 'personal_access_token[projects]', id: 'personal_access_token_projects', data: { js_name: 'projects' } }
.gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' }
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 042508d08f2..6f99fd089ac 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -11,6 +11,8 @@ module WorkerAttributes
# Urgencies that workers can declare through the `urgencies` attribute
VALID_URGENCIES = [:high, :low, :throttled].freeze
+ VALID_DATA_CONSISTENCIES = [:always, :sticky, :delayed].freeze
+
NAMESPACE_WEIGHTS = {
auto_devops: 2,
auto_merge: 3,
@@ -69,6 +71,35 @@ module WorkerAttributes
class_attributes[:urgency] || :low
end
+ def data_consistency(data_consistency, feature_flag: nil)
+ raise ArgumentError, "Invalid data consistency: #{data_consistency}" unless VALID_DATA_CONSISTENCIES.include?(data_consistency)
+ raise ArgumentError, 'Data consistency is already set' if class_attributes[:data_consistency]
+
+ class_attributes[:data_consistency_feature_flag] = feature_flag if feature_flag
+ class_attributes[:data_consistency] = data_consistency
+
+ validate_worker_attributes!
+ end
+
+ def validate_worker_attributes!
+ # Since the deduplication should always take into account the latest binary replication pointer into account,
+ # not the first one, the deduplication will not work with sticky or delayed.
+ # Follow up issue to improve this: https://gitlab.com/gitlab-org/gitlab/-/issues/325291
+ if idempotent? && get_data_consistency != :always
+ raise ArgumentError, "Class can't be marked as idempotent if data_consistency is not set to :always"
+ end
+ end
+
+ def get_data_consistency
+ class_attributes[:data_consistency] || :always
+ end
+
+ def get_data_consistency_feature_flag_enabled?
+ return true unless class_attributes[:data_consistency_feature_flag]
+
+ Feature.enabled?(class_attributes[:data_consistency_feature_flag], default_enabled: :yaml)
+ end
+
# Set this attribute on a job when it will call to services outside of the
# application, such as 3rd party applications, other k8s clusters etc See
# doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for
@@ -96,6 +127,8 @@ module WorkerAttributes
def idempotent!
class_attributes[:idempotent] = true
+
+ validate_worker_attributes!
end
def idempotent?
diff --git a/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml b/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml
new file mode 100644
index 00000000000..33e2ffa23f5
--- /dev/null
+++ b/changelogs/unreleased/325884-usage-data-count-start-finish-problems.yml
@@ -0,0 +1,5 @@
+---
+title: Fix usage data count start/finish export issue
+merge_request: 57403
+author:
+type: fixed
diff --git a/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml b/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml
new file mode 100644
index 00000000000..16e76dbc957
--- /dev/null
+++ b/changelogs/unreleased/326057-rspec-feature-flag-failure-for-migrate_delayed_project_removal.yml
@@ -0,0 +1,5 @@
+---
+title: Removed migrate_delayed_project_removal feature flag
+merge_request: 57541
+author:
+type: other
diff --git a/config/feature_flags/development/migrate_delayed_project_removal.yml b/config/feature_flags/development/migrate_delayed_project_removal.yml
deleted file mode 100644
index 2d4a7ef762e..00000000000
--- a/config/feature_flags/development/migrate_delayed_project_removal.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: migrate_delayed_project_removal
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53916
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300207
-milestone: '13.9'
-type: development
-group: group::access
-default_enabled: true
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 571f5149d24..87ac8109d3c 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -9914,7 +9914,7 @@ Counts of MAU adding epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314215451_g_project_management_users_creating_epic_notes_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9926,7 +9926,7 @@ Counts of WAU adding epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314231518_g_project_management_users_creating_epic_notes_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9938,7 +9938,7 @@ Counts of MAU destroying epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315034808_g_project_management_users_destroying_epic_notes_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9950,7 +9950,7 @@ Counts of WAU destroying epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315034846_g_project_management_users_destroying_epic_notes_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9962,7 +9962,7 @@ Counts of MAU setting epic start date as fixed
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055624_g_project_management_users_setting_epic_start_date_as_fixed_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9974,7 +9974,7 @@ Counts of WAU setting epic start date as fixed
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315054905_g_project_management_users_setting_epic_start_date_as_fixed_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9986,7 +9986,7 @@ Counts of MAU setting epic start date as inherited
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055439_g_project_management_users_setting_epic_start_date_as_inherited_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -9998,7 +9998,7 @@ Counts of WAU setting epic start date as inherited
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315055342_g_project_management_users_setting_epic_start_date_as_inherited_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10010,7 +10010,7 @@ Counts of MAU changing epic descriptions
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312102051_g_project_management_users_updating_epic_descriptions_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10022,7 +10022,7 @@ Counts of WAU changing epic descriptions
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101753_g_project_management_users_updating_epic_descriptions_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10034,7 +10034,7 @@ Counts of MAU updating epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314234202_g_project_management_users_updating_epic_notes_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10046,7 +10046,7 @@ Counts of WAU updating epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314234041_g_project_management_users_updating_epic_notes_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10058,7 +10058,7 @@ Counts of MAU changing epic titles
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312101935_g_project_management_users_updating_epic_titles_monthly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
@@ -10070,7 +10070,7 @@ Counts of WAU changing epic titles
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101826_g_project_management_users_updating_epic_titles_weekly.yml)
-Group: `group:product planning`
+Group: `group::product planning`
Status: `implemented`
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 61de6b02453..420803a17e7 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -16,6 +16,7 @@ module Gitlab
:elasticsearch_calls,
:elasticsearch_duration_s,
:elasticsearch_timed_out_count,
+ :worker_data_consistency,
*::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
*::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index a2696e17078..563a105484d 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -43,3 +43,5 @@ module Gitlab
end
end
end
+
+Gitlab::SidekiqMiddleware.singleton_class.prepend_if_ee('EE::Gitlab::SidekiqMiddleware')
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
index c00e7a2aa13..dc37e2fef1d 100644
--- a/lib/gitlab/usage_data_queries.rb
+++ b/lib/gitlab/usage_data_queries.rb
@@ -5,11 +5,11 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091
class UsageDataQueries < UsageData
class << self
- def count(relation, column = nil, *rest)
+ def count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column)
end
- def distinct_count(relation, column = nil, *rest)
+ def distinct_count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column, :distinct)
end
@@ -21,14 +21,14 @@ module Gitlab
end
end
- def sum(relation, column, *rest)
+ def sum(relation, column, *args, **kwargs)
relation.select(relation.all.table[column].sum).to_sql
end
# For estimated distinct count use exact query instead of hll
# buckets query, because it can't be used to obtain estimations without
# supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter
- def estimate_batch_distinct_count(relation, column = nil, *rest)
+ def estimate_batch_distinct_count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column, :distinct)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index da149087253..18f064a6bb8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8707,6 +8707,9 @@ msgstr ""
msgid "Could not find iteration"
msgstr ""
+msgid "Could not get the data properly"
+msgstr ""
+
msgid "Could not load the user chart. Please refresh the page to try again."
msgstr ""
@@ -23056,6 +23059,9 @@ msgstr ""
msgid "Please try again"
msgstr ""
+msgid "Please try and refresh the page. If the problem persists please contact support."
+msgstr ""
+
msgid "Please type %{phrase_code} to proceed or close this modal to cancel."
msgstr ""
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index 994b1c02a3d..c5887b84be6 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -68,20 +68,30 @@ module QA
end
end
- def has_child_pipeline?
- has_element? :child_pipeline
+ def has_child_pipeline?(title: nil)
+ title ? find_child_pipeline_by_title(title) : has_element?(:child_pipeline)
end
def has_no_child_pipeline?
- has_no_element? :child_pipeline
+ has_no_element?(:child_pipeline)
end
def click_job(job_name)
click_element(:job_link, Project::Job::Show, text: job_name)
end
- def expand_child_pipeline
- within_element(:child_pipeline) do
+ def child_pipelines
+ all_elements(:child_pipeline, minimum: 1)
+ end
+
+ def find_child_pipeline_by_title(title)
+ child_pipelines.find { |pipeline| pipeline[:title].include?(title) }
+ end
+
+ def expand_child_pipeline(title: nil)
+ child_pipeline = title ? find_child_pipeline_by_title(title) : child_pipelines.first
+
+ within_element_by_index(:child_pipeline, child_pipelines.index(child_pipeline)) do
click_element(:expand_pipeline_button)
end
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb
new file mode 100644
index 00000000000..d87fa0f5127
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_matrix_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'faker'
+
+module QA
+ RSpec.describe 'Verify', :runner do
+ describe 'Trigger matrix' do
+ let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-with-pipeline'
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.project = project
+ runner.name = executor
+ runner.tags = [executor]
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ add_ci_files
+ project.visit!
+ Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'succeeded')
+ end
+
+ after do
+ runner.remove_via_api!
+ project.remove_via_api!
+ end
+
+ it 'creates 2 trigger jobs and passes corresponding matrix variables', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1732' do
+ Page::Project::Pipeline::Show.perform do |parent_pipeline|
+ trigger_title1 = 'deploy: [ovh, monitoring]'
+ trigger_title2 = 'deploy: [ovh, app]'
+
+ aggregate_failures 'Creates two child pipelines' do
+ expect(parent_pipeline).to have_child_pipeline(title: trigger_title1)
+ expect(parent_pipeline).to have_child_pipeline(title: trigger_title2)
+ end
+
+ # Only check output of one of the child pipelines, should be sufficient
+ parent_pipeline.expand_child_pipeline(title: trigger_title1)
+ parent_pipeline.click_job('test_vars')
+ end
+
+ Page::Project::Job::Show.perform do |show|
+ Support::Waiter.wait_until { show.successful? }
+
+ aggregate_failures 'Job output has the correct variables' do
+ expect(show.output).to have_content('ovh')
+ expect(show.output).to have_content('monitoring')
+ end
+ end
+ end
+
+ private
+
+ def add_ci_files
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add parent and child pipelines CI files.'
+ commit.add_files(
+ [
+ child_ci_file,
+ parent_ci_file
+ ]
+ )
+ end
+ end
+
+ def parent_ci_file
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ test:
+ stage: test
+ script: echo test
+ tags: [#{executor}]
+
+ deploy:
+ stage: deploy
+ trigger:
+ include: child.yml
+ parallel:
+ matrix:
+ - PROVIDER: ovh
+ STACK: [monitoring, app]
+
+ YAML
+ }
+ end
+
+ def child_ci_file
+ {
+ file_path: 'child.yml',
+ content: <<~YAML
+ test_vars:
+ script:
+ - echo $PROVIDER
+ - echo $STACK
+ tags: [#{executor}]
+ YAML
+ }
+ end
+ end
+ end
+end
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index cf55fca7452..5b4b557c305 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -70,10 +70,10 @@ gitlab:
resources:
requests:
cpu: 746m
- memory: 1873M
+ memory: 2809M
limits:
cpu: 1119m
- memory: 2809M
+ memory: 4214M
deployment:
readinessProbe:
initialDelaySeconds: 5 # Default is 0
@@ -83,10 +83,10 @@ gitlab:
resources:
requests:
cpu: 400m
- memory: 50M
+ memory: 75M
limits:
cpu: 600m
- memory: 75M
+ memory: 113M
readinessProbe:
initialDelaySeconds: 5 # Default is 0
periodSeconds: 15 # Default is 10
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 8d0fa3e023b..ff9e0b9d054 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
+ stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 2392f9d2f8a..ab544022bff 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards', :js do
+RSpec.describe 'Project issue boards', :js do
include DragTo
include MobileHelpers
@@ -23,7 +23,7 @@ RSpec.describe 'Issue Boards', :js do
context 'no lists' do
before do
- visit project_board_path(project, board)
+ visit_project_board_path_without_query_limit(project, board)
end
it 'creates default lists' do
@@ -52,6 +52,7 @@ RSpec.describe 'Issue Boards', :js do
let_it_be(:a_plus) { create(:label, project: project, name: 'A+') }
let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
+ let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
@@ -68,7 +69,7 @@ RSpec.describe 'Issue Boards', :js do
before do
stub_feature_flags(board_new_list: false)
- visit project_board_path(project, board)
+ visit_project_board_path_without_query_limit(project, board)
wait_for_requests
@@ -121,7 +122,8 @@ RSpec.describe 'Issue Boards', :js do
context 'with the NOT queries feature flag disabled' do
before do
stub_feature_flags(not_issuable_queries: false)
- visit project_board_path(project, board)
+
+ visit_project_board_path_without_query_limit(project, board)
end
it 'does not have the != option' do
@@ -141,7 +143,8 @@ RSpec.describe 'Issue Boards', :js do
context 'with the NOT queries feature flag enabled' do
before do
stub_feature_flags(not_issuable_queries: true)
- visit project_board_path(project, board)
+
+ visit_project_board_path_without_query_limit(project, board)
end
it 'does not have the != option' do
@@ -171,8 +174,7 @@ RSpec.describe 'Issue Boards', :js do
it 'infinite scrolls list' do
create_list(:labeled_issue, 50, project: project, labels: [planning])
- visit project_board_path(project, board)
- wait_for_requests
+ visit_project_board_path_without_query_limit(project, board)
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('58')
@@ -180,15 +182,19 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_content('Showing 20 of 58 issues')
find('.board .board-list')
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
- wait_for_requests
+
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 58 issues')
find('.board .board-list')
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
- wait_for_requests
+
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
expect(page).to have_selector('.board-card', count: 58)
expect(page).to have_content('Showing all issues')
@@ -236,13 +242,13 @@ RSpec.describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(development.title)
- expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ expect(find('.board:nth-child(3)')).to have_content(planning.title)
# Make sure list positions are preserved after a reload
- visit project_board_path(project, board)
+ visit_project_board_path_without_query_limit(project, board)
expect(find('.board:nth-child(2)')).to have_content(development.title)
- expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ expect(find('.board:nth-child(3)')).to have_content(planning.title)
end
it 'dragging does not duplicate list' do
@@ -254,7 +260,8 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_selector(selector, text: development.title, count: 1)
end
- it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
+ # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323551
+ xit 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7)
@@ -467,14 +474,16 @@ RSpec.describe 'Issue Boards', :js do
end
it 'removes filtered labels' do
- set_filter("label", testing.title)
- click_filter_link(testing.title)
- submit_filter
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
- wait_for_board_cards(2, 1)
+ wait_for_board_cards(2, 1)
- find('.clear-search').click
- submit_filter
+ find('.clear-search').click
+ submit_filter
+ end
wait_for_board_cards(2, 8)
end
@@ -484,7 +493,9 @@ RSpec.describe 'Issue Boards', :js do
set_filter("label", testing.title)
click_filter_link(testing.title)
- submit_filter
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ submit_filter
+ end
wait_for_requests
@@ -494,13 +505,18 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_content('Showing 20 of 51 issues')
find('.board .board-list')
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues')
find('.board .board-list')
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
expect(page).to have_selector('.board-card', count: 51)
expect(page).to have_content('Showing all issues')
@@ -569,7 +585,7 @@ RSpec.describe 'Issue Boards', :js do
context 'keyboard shortcuts' do
before do
- visit project_board_path(project, board)
+ visit_project_board_path_without_query_limit(project, board)
wait_for_requests
end
@@ -617,15 +633,19 @@ RSpec.describe 'Issue Boards', :js do
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true)
# ensure there is enough horizontal space for four boards
- resize_window(2000, 800)
-
- drag_to(selector: selector,
- scrollable: '#board-app',
- list_from_index: list_from_index,
- from_index: from_index,
- to_index: to_index,
- list_to_index: list_to_index,
- perform_drop: perform_drop)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ resize_window(2000, 800)
+
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index,
+ perform_drop: perform_drop)
+ end
+
+ wait_for_requests
end
def wait_for_board_cards(board_number, expected_cards)
@@ -666,4 +686,10 @@ RSpec.describe 'Issue Boards', :js do
accept_confirm { find('[data-testid="remove-list"]').click }
end
end
+
+ def visit_project_board_path_without_query_limit(project, board)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ visit project_board_path(project, board)
+ end
+ end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 5aeb9eb5e50..d2b7686a9e2 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe 'Issue Boards add issue modal filtering', :js do
let!(:issue1) { create(:issue, project: project) }
before do
+ stub_feature_flags(graphql_board_lists: false)
+ stub_feature_flags(add_issues_button: true)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
index 162455f75e6..ca322355b8f 100644
--- a/spec/features/boards/multi_select_spec.rb
+++ b/spec/features/boards/multi_select_spec.rb
@@ -41,6 +41,10 @@ RSpec.describe 'Multi Select Issue', :js do
before do
project.add_maintainer(user)
+ # multi-drag disabled with feature flag for now
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/289797
+ stub_feature_flags(graphql_board_lists: false)
+
sign_in(user)
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f434ea0c66f..4d419f89aa3 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe 'Issue Boards new issue', :js do
- let(:project) { create(:project, :public) }
- let(:board) { create(:board, project: project) }
- let!(:list) { create(:list, board: board, position: 0) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:backlog_list) { create(:backlog_list, board: board) }
+ let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
+ let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
+ let_it_be(:user) { create(:user) }
context 'authorized user' do
before do
@@ -15,6 +17,7 @@ RSpec.describe 'Issue Boards new issue', :js do
sign_in(user)
visit project_board_path(project, board)
+
wait_for_requests
expect(page).to have_selector('.board', count: 3)
@@ -70,11 +73,12 @@ RSpec.describe 'Issue Boards new issue', :js do
issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference)
- expect(page).to have_link(issue.title, href: issue_path(issue))
+ expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
end
end
- it 'shows sidebar when creating new issue' do
+ # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323446
+ xit 'shows sidebar when creating new issue' do
page.within(first('.board')) do
find('.issue-count-badge-add-button').click
end
@@ -101,12 +105,16 @@ RSpec.describe 'Issue Boards new issue', :js do
wait_for_requests
+ page.within(first('.board')) do
+ find('.board-card').click
+ end
+
page.within(first('.issue-boards-sidebar')) do
- find('.labels .edit-link').click
+ find('.labels [data-testid="edit-button"]').click
wait_for_requests
- expect(page).to have_selector('.labels .dropdown-content li a')
+ expect(page).to have_selector('.labels-select-contents-list .dropdown-content li a')
end
end
end
diff --git a/spec/features/boards/reload_boards_on_browser_back_spec.rb b/spec/features/boards/reload_boards_on_browser_back_spec.rb
index 181cbcc9811..3530be20009 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js do
issue = project.issues.find_by_title('issue should be shown')
expect(page).to have_content(issue.to_reference)
- expect(page).to have_link(issue.title, href: issue_path(issue))
+ expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
end
end
end
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index 82383ece2d3..6835721bb4d 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_due_date_spec.rb b/spec/features/boards/sidebar_due_date_spec.rb
index f2d51fb56a7..52ec51c317e 100644
--- a/spec/features/boards/sidebar_due_date_spec.rb
+++ b/spec/features/boards/sidebar_due_date_spec.rb
@@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar due date', :js do
end
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index d6e908698c6..37de561e689 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -18,6 +18,8 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_milestones_spec.rb b/spec/features/boards/sidebar_milestones_spec.rb
index d4f130ba3ee..d815d60d5b0 100644
--- a/spec/features/boards/sidebar_milestones_spec.rb
+++ b/spec/features/boards/sidebar_milestones_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 4c93707cc44..45fe5ab8376 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe 'Project issue boards sidebar', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_subscription_spec.rb b/spec/features/boards/sidebar_subscription_spec.rb
index 77766e909f9..598fec7514e 100644
--- a/spec/features/boards/sidebar_subscription_spec.rb
+++ b/spec/features/boards/sidebar_subscription_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar subscription', :js do
let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_time_tracking_spec.rb b/spec/features/boards/sidebar_time_tracking_spec.rb
index 0cdf1e9a787..3ac8b93692a 100644
--- a/spec/features/boards/sidebar_time_tracking_spec.rb
+++ b/spec/features/boards/sidebar_time_tracking_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe 'Project issue boards sidebar time tracking', :js do
let(:application_settings) { {} }
before do
+ stub_feature_flags(graphql_board_lists: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb
index cd3d61726f6..bde5f061a67 100644
--- a/spec/features/boards/sub_group_project_spec.rb
+++ b/spec/features/boards/sub_group_project_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe 'Sub-group project issue boards', :js do
wait_for_requests
end
- it 'creates new label from sidebar' do
+ # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/324290
+ xit 'creates new label from sidebar' do
find('.board-card').click
page.within '.labels' do
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index e3f17e21739..1d8ac7cec25 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -25,18 +25,22 @@ describe('access tokens', () => {
});
describe.each`
- initFunction | mountSelector | expectedComponent
- ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField}
- ${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField}
- `('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => {
+ initFunction | mountSelector | fieldName | expectedComponent
+ ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField}
+ ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
+ `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
describe('when mount element exists', () => {
+ const nameAttribute = `access_tokens[${fieldName}]`;
+ const idAttribute = `access_tokens_${fieldName}`;
+
beforeEach(() => {
const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector);
const input = document.createElement('input');
- input.setAttribute('name', 'foo-bar');
- input.setAttribute('id', 'foo-bar');
+ input.setAttribute('name', nameAttribute);
+ input.setAttribute('data-js-name', fieldName);
+ input.setAttribute('id', idAttribute);
input.setAttribute('placeholder', 'Foo bar');
input.setAttribute('value', '1,2');
@@ -57,8 +61,8 @@ describe('access tokens', () => {
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
- name: 'foo-bar',
- id: 'foo-bar',
+ name: nameAttribute,
+ id: idAttribute,
value: '1,2',
placeholder: 'Foo bar',
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index c0f91d3c629..7511a4ad2cb 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -639,10 +639,13 @@ describe('resetIssues', () => {
});
describe('moveItem', () => {
- it('should dispatch moveIssue action', () => {
+ it('should dispatch moveIssue action with payload', () => {
+ const payload = { mock: 'payload' };
+
testAction({
action: actions.moveItem,
- expectedActions: [{ type: 'moveIssue' }],
+ payload,
+ expectedActions: [{ type: 'moveIssue', payload }],
});
});
});
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index f65bd8ffe0c..123d36ac5d5 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -1,4 +1,9 @@
-import { serializeForm, serializeFormObject, isEmptyValue } from '~/lib/utils/forms';
+import {
+ serializeForm,
+ serializeFormObject,
+ isEmptyValue,
+ parseRailsFormFields,
+} from '~/lib/utils/forms';
describe('lib/utils/forms', () => {
const createDummyForm = (inputs) => {
@@ -135,4 +140,160 @@ describe('lib/utils/forms', () => {
});
});
});
+
+ describe('parseRailsFormFields', () => {
+ let mountEl;
+
+ beforeEach(() => {
+ mountEl = document.createElement('div');
+ mountEl.classList.add('js-foo-bar');
+ });
+
+ afterEach(() => {
+ mountEl = null;
+ });
+
+ it('parses fields generated by Rails and returns object with HTML attributes', () => {
+ mountEl.innerHTML = `
+ <input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
+ <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
+ <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contact_info_phone">
+ <input type="hidden" placeholder="Job title" value="" name="user[job_title]" id="user_job_title" data-js-name="jobTitle">
+ <textarea name="user[bio]" id="user_bio" data-js-name="bio">Foo bar</textarea>
+ <select name="user[timezone]" id="user_timezone" data-js-name="timezone">
+ <option value="utc+12">[UTC - 12] International Date Line West</option>
+ <option value="utc+11" selected>[UTC - 11] American Samoa</option>
+ </select>
+ <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests">
+ <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests">
+ <input type="radio" name="user[access_level]" value="regular" id="user_access_level_regular" data-js-name="accessLevel">
+ <input type="radio" name="user[access_level]" value="admin" id="user_access_level_admin" checked data-js-name="access_level">
+ <input name="user[private_profile]" type="hidden" value="0">
+ <input type="radio" name="user[private_profile]" id="user_private_profile" value="1" checked data-js-name="privateProfile">
+ <input name="user[email_notifications]" type="hidden" value="0">
+ <input type="radio" name="user[email_notifications]" id="user_email_notifications" value="1" data-js-name="emailNotifications">
+ `;
+
+ expect(parseRailsFormFields(mountEl)).toEqual({
+ name: {
+ name: 'user[name]',
+ id: 'user_name',
+ value: 'Administrator',
+ placeholder: 'Name',
+ },
+ contactInfoEmail: {
+ name: 'user[contact_info][email]',
+ id: 'user_contact_info_email',
+ value: 'foo@bar.com',
+ placeholder: 'Email',
+ },
+ contactInfoPhone: {
+ name: 'user[contact_info][phone]',
+ id: 'user_contact_info_phone',
+ value: '(123) 456-7890',
+ placeholder: 'Phone',
+ },
+ jobTitle: {
+ name: 'user[job_title]',
+ id: 'user_job_title',
+ value: '',
+ placeholder: 'Job title',
+ },
+ bio: {
+ name: 'user[bio]',
+ id: 'user_bio',
+ value: 'Foo bar',
+ },
+ timezone: {
+ name: 'user[timezone]',
+ id: 'user_timezone',
+ value: 'utc+11',
+ },
+ interests: [
+ {
+ name: 'user[interests][]',
+ id: 'user_interests_vue',
+ value: 'Vue',
+ checked: true,
+ },
+ {
+ name: 'user[interests][]',
+ id: 'user_interests_graphql',
+ value: 'GraphQL',
+ checked: false,
+ },
+ ],
+ accessLevel: [
+ {
+ name: 'user[access_level]',
+ id: 'user_access_level_regular',
+ value: 'regular',
+ checked: false,
+ },
+ {
+ name: 'user[access_level]',
+ id: 'user_access_level_admin',
+ value: 'admin',
+ checked: true,
+ },
+ ],
+ privateProfile: [
+ {
+ name: 'user[private_profile]',
+ id: 'user_private_profile',
+ value: '1',
+ checked: true,
+ },
+ ],
+ emailNotifications: [
+ {
+ name: 'user[email_notifications]',
+ id: 'user_email_notifications',
+ value: '1',
+ checked: false,
+ },
+ ],
+ });
+ });
+
+ it('returns an empty object if there are no inputs', () => {
+ expect(parseRailsFormFields(mountEl)).toEqual({});
+ });
+
+ it('returns an empty object if inputs do not have `name` attributes', () => {
+ mountEl.innerHTML = `
+ <input type="text" placeholder="Name" value="Administrator" id="user_name">
+ <input type="text" placeholder="Email" value="foo@bar.com" id="user_contact_info_email">
+ <input type="text" placeholder="Phone" value="(123) 456-7890" id="user_contact_info_phone">
+ `;
+
+ expect(parseRailsFormFields(mountEl)).toEqual({});
+ });
+
+ it('does not include field if `data-js-name` attribute is missing', () => {
+ mountEl.innerHTML = `
+ <input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
+ <input type="text" placeholder="Email" value="foo@bar.com" name="user[email]" id="email">
+ `;
+
+ expect(parseRailsFormFields(mountEl)).toEqual({
+ name: {
+ name: 'user[name]',
+ id: 'user_name',
+ value: 'Administrator',
+ placeholder: 'Name',
+ },
+ });
+ });
+
+ it('throws error if `mountEl` argument is not passed', () => {
+ expect(() => parseRailsFormFields()).toThrow(new TypeError('`mountEl` argument is required'));
+ });
+
+ it('throws error if `mountEl` argument is `null`', () => {
+ expect(() => parseRailsFormFields(null)).toThrow(
+ new TypeError('`mountEl` argument is required'),
+ );
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js
new file mode 100644
index 00000000000..eef8b452f5f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/ensure_data_spec.js
@@ -0,0 +1,145 @@
+import { GlEmptyState } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { mount } from '@vue/test-utils';
+import ensureData from '~/ensure_data';
+
+const mockData = { message: 'Hello there' };
+const defaultOptions = {
+ parseData: () => mockData,
+ data: mockData,
+};
+
+const MockChildComponent = {
+ inject: ['message'],
+ render(createElement) {
+ return createElement('h1', this.message);
+ },
+};
+
+const MockParentComponent = {
+ components: {
+ MockChildComponent,
+ },
+ props: {
+ message: {
+ type: String,
+ required: true,
+ },
+ otherProp: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ render(createElement) {
+ return createElement('div', [this.message, createElement(MockChildComponent)]);
+ },
+};
+
+describe('EnsureData', () => {
+ let wrapper;
+
+ function findEmptyState() {
+ return wrapper.findComponent(GlEmptyState);
+ }
+
+ function findChild() {
+ return wrapper.findComponent(MockChildComponent);
+ }
+ function findParent() {
+ return wrapper.findComponent(MockParentComponent);
+ }
+
+ function createComponent(options = defaultOptions) {
+ return mount(ensureData(MockParentComponent, options));
+ }
+
+ beforeEach(() => {
+ Sentry.captureException = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ Sentry.captureException.mockClear();
+ });
+
+ describe('when parseData throws', () => {
+ it('should render GlEmptyState', () => {
+ wrapper = createComponent({
+ parseData: () => {
+ throw new Error();
+ },
+ });
+
+ expect(findParent().exists()).toBe(false);
+ expect(findChild().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('should not log to Sentry when shouldLog=false (default)', () => {
+ wrapper = createComponent({
+ parseData: () => {
+ throw new Error();
+ },
+ });
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('should log to Sentry when shouldLog=true', () => {
+ const error = new Error('Error!');
+ wrapper = createComponent({
+ parseData: () => {
+ throw error;
+ },
+ shouldLog: true,
+ });
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ });
+ });
+
+ describe('when parseData succeeds', () => {
+ it('should render MockParentComponent and MockChildComponent', () => {
+ wrapper = createComponent();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findParent().exists()).toBe(true);
+ expect(findChild().exists()).toBe(true);
+ });
+
+ it('enables user to provide data to child components', () => {
+ wrapper = createComponent();
+
+ const childComponent = findChild();
+ expect(childComponent.text()).toBe(mockData.message);
+ });
+
+ it('enables user to override provide data', () => {
+ const message = 'Another message';
+ wrapper = createComponent({ ...defaultOptions, provide: { message } });
+
+ const childComponent = findChild();
+ expect(childComponent.text()).toBe(message);
+ });
+
+ it('enables user to pass props to parent component', () => {
+ wrapper = createComponent();
+
+ expect(findParent().props()).toMatchObject(mockData);
+ });
+
+ it('enables user to override props data', () => {
+ const props = { message: 'Another message', otherProp: true };
+ wrapper = createComponent({ ...defaultOptions, props });
+
+ expect(findParent().props()).toMatchObject(props);
+ });
+
+ it('should not log to Sentry when shouldLog=true', () => {
+ wrapper = createComponent({ ...defaultOptions, shouldLog: true });
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index a5c9cde4c37..bbfdb8f4c2c 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::InstrumentationHelper do
:elasticsearch_calls,
:elasticsearch_duration_s,
:elasticsearch_timed_out_count,
+ :worker_data_consistency,
:mem_objects,
:mem_bytes,
:mem_mallocs,
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 12eac643383..113df26cdca 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -11,12 +11,24 @@ RSpec.describe Gitlab::UsageDataQueries do
it 'returns the raw SQL' do
expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')
end
+
+ it 'does not mix a nil column with keyword arguments' do
+ expect(described_class).to receive(:raw_sql).with(User, nil)
+
+ described_class.count(User, start: 1, finish: 2)
+ end
end
describe '.distinct_count' do
it 'returns the raw SQL' do
expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
end
+
+ it 'does not mix a nil column with keyword arguments' do
+ expect(described_class).to receive(:raw_sql).with(Issue, nil, :distinct)
+
+ described_class.distinct_count(Issue, nil, start: 1, finish: 2)
+ end
end
describe '.redis_usage_data' do
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index a8e9ab2bafe..95d8936588c 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -2,25 +2,26 @@
module NextInstanceOf
def expect_next_instance_of(klass, *new_args, &blk)
- stub_new(expect(klass), nil, *new_args, &blk)
+ stub_new(expect(klass), nil, false, *new_args, &blk)
end
- def expect_next_instances_of(klass, number, *new_args, &blk)
- stub_new(expect(klass), number, *new_args, &blk)
+ def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk)
+ stub_new(expect(klass), number, ordered, *new_args, &blk)
end
def allow_next_instance_of(klass, *new_args, &blk)
- stub_new(allow(klass), nil, *new_args, &blk)
+ stub_new(allow(klass), nil, false, *new_args, &blk)
end
- def allow_next_instances_of(klass, number, *new_args, &blk)
- stub_new(allow(klass), number, *new_args, &blk)
+ def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk)
+ stub_new(allow(klass), number, ordered, *new_args, &blk)
end
private
- def stub_new(target, number, *new_args, &blk)
+ def stub_new(target, number, ordered = false, *new_args, &blk)
receive_new = receive(:new)
+ receive_new.ordered if ordered
receive_new.exactly(number).times if number
receive_new.with(*new_args) if new_args.any?
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
new file mode 100644
index 00000000000..a654ecbd3e2
--- /dev/null
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkerAttributes do
+ let(:worker) do
+ Class.new do
+ def self.name
+ "TestWorker"
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ describe '.data_consistency' do
+ context 'with valid data_consistency' do
+ it 'returns correct data_consistency' do
+ worker.data_consistency(:sticky)
+
+ expect(worker.get_data_consistency).to eq(:sticky)
+ end
+ end
+
+ context 'when data_consistency is not provided' do
+ it 'defaults to :always' do
+ expect(worker.get_data_consistency).to eq(:always)
+ end
+ end
+
+ context 'with invalid data_consistency' do
+ it 'raise exception' do
+ expect { worker.data_consistency(:invalid) }
+ .to raise_error('Invalid data consistency: invalid')
+ end
+ end
+
+ context 'when job is idempotent' do
+ context 'when data_consistency is not :always' do
+ it 'raise exception' do
+ worker.idempotent!
+
+ expect { worker.data_consistency(:sticky) }
+ .to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always")
+ end
+ end
+
+ context 'when feature_flag is provided' do
+ before do
+ stub_feature_flags(test_feature_flag: false)
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+ end
+
+ it 'returns correct feature flag value' do
+ worker.data_consistency(:sticky, feature_flag: :test_feature_flag)
+
+ expect(worker.get_data_consistency_feature_flag_enabled?).not_to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '.idempotent!' do
+ context 'when data consistency is not :always' do
+ it 'raise exception' do
+ worker.data_consistency(:sticky)
+
+ expect { worker.idempotent! }
+ .to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always")
+ end
+ end
+ end
+end