Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2d40635435
commit
26bba9525d
43 changed files with 889 additions and 121 deletions
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
6
app/assets/javascripts/boards/mixins/board_new_issue.js
Normal file
6
app/assets/javascripts/boards/mixins/board_new_issue.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
// EE-only
|
||||||
|
methods: {
|
||||||
|
extraIssueInput: () => {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -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: (
|
||||||
|
|
56
app/assets/javascripts/ensure_data.js
Normal file
56
app/assets/javascripts/ensure_data.js
Normal 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)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' }
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix usage data count start/finish export issue
|
||||||
|
merge_request: 57403
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Removed migrate_delayed_project_removal feature flag
|
||||||
|
merge_request: 57541
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -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
|
|
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -43,3 +43,5 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Gitlab::SidekiqMiddleware.singleton_class.prepend_if_ee('EE::Gitlab::SidekiqMiddleware')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
145
spec/frontend/vue_shared/components/ensure_data_spec.js
Normal file
145
spec/frontend/vue_shared/components/ensure_data_spec.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
74
spec/workers/concerns/worker_attributes_spec.rb
Normal file
74
spec/workers/concerns/worker_attributes_spec.rb
Normal 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
|
Loading…
Reference in a new issue