Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-26 09:09:24 +00:00
parent 2d40635435
commit 26bba9525d
43 changed files with 889 additions and 121 deletions

View file

@ -1,20 +1,10 @@
import Vue from 'vue'; import Vue from 'vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale'; import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue'; 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 = () => { export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at'); const el = document.querySelector('.js-access-tokens-expires-at');
@ -22,7 +12,7 @@ export const initExpiresAtField = () => {
return null; return null;
} }
const inputAttrs = getInputAttrs(el); const { expiresAt: inputAttrs } = parseRailsFormFields(el);
return new Vue({ return new Vue({
el, el,
@ -43,7 +33,7 @@ export const initProjectsField = () => {
return null; return null;
} }
const inputAttrs = getInputAttrs(el); const { projects: inputAttrs } = parseRailsFormFields(el);
if (window.gon.features.personalAccessTokensScopedToProjects) { if (window.gon.features.personalAccessTokensScopedToProjects) {
return new Promise((resolve) => { return new Promise((resolve) => {

View file

@ -328,6 +328,7 @@ export default {
<div <div
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
data-testid="issue-count-badge"
:class="{ :class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader, 'gl-display-none!': list.collapsed && isSwimlanesHeader,
'gl-p-0': list.collapsed, 'gl-p-0': list.collapsed,

View file

@ -2,8 +2,8 @@
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util'; import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue'; import ProjectSelect from './project_select.vue';
@ -17,8 +17,8 @@ export default {
ProjectSelect, ProjectSelect,
GlButton, GlButton,
}, },
mixins: [glFeatureFlagMixin()], mixins: [BoardNewIssueMixin],
inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], inject: ['groupId'],
props: { props: {
list: { list: {
type: Object, type: Object,
@ -53,14 +53,11 @@ export default {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
const { title } = this;
const labels = this.list.label ? [this.list.label] : []; const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : []; const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list); const milestone = getMilestone(this.list);
const weight = this.weightFeatureAvailable ? this.boardWeight : undefined;
const { title } = this;
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
return this.addListNewIssue({ return this.addListNewIssue({
@ -70,7 +67,7 @@ export default {
assigneeIds: assignees?.map((a) => a?.id), assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id, milestoneId: milestone?.id,
projectPath: this.selectedProject.fullPath, projectPath: this.selectedProject.fullPath,
weight: weight >= 0 ? weight : null, ...this.extraIssueInput(),
}, },
list: this.list, list: this.list,
}).then(() => { }).then(() => {

View file

@ -0,0 +1,6 @@
export default {
// EE-only
methods: {
extraIssueInput: () => {},
},
};

View file

@ -327,8 +327,8 @@ export default {
commit(types.RESET_ISSUES); commit(types.RESET_ISSUES);
}, },
moveItem: ({ dispatch }) => { moveItem: ({ dispatch }, payload) => {
dispatch('moveIssue'); dispatch('moveIssue', payload);
}, },
moveIssue: ( moveIssue: (

View file

@ -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)}`,
},
});
},
};
}
}

View file

@ -1,3 +1,5 @@
import { convertToCamelCase } from '~/lib/utils/text_utility';
export const serializeFormEntries = (entries) => export const serializeFormEntries = (entries) =>
entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {}); entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {});
@ -51,3 +53,95 @@ export const serializeFormObject = (form) =>
return acc; 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,
};
}, {});
};

View file

@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:add_issues_button) push_frontend_feature_flag(:add_issues_button)
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
end end
feature_category :boards feature_category :boards

View file

@ -23,7 +23,7 @@
= render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime' = render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
.js-access-tokens-expires-at .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 .form-group
= f.label :scopes, _('Scopes'), class: 'label-bold' = 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) - if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects .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 .gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' } = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' }

View file

@ -11,6 +11,8 @@ module WorkerAttributes
# Urgencies that workers can declare through the `urgencies` attribute # Urgencies that workers can declare through the `urgencies` attribute
VALID_URGENCIES = [:high, :low, :throttled].freeze VALID_URGENCIES = [:high, :low, :throttled].freeze
VALID_DATA_CONSISTENCIES = [:always, :sticky, :delayed].freeze
NAMESPACE_WEIGHTS = { NAMESPACE_WEIGHTS = {
auto_devops: 2, auto_devops: 2,
auto_merge: 3, auto_merge: 3,
@ -69,6 +71,35 @@ module WorkerAttributes
class_attributes[:urgency] || :low class_attributes[:urgency] || :low
end 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 # 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 # application, such as 3rd party applications, other k8s clusters etc See
# doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for # doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for
@ -96,6 +127,8 @@ module WorkerAttributes
def idempotent! def idempotent!
class_attributes[:idempotent] = true class_attributes[:idempotent] = true
validate_worker_attributes!
end end
def idempotent? def idempotent?

View file

@ -0,0 +1,5 @@
---
title: Fix usage data count start/finish export issue
merge_request: 57403
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Removed migrate_delayed_project_removal feature flag
merge_request: 57541
author:
type: other

View file

@ -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

View file

@ -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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` 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) [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` Status: `implemented`

View file

@ -16,6 +16,7 @@ module Gitlab
:elasticsearch_calls, :elasticsearch_calls,
:elasticsearch_duration_s, :elasticsearch_duration_s,
:elasticsearch_timed_out_count, :elasticsearch_timed_out_count,
:worker_data_consistency,
*::Gitlab::Memory::Instrumentation::KEY_MAPPING.values, *::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
*::Gitlab::Instrumentation::Redis.known_payload_keys, *::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS, *::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,

View file

@ -43,3 +43,5 @@ module Gitlab
end end
end end
end end
Gitlab::SidekiqMiddleware.singleton_class.prepend_if_ee('EE::Gitlab::SidekiqMiddleware')

View file

@ -5,11 +5,11 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091 # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091
class UsageDataQueries < UsageData class UsageDataQueries < UsageData
class << self class << self
def count(relation, column = nil, *rest) def count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column) raw_sql(relation, column)
end end
def distinct_count(relation, column = nil, *rest) def distinct_count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column, :distinct) raw_sql(relation, column, :distinct)
end end
@ -21,14 +21,14 @@ module Gitlab
end end
end end
def sum(relation, column, *rest) def sum(relation, column, *args, **kwargs)
relation.select(relation.all.table[column].sum).to_sql relation.select(relation.all.table[column].sum).to_sql
end end
# For estimated distinct count use exact query instead of hll # For estimated distinct count use exact query instead of hll
# buckets query, because it can't be used to obtain estimations without # buckets query, because it can't be used to obtain estimations without
# supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter # 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) raw_sql(relation, column, :distinct)
end end

View file

@ -8707,6 +8707,9 @@ msgstr ""
msgid "Could not find iteration" msgid "Could not find iteration"
msgstr "" msgstr ""
msgid "Could not get the data properly"
msgstr ""
msgid "Could not load the user chart. Please refresh the page to try again." msgid "Could not load the user chart. Please refresh the page to try again."
msgstr "" msgstr ""
@ -23056,6 +23059,9 @@ msgstr ""
msgid "Please try again" msgid "Please try again"
msgstr "" 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." msgid "Please type %{phrase_code} to proceed or close this modal to cancel."
msgstr "" msgstr ""

View file

@ -68,20 +68,30 @@ module QA
end end
end end
def has_child_pipeline? def has_child_pipeline?(title: nil)
has_element? :child_pipeline title ? find_child_pipeline_by_title(title) : has_element?(:child_pipeline)
end end
def has_no_child_pipeline? def has_no_child_pipeline?
has_no_element? :child_pipeline has_no_element?(:child_pipeline)
end end
def click_job(job_name) def click_job(job_name)
click_element(:job_link, Project::Job::Show, text: job_name) click_element(:job_link, Project::Job::Show, text: job_name)
end end
def expand_child_pipeline def child_pipelines
within_element(:child_pipeline) do 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) click_element(:expand_pipeline_button)
end end
end end

View file

@ -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

View file

@ -70,10 +70,10 @@ gitlab:
resources: resources:
requests: requests:
cpu: 746m cpu: 746m
memory: 1873M memory: 2809M
limits: limits:
cpu: 1119m cpu: 1119m
memory: 2809M memory: 4214M
deployment: deployment:
readinessProbe: readinessProbe:
initialDelaySeconds: 5 # Default is 0 initialDelaySeconds: 5 # Default is 0
@ -83,10 +83,10 @@ gitlab:
resources: resources:
requests: requests:
cpu: 400m cpu: 400m
memory: 50M memory: 75M
limits: limits:
cpu: 600m cpu: 600m
memory: 75M memory: 113M
readinessProbe: readinessProbe:
initialDelaySeconds: 5 # Default is 0 initialDelaySeconds: 5 # Default is 0
periodSeconds: 15 # Default is 10 periodSeconds: 15 # Default is 10

View file

@ -14,6 +14,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') } let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Issue Boards', :js do RSpec.describe 'Project issue boards', :js do
include DragTo include DragTo
include MobileHelpers include MobileHelpers
@ -23,7 +23,7 @@ RSpec.describe 'Issue Boards', :js do
context 'no lists' do context 'no lists' do
before do before do
visit project_board_path(project, board) visit_project_board_path_without_query_limit(project, board)
end end
it 'creates default lists' do 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(:a_plus) { create(:label, project: project, name: 'A+') }
let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } 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(: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(: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) } 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 before do
stub_feature_flags(board_new_list: false) 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 wait_for_requests
@ -121,7 +122,8 @@ RSpec.describe 'Issue Boards', :js do
context 'with the NOT queries feature flag disabled' do context 'with the NOT queries feature flag disabled' do
before do before do
stub_feature_flags(not_issuable_queries: false) stub_feature_flags(not_issuable_queries: false)
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
end end
it 'does not have the != option' do 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 context 'with the NOT queries feature flag enabled' do
before do before do
stub_feature_flags(not_issuable_queries: true) stub_feature_flags(not_issuable_queries: true)
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
end end
it 'does not have the != option' do it 'does not have the != option' do
@ -171,8 +174,7 @@ RSpec.describe 'Issue Boards', :js do
it 'infinite scrolls list' do it 'infinite scrolls list' do
create_list(:labeled_issue, 50, project: project, labels: [planning]) create_list(:labeled_issue, 50, project: project, labels: [planning])
visit project_board_path(project, board) visit_project_board_path_without_query_limit(project, board)
wait_for_requests
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('58') 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') expect(page).to have_content('Showing 20 of 58 issues')
find('.board .board-list') 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_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 58 issues') expect(page).to have_content('Showing 40 of 58 issues')
find('.board .board-list') 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_selector('.board-card', count: 58)
expect(page).to have_content('Showing all issues') expect(page).to have_content('Showing all issues')
@ -236,13 +242,13 @@ RSpec.describe 'Issue Boards', :js do
wait_for_board_cards(4, 1) 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(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 # 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(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 end
it 'dragging does not duplicate list' do 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) expect(page).to have_selector(selector, text: development.title, count: 1)
end 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) drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7) wait_for_board_cards(2, 7)
@ -467,14 +474,16 @@ RSpec.describe 'Issue Boards', :js do
end end
it 'removes filtered labels' do it 'removes filtered labels' do
set_filter("label", testing.title) inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
click_filter_link(testing.title) set_filter("label", testing.title)
submit_filter click_filter_link(testing.title)
submit_filter
wait_for_board_cards(2, 1) wait_for_board_cards(2, 1)
find('.clear-search').click find('.clear-search').click
submit_filter submit_filter
end
wait_for_board_cards(2, 8) wait_for_board_cards(2, 8)
end end
@ -484,7 +493,9 @@ RSpec.describe 'Issue Boards', :js do
set_filter("label", testing.title) set_filter("label", testing.title)
click_filter_link(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 wait_for_requests
@ -494,13 +505,18 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_content('Showing 20 of 51 issues') expect(page).to have_content('Showing 20 of 51 issues')
find('.board .board-list') 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_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues') expect(page).to have_content('Showing 40 of 51 issues')
find('.board .board-list') 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_selector('.board-card', count: 51)
expect(page).to have_content('Showing all issues') expect(page).to have_content('Showing all issues')
@ -569,7 +585,7 @@ RSpec.describe 'Issue Boards', :js do
context 'keyboard shortcuts' do context 'keyboard shortcuts' do
before do before do
visit project_board_path(project, board) visit_project_board_path_without_query_limit(project, board)
wait_for_requests wait_for_requests
end 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) 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 # ensure there is enough horizontal space for four boards
resize_window(2000, 800) 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, drag_to(selector: selector,
scrollable: '#board-app', scrollable: '#board-app',
list_from_index: list_from_index, list_from_index: list_from_index,
from_index: from_index, from_index: from_index,
to_index: to_index, to_index: to_index,
list_to_index: list_to_index, list_to_index: list_to_index,
perform_drop: perform_drop) perform_drop: perform_drop)
end
wait_for_requests
end end
def wait_for_board_cards(board_number, expected_cards) 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 } accept_confirm { find('[data-testid="remove-list"]').click }
end end
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 end

View file

@ -12,6 +12,8 @@ RSpec.describe 'Issue Boards add issue modal filtering', :js do
let!(:issue1) { create(:issue, project: project) } let!(:issue1) { create(:issue, project: project) }
before do before do
stub_feature_flags(graphql_board_lists: false)
stub_feature_flags(add_issues_button: true)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -41,6 +41,10 @@ RSpec.describe 'Multi Select Issue', :js do
before do before do
project.add_maintainer(user) 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) sign_in(user)
end end

View file

@ -3,10 +3,12 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Issue Boards new issue', :js do RSpec.describe 'Issue Boards new issue', :js do
let(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) } let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let(:user) { create(:user) } 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 context 'authorized user' do
before do before do
@ -15,6 +17,7 @@ RSpec.describe 'Issue Boards new issue', :js do
sign_in(user) sign_in(user)
visit project_board_path(project, board) visit project_board_path(project, board)
wait_for_requests wait_for_requests
expect(page).to have_selector('.board', count: 3) 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') issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference) 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 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 page.within(first('.board')) do
find('.issue-count-badge-add-button').click find('.issue-count-badge-add-button').click
end end
@ -101,12 +105,16 @@ RSpec.describe 'Issue Boards new issue', :js do
wait_for_requests wait_for_requests
page.within(first('.board')) do
find('.board-card').click
end
page.within(first('.issue-boards-sidebar')) do page.within(first('.issue-boards-sidebar')) do
find('.labels .edit-link').click find('.labels [data-testid="edit-button"]').click
wait_for_requests 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 end
end end

View file

@ -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') issue = project.issues.find_by_title('issue should be shown')
expect(page).to have_content(issue.to_reference) 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 end
end end

View file

@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') } let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar due date', :js do
end end
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -18,6 +18,8 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') } let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -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)') } let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -13,6 +13,8 @@ RSpec.describe 'Project issue boards sidebar', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') } let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -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)') } let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -15,6 +15,8 @@ RSpec.describe 'Project issue boards sidebar time tracking', :js do
let(:application_settings) { {} } let(:application_settings) { {} }
before do before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View file

@ -21,7 +21,8 @@ RSpec.describe 'Sub-group project issue boards', :js do
wait_for_requests wait_for_requests
end 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 find('.board-card').click
page.within '.labels' do page.within '.labels' do

View file

@ -25,18 +25,22 @@ describe('access tokens', () => {
}); });
describe.each` describe.each`
initFunction | mountSelector | expectedComponent initFunction | mountSelector | fieldName | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField} ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField} ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => { `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
describe('when mount element exists', () => { describe('when mount element exists', () => {
const nameAttribute = `access_tokens[${fieldName}]`;
const idAttribute = `access_tokens_${fieldName}`;
beforeEach(() => { beforeEach(() => {
const mountEl = document.createElement('div'); const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector); mountEl.classList.add(mountSelector);
const input = document.createElement('input'); const input = document.createElement('input');
input.setAttribute('name', 'foo-bar'); input.setAttribute('name', nameAttribute);
input.setAttribute('id', 'foo-bar'); input.setAttribute('data-js-name', fieldName);
input.setAttribute('id', idAttribute);
input.setAttribute('placeholder', 'Foo bar'); input.setAttribute('placeholder', 'Foo bar');
input.setAttribute('value', '1,2'); input.setAttribute('value', '1,2');
@ -57,8 +61,8 @@ describe('access tokens', () => {
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({ expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar', name: nameAttribute,
id: 'foo-bar', id: idAttribute,
value: '1,2', value: '1,2',
placeholder: 'Foo bar', placeholder: 'Foo bar',
}); });

View file

@ -639,10 +639,13 @@ describe('resetIssues', () => {
}); });
describe('moveItem', () => { describe('moveItem', () => {
it('should dispatch moveIssue action', () => { it('should dispatch moveIssue action with payload', () => {
const payload = { mock: 'payload' };
testAction({ testAction({
action: actions.moveItem, action: actions.moveItem,
expectedActions: [{ type: 'moveIssue' }], payload,
expectedActions: [{ type: 'moveIssue', payload }],
}); });
}); });
}); });

View file

@ -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', () => { describe('lib/utils/forms', () => {
const createDummyForm = (inputs) => { 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'),
);
});
});
}); });

View file

@ -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();
});
});
});

View file

@ -17,6 +17,7 @@ RSpec.describe Gitlab::InstrumentationHelper do
:elasticsearch_calls, :elasticsearch_calls,
:elasticsearch_duration_s, :elasticsearch_duration_s,
:elasticsearch_timed_out_count, :elasticsearch_timed_out_count,
:worker_data_consistency,
:mem_objects, :mem_objects,
:mem_bytes, :mem_bytes,
:mem_mallocs, :mem_mallocs,

View file

@ -11,12 +11,24 @@ RSpec.describe Gitlab::UsageDataQueries do
it 'returns the raw SQL' do it 'returns the raw SQL' do
expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"') expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')
end 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 end
describe '.distinct_count' do describe '.distinct_count' do
it 'returns the raw SQL' 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"') expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
end 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 end
describe '.redis_usage_data' do describe '.redis_usage_data' do

View file

@ -2,25 +2,26 @@
module NextInstanceOf module NextInstanceOf
def expect_next_instance_of(klass, *new_args, &blk) 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 end
def expect_next_instances_of(klass, number, *new_args, &blk) def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk)
stub_new(expect(klass), number, *new_args, &blk) stub_new(expect(klass), number, ordered, *new_args, &blk)
end end
def allow_next_instance_of(klass, *new_args, &blk) 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 end
def allow_next_instances_of(klass, number, *new_args, &blk) def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk)
stub_new(allow(klass), number, *new_args, &blk) stub_new(allow(klass), number, ordered, *new_args, &blk)
end end
private 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 = receive(:new)
receive_new.ordered if ordered
receive_new.exactly(number).times if number receive_new.exactly(number).times if number
receive_new.with(*new_args) if new_args.any? receive_new.with(*new_args) if new_args.any?

View file

@ -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