Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-12 06:07:53 +00:00
parent 48d9e7ff8d
commit 90726a8ccc
45 changed files with 647 additions and 731 deletions

View file

@ -37,3 +37,13 @@ export const integrationFormSectionComponents = {
[integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger',
[integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
};
export const billingPlans = {
PREMIUM: 'premium',
ULTIMATE: 'ultimate',
};
export const billingPlanNames = {
[billingPlans.PREMIUM]: s__('BillingPlans|Premium'),
[billingPlans.ULTIMATE]: s__('BillingPlans|Ultimate'),
};

View file

@ -1,5 +1,11 @@
<script>
import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml, GlForm } from '@gitlab/ui';
import {
GlBadge,
GlButton,
GlModalDirective,
GlSafeHtmlDirective as SafeHtml,
GlForm,
} from '@gitlab/ui';
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
@ -10,6 +16,7 @@ import {
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
integrationLevels,
integrationFormSectionComponents,
billingPlanNames,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
@ -42,6 +49,7 @@ export default {
import(
/* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
),
GlBadge,
GlButton,
GlForm,
},
@ -177,6 +185,7 @@ export default {
},
csrf,
integrationFormSectionComponents,
billingPlanNames,
};
</script>
@ -214,7 +223,20 @@ export default {
>
<div class="row">
<div class="col-lg-4">
<h4 class="gl-mt-0">{{ section.title }}</h4>
<h4 class="gl-mt-0">
{{ section.title
}}<gl-badge
v-if="section.plan"
:href="propsSource.aboutPricingUrl"
target="_blank"
rel="noopener noreferrer"
variant="tier"
icon="license"
class="gl-ml-3"
>
{{ $options.billingPlanNames[section.plan] }}
</gl-badge>
</h4>
<p v-safe-html:[$options.descriptionHtmlConfig]="section.description"></p>
</div>

View file

@ -2,7 +2,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { s__, __ } from '~/locale';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
export default {
name: 'JiraIssuesFields',
@ -10,7 +9,6 @@ export default {
GlFormGroup,
GlFormCheckbox,
GlFormInput,
JiraUpgradeCta,
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
@ -45,11 +43,6 @@ export default {
required: false,
default: null,
},
upgradePlanPath: {
type: String,
required: false,
default: '',
},
isValidated: {
type: Boolean,
required: false,
@ -64,6 +57,9 @@ export default {
},
computed: {
...mapGetters(['isInheriting']),
checkboxDisabled() {
return !this.showJiraIssuesIntegration || this.isInheriting;
},
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated;
},
@ -85,64 +81,48 @@ export default {
<template>
<div>
<template v-if="showJiraIssuesIntegration">
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
<gl-form-checkbox
v-model="enableJiraIssues"
:disabled="isInheriting"
data-qa-selector="service_jira_issues_enabled_checkbox"
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
<gl-form-checkbox
v-model="enableJiraIssues"
:disabled="checkboxDisabled"
data-qa-selector="service_jira_issues_enabled_checkbox"
>
{{ $options.i18n.enableCheckboxLabel }}
<template #help>
{{ $options.i18n.enableCheckboxHelp }}
</template>
</gl-form-checkbox>
<div v-if="enableJiraIssues" class="gl-pl-6 gl-mt-3">
<gl-form-group
:label="$options.i18n.projectKeyLabel"
label-for="service_project_key"
:invalid-feedback="$options.i18n.requiredFieldFeedback"
:state="validProjectKey"
class="gl-max-w-26"
data-testid="project-key-form-group"
>
{{ $options.i18n.enableCheckboxLabel }}
<template #help>
{{ $options.i18n.enableCheckboxHelp }}
</template>
</gl-form-checkbox>
<div v-if="enableJiraIssues" class="gl-pl-6 gl-mt-3">
<gl-form-group
:label="$options.i18n.projectKeyLabel"
label-for="service_project_key"
:invalid-feedback="$options.i18n.requiredFieldFeedback"
<gl-form-input
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
data-qa-selector="service_jira_project_key_field"
:placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
class="gl-max-w-26"
data-testid="project-key-form-group"
>
<gl-form-input
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
data-qa-selector="service_jira_project_key_field"
:placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
:readonly="isInheriting"
/>
</gl-form-group>
<jira-issue-creation-vulnerabilities
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesIntegration"
class="gl-mt-6"
data-testid="jira-for-vulnerabilities"
@request-jira-issue-types="$emit('request-jira-issue-types')"
:readonly="isInheriting"
/>
<jira-upgrade-cta
v-if="!showJiraVulnerabilitiesIntegration"
class="gl-mt-2 gl-ml-6"
data-testid="ultimate-upgrade-cta"
show-ultimate-message
:upgrade-plan-path="upgradePlanPath"
/>
</div>
</template>
</gl-form-group>
<jira-upgrade-cta
v-else
data-testid="premium-upgrade-cta"
show-premium-message
:upgrade-plan-path="upgradePlanPath"
/>
<jira-issue-creation-vulnerabilities
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesIntegration"
class="gl-mt-6"
data-testid="jira-for-vulnerabilities"
@request-jira-issue-types="$emit('request-jira-issue-types')"
/>
</div>
</div>
</template>

View file

@ -23,6 +23,7 @@ function parseDatasetToProps(data) {
projectKey,
upgradePlanPath,
learnMorePath,
aboutPricingUrl,
triggerEvents,
sections,
fields,
@ -82,6 +83,7 @@ function parseDatasetToProps(data) {
upgradePlanPath,
},
learnMorePath,
aboutPricingUrl,
triggerEvents: JSON.parse(triggerEvents),
sections: JSON.parse(sections, { deep: true }),
fields: convertObjectPropsToCamelCase(JSON.parse(fields), { deep: true }),

View file

@ -78,9 +78,9 @@ export default {
},
},
fields: [
{ key: 'spentAt', label: __('Spent At'), sortable: true },
{ key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' },
{ key: 'user', label: __('User'), sortable: true },
{ key: 'timeSpent', label: __('Time Spent'), sortable: true },
{ key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' },
{ key: 'summary', label: __('Summary / Note'), sortable: true },
],
};

View file

@ -249,6 +249,7 @@ export default {
</gl-link>
<gl-modal
modal-id="time-tracking-report"
size="lg"
:title="__('Time tracking report')"
:hide-footer="true"
>

View file

@ -78,17 +78,7 @@ export default {
return !this.userIsLoading && this.user.username !== gon.current_username;
},
shouldRenderToggleFollowButton() {
return (
/*
* We're using `gon` to access feature flag because this component
* gets initialized dynamically multiple times from `user_popovers.js`
* for each user link present on the page, and using `glFeatureFlagMixin()`
* doesn't inject available feature flags into the component during init.
*/
gon?.features?.followInUserPopover &&
this.isNotCurrentUser &&
typeof this.user?.isFollowed !== 'undefined'
);
return this.isNotCurrentUser && typeof this.user?.isFollowed !== 'undefined';
},
toggleFollowButtonText() {
if (this.toggleFollowLoading) return null;

View file

@ -2,33 +2,55 @@
module Packages
class BuildInfosFinder
include ActiveRecord::ConnectionAdapters::Quoting
MAX_PAGE_SIZE = 100
def initialize(package, params)
@package = package
def initialize(package_ids, params)
@package_ids = package_ids
@params = params
end
def execute
build_infos = @package.build_infos.without_empty_pipelines
build_infos = apply_order(build_infos)
build_infos = apply_limit(build_infos)
apply_cursor(build_infos)
return Packages::BuildInfo.none if @package_ids.blank?
# This is a highly custom query that
# will not be re-used elsewhere
# rubocop: disable CodeReuse/ActiveRecord
query = Packages::Package.id_in(@package_ids)
.select('build_infos.*')
.from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')])
.order('build_infos.id DESC')
# We manually select build_infos fields from the lateral query.
# Thus, we need to instruct ActiveRecord that returned rows are
# actually Packages::BuildInfo objects
Packages::BuildInfo.find_by_sql(query.to_sql)
# rubocop: enable CodeReuse/ActiveRecord
end
private
def apply_order(build_infos)
order_direction = :desc
order_direction = :asc if last
def lateral_query
order_direction = last ? :asc : :desc
build_infos.order_by_pipeline_id(order_direction)
# This is a highly custom query that
# will not be re-used elsewhere
# rubocop: disable CodeReuse/ActiveRecord
where_condition = Packages::BuildInfo.arel_table[:package_id]
.eq(Arel.sql("#{Packages::Package.table_name}.id"))
build_infos = ::Packages::BuildInfo.without_empty_pipelines
.where(where_condition)
.order(id: order_direction)
.limit(max_rows_per_package_id)
# rubocop: enable CodeReuse/ActiveRecord
apply_cursor(build_infos)
end
def apply_limit(build_infos)
def max_rows_per_package_id
limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min
limit += 1 if support_next_page
build_infos.limit(limit)
limit
end
def apply_cursor(build_infos)

View file

@ -1,92 +0,0 @@
# frozen_string_literal: true
module Packages
# TODO rename to BuildInfosFinder when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
class BuildInfosForManyPackagesFinder
include ActiveRecord::ConnectionAdapters::Quoting
MAX_PAGE_SIZE = 100
def initialize(package_ids, params)
@package_ids = package_ids
@params = params
end
def execute
return Packages::BuildInfo.none if @package_ids.blank?
# This is a highly custom query that
# will not be re-used elsewhere
# rubocop: disable CodeReuse/ActiveRecord
query = Packages::Package.id_in(@package_ids)
.select('build_infos.*')
.from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')])
.order('build_infos.id DESC')
# We manually select build_infos fields from the lateral query.
# Thus, we need to instruct ActiveRecord that returned rows are
# actually Packages::BuildInfo objects
Packages::BuildInfo.find_by_sql(query.to_sql)
# rubocop: enable CodeReuse/ActiveRecord
end
private
def lateral_query
order_direction = last ? :asc : :desc
# This is a highly custom query that
# will not be re-used elsewhere
# rubocop: disable CodeReuse/ActiveRecord
where_condition = Packages::BuildInfo.arel_table[:package_id]
.eq(Arel.sql("#{Packages::Package.table_name}.id"))
build_infos = ::Packages::BuildInfo.without_empty_pipelines
.where(where_condition)
.order(id: order_direction)
.limit(max_rows_per_package_id)
# rubocop: enable CodeReuse/ActiveRecord
apply_cursor(build_infos)
end
def max_rows_per_package_id
limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min
limit += 1 if support_next_page
limit
end
def apply_cursor(build_infos)
if before
build_infos.with_pipeline_id_greater_than(before)
elsif after
build_infos.with_pipeline_id_less_than(after)
else
build_infos
end
end
def first
@params[:first]
end
def last
@params[:last]
end
def max_page_size
@params[:max_page_size]
end
def before
@params[:before]
end
def after
@params[:after]
end
def support_next_page
@params[:support_next_page]
end
end
end

View file

@ -16,10 +16,6 @@ module Resolvers
}).freeze
def ready?(**args)
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
context.scoped_set!(:packages_access_level, :group)
context[self.class] ||= { executions: 0 }
context[self.class][:executions] += 1
raise GraphQL::ExecutionError, "Packages can be requested only for one group at a time" if context[self.class][:executions] > 1

View file

@ -16,45 +16,13 @@ module Resolvers
# number of build infos returned for _each_ package when using the new finder.
MAX_PAGE_SIZE = 20
def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
case detect_mode
when :object_field
package.pipelines
when :new_finder
resolve_with_new_finder(first: first, last: last, after: after, before: before, lookahead: lookahead)
else
resolve_with_old_finder(first: first, last: last, after: after, before: before, lookahead: lookahead)
end
end
# we manage the pagination manually, so opt out of the connection field extension
def self.field_options
super.merge(
connection: false,
extras: [:lookahead]
)
end
private
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def detect_mode
return :new_finder if Feature.enabled?(:packages_graphql_pipelines_resolver)
return :object_field if context[:packages_access_level] == :group || context[:packages_access_level] == :project
:old_finder
end
# This returns a promise for a connection of promises for pipelines:
# Lazy[Connection[Lazy[Pipeline]]] structure
# TODO rename to #resolve when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def resolve_with_new_finder(first:, last:, after:, before:, lookahead:)
def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
default_value = default_value_for(first: first, last: last, after: after, before: before)
BatchLoader::GraphQL.for(package.id)
.batch(default_value: default_value) do |package_ids, loader|
build_infos = ::Packages::BuildInfosForManyPackagesFinder.new(
build_infos = ::Packages::BuildInfosFinder.new(
package_ids,
first: first,
last: last,
@ -73,6 +41,16 @@ module Resolvers
end
end
# we manage the pagination manually, so opt out of the connection field extension
def self.field_options
super.merge(
connection: false,
extras: [:lookahead]
)
end
private
def lazy_load_pipeline(id)
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, id)
.find
@ -89,25 +67,6 @@ module Resolvers
)
end
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def resolve_with_old_finder(first:, last:, after:, before:, lookahead:)
finder = ::Packages::BuildInfosFinder.new(
package,
first: first,
last: last,
after: decode_cursor(after),
before: decode_cursor(before),
max_page_size: context.schema.default_max_page_size,
support_next_page: lookahead.selects?(:page_info)
)
build_infos = finder.execute
# this .pluck_pipeline_ids can load max 101 pipelines ids
::Ci::Pipeline.id_in(build_infos.pluck_pipeline_ids)
end
def decode_cursor(encoded)
return unless encoded

View file

@ -5,14 +5,6 @@ module Resolvers
class ProjectPackagesResolver < PackagesBaseResolver
# The GraphQL type is defined in the extended class
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def ready?(**args)
context.scoped_set!(:packages_access_level, :project)
super
end
def resolve(sort:, **filters)
return unless packages_available?

View file

@ -112,6 +112,7 @@ module IntegrationsHelper
enable_comments: integration.comment_on_event_enabled.to_s,
comment_detail: integration.comment_detail,
learn_more_path: integrations_help_page_path,
about_pricing_url: Gitlab::Saas.about_pricing_url,
trigger_events: trigger_events_for_integration(integration),
sections: integration.sections.to_json,
fields: fields_for_integration(integration),

View file

@ -168,7 +168,8 @@ module Integrations
sections.push({
type: SECTION_TYPE_JIRA_ISSUES,
title: _('Issues'),
description: jira_issues_section_description
description: jira_issues_section_description,
plan: 'premium'
})
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class NamespaceCiCdSettingPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
delegate { @subject.namespace }
end

View file

@ -21,7 +21,7 @@
.text-secondary
= sprite_icon("rocket", size: 12)
= _("Release")
= link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'gl-text-blue-600!'
= link_to release.name, project_release_path(@project, release.tag), class: 'gl-text-blue-600!'
- if tag.message.present?
%pre.wrap

View file

@ -1,8 +0,0 @@
---
name: follow_in_user_popover
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76050
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355070
milestone: '14.9'
type: development
group: group::workspace
default_enabled: false

View file

@ -1,8 +0,0 @@
---
name: packages_graphql_pipelines_resolver
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82496
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358432
milestone: '14.10'
type: development
group: group::package
default_enabled: false

View file

@ -0,0 +1,10 @@
- name: "Remove `type` and `types` keyword from CI/CD configuration"
announcement_milestone: "14.6"
announcement_date: "2021-12-22"
removal_milestone: "15.0"
removal_date: "2022-05-22"
breaking_change: true
reporter: dhershkovitch
body: | # Do not modify this line, instead modify the lines below.
The `type` and `types` CI/CD keywords is removed in GitLab 15.0, so pipelines that use these keywords fail with a syntax error. Switch to `stage` and `stages`, which have the same behavior.
# The following items are not published on the docs page, but may be used in the future.

View file

@ -3677,6 +3677,26 @@ Input type: `MergeRequestUpdateInput`
| <a id="mutationmergerequestupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationmergerequestupdatemergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
### `Mutation.namespaceCiCdSettingsUpdate`
Input type: `NamespaceCiCdSettingsUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationnamespacecicdsettingsupdateallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean`](#boolean) | Indicates if stale runners directly belonging to this namespace should be periodically pruned. |
| <a id="mutationnamespacecicdsettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationnamespacecicdsettingsupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace the settings belong to. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationnamespacecicdsettingsupdatecicdsettings"></a>`ciCdSettings` | [`NamespaceCiCdSetting!`](#namespacecicdsetting) | CI/CD settings after mutation. |
| <a id="mutationnamespacecicdsettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationnamespacecicdsettingsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.namespaceIncreaseStorageTemporarily`
Input type: `NamespaceIncreaseStorageTemporarilyInput`
@ -13857,6 +13877,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="namespacescanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`. |
| <a id="namespacescanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
### `NamespaceCiCdSetting`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespacecicdsettingallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean`](#boolean) | Indicates if stale runners directly belonging to this namespace should be periodically pruned. |
| <a id="namespacecicdsettingnamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace the CI/CD settings belong to. |
### `NetworkPolicy`
Represents the network policy.

View file

@ -198,6 +198,16 @@ changes to your code, settings, or workflow.
In GitLab 14.5, we introduced the command `gitlab-ctl promote` to promote any Geo secondary node to a primary during a failover. This command replaces `gitlab-ctl promote-to-primary-node` which was only usable for single-node Geo sites. `gitlab-ctl promote-to-primary-node` has been removed in GitLab 15.0.
### Remove `type` and `types` keyword from CI/CD configuration
WARNING:
This feature was changed or removed in 15.0
as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
Before updating GitLab, review the details carefully to determine if you need to make any
changes to your code, settings, or workflow.
The `type` and `types` CI/CD keywords is removed in GitLab 15.0, so pipelines that use these keywords fail with a syntax error. Switch to `stage` and `stages`, which have the same behavior.
### Remove dependency_proxy_for_private_groups feature flag
WARNING:

View file

@ -174,6 +174,7 @@ To group issues by label:
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1.
> - Deployed behind a [feature flag](../../feature_flags.md), named `iteration_cadences`, disabled by default.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an
@ -200,7 +201,8 @@ To create an iteration cadence:
1. Select **New iteration cadence**.
1. Complete the fields.
- Enter the title and description of the iteration cadence.
- Enter the start date of the iteration cadence.
- Enter the start date of the iteration cadence. Iterations will be scheduled to
begin on the same day of the week as the day of the week of the start date.
- From the **Duration** dropdown list, select how many weeks each iteration should last.
- From the **Future iterations** dropdown list, select how many future iterations should be
created and maintained by GitLab.
@ -277,6 +279,35 @@ If you attempt to set a new start date, the conversion fails with an error messa
If your manual cadence is empty, converting it to use automatic scheduling is effectively
the same as creating a new automated cadence.
GitLab will start scheduling new iterations on the same day of the week as the start date,
starting from the nearest such day from the current date.
During the conversion process GitLab does not delete or modify existing **ongoing** or
**closed** iterations. If you have iterations with start dates in the future,
they are updated to fit your cadence settings.
#### Converted cadences example
For example, suppose it's Friday, April 15, and you have three iterations in a manual cadence:
- Monday, April 4 - Friday, April 8 (closed)
- Tuesday, April 12 - Friday, April 15 (ongoing)
- Tuesday, May 3 - Friday, May 6 (upcoming)
On Friday, April 15, you convert the manual cadence
to automate scheduling iterations every week, up to two upcoming iterations.
The first iteration is closed, and the second iteration is ongoing,
so they aren't deleted or modified in the conversion process.
To observe the weekly duration, the third iteration is updated so that it:
- Starts on Monday, April 18 - which is the nearest date that is Monday.
- Ends on Sunday, April 24.
Finally, to always have two upcoming iterations, an additional iteration is scheduled:
- Monday, April 4 - Friday, April 8 (closed)
- Tuesday, April 12 - Friday, April 15 (ongoing)
- Monday, April 18 - Sunday, April 24 (upcoming)
- Monday, April 25 - Sunday, May 1 (upcoming)

View file

@ -30,6 +30,8 @@ module API
environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, params).execute
present paginate(environments), with: Entities::Environment, current_user: current_user
rescue ::Environments::EnvironmentsFinder::InvalidStatesError => exception
bad_request!(exception.message)
end
desc 'Creates a new environment' do

View file

@ -55,7 +55,7 @@ module Gitlab
end
def cleanup_gin_index(table_name)
index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'")
index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%using gin%'")
index_names.each do |index_name|
ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')")

View file

@ -943,7 +943,7 @@ module Gitlab
execute("DELETE FROM batched_background_migrations WHERE #{conditions}")
end
def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:)
def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:, finalize: true)
migration = Gitlab::Database::BackgroundMigration::BatchedMigration
.for_configuration(job_class_name, table_name, column_name, job_arguments).first
@ -954,9 +954,13 @@ module Gitlab
job_arguments: job_arguments
}
if migration.nil?
Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}"
elsif !migration.finished?
return Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}" if migration.nil?
return if migration.finished?
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection) if finalize
unless migration.reload.finished? # rubocop:disable Cop/ActiveRecordAssociationReload
raise "Expected batched background migration for the given configuration to be marked as 'finished', " \
"but it is '#{migration.status_name}':" \
"\t#{configuration}" \

View file

@ -122,6 +122,14 @@ module Gitlab
migration.save!
migration
end
def finalize_batched_background_migration(job_class_name:, table_name:, column_name:, job_arguments:)
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(job_class_name, table_name, column_name, job_arguments)
raise 'Could not find batched background migration' if migration.nil?
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection)
end
end
end
end

View file

@ -59,7 +59,6 @@ module Gitlab
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars)
push_frontend_feature_flag(:mr_attention_requests, current_user)
push_frontend_feature_flag(:follow_in_user_popover, current_user)
end
# Exposes the state of a feature flag to the frontend code.

View file

@ -5770,6 +5770,9 @@ msgstr ""
msgid "BillingPlans|Portfolio management"
msgstr ""
msgid "BillingPlans|Premium"
msgstr ""
msgid "BillingPlans|Pricing page"
msgstr ""
@ -5812,6 +5815,9 @@ msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Ultimate"
msgstr ""
msgid "BillingPlans|Upgrade to Premium"
msgstr ""

View file

@ -56,8 +56,8 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.12.0",
"@gitlab/ui": "40.0.0",
"@gitlab/svgs": "2.14.0",
"@gitlab/ui": "40.2.0",
"@gitlab/visual-review-tools": "1.7.1",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",

View file

@ -9,7 +9,20 @@ RSpec.describe ::Packages::BuildInfosFinder do
let_it_be(:build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: package) }
let_it_be(:build_info_with_empty_pipeline) { create(:package_build_info, package: package) }
let(:finder) { described_class.new(package, params) }
let_it_be(:other_package) { create(:package) }
let_it_be(:other_build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: other_package) }
let_it_be(:other_build_info_with_empty_pipeline) { create(:package_build_info, package: other_package) }
let_it_be(:all_build_infos) { build_infos + other_build_infos }
let(:finder) { described_class.new(packages, params) }
let(:packages) { nil }
let(:first) { nil }
let(:last) { nil }
let(:after) { nil }
let(:before) { nil }
let(:max_page_size) { nil }
let(:support_next_page) { false }
let(:params) do
{
first: first,
@ -24,41 +37,100 @@ RSpec.describe ::Packages::BuildInfosFinder do
describe '#execute' do
subject { finder.execute }
where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do
# F L AI BI MPS SNP
nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 2 | false | [4, 3]
2 | nil | nil | nil | nil | false | [4, 3]
2 | nil | nil | nil | nil | true | [4, 3, 2]
2 | nil | 3 | nil | nil | false | [2, 1]
2 | nil | 3 | nil | nil | true | [2, 1, 0]
3 | nil | 4 | nil | 2 | false | [3, 2]
3 | nil | 4 | nil | 2 | true | [3, 2, 1]
nil | 2 | nil | nil | nil | false | [0, 1]
nil | 2 | nil | nil | nil | true | [0, 1, 2]
nil | 2 | nil | 1 | nil | false | [2, 3]
nil | 2 | nil | 1 | nil | true | [2, 3, 4]
nil | 3 | nil | 0 | 2 | false | [1, 2]
nil | 3 | nil | 0 | 2 | true | [1, 2, 3]
end
with_them do
shared_examples 'returning the expected build infos' do
let(:expected_build_infos) do
expected_build_infos_indexes.map do |idx|
build_infos[idx]
all_build_infos[idx]
end
end
let(:after) do
build_infos[after_index].pipeline_id if after_index
all_build_infos[after_index].pipeline_id if after_index
end
let(:before) do
build_infos[before_index].pipeline_id if before_index
all_build_infos[before_index].pipeline_id if before_index
end
it { is_expected.to eq(expected_build_infos) }
end
context 'with nil packages' do
let(:packages) { nil }
it { is_expected.to be_empty }
end
context 'with [] packages' do
let(:packages) { [] }
it { is_expected.to be_empty }
end
context 'with empy scope packages' do
let(:packages) { Packages::Package.none }
it { is_expected.to be_empty }
end
context 'with a single package' do
let(:packages) { package.id }
# rubocop: disable Layout/LineLength
where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do
# F L AI BI MPS SNP
nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 2 | false | [4, 3]
2 | nil | nil | nil | nil | false | [4, 3]
2 | nil | nil | nil | nil | true | [4, 3, 2]
2 | nil | 3 | nil | nil | false | [2, 1]
2 | nil | 3 | nil | nil | true | [2, 1, 0]
3 | nil | 4 | nil | 2 | false | [3, 2]
3 | nil | 4 | nil | 2 | true | [3, 2, 1]
nil | 2 | nil | nil | nil | false | [1, 0]
nil | 2 | nil | nil | nil | true | [2, 1, 0]
nil | 2 | nil | 1 | nil | false | [3, 2]
nil | 2 | nil | 1 | nil | true | [4, 3, 2]
nil | 3 | nil | 0 | 2 | false | [2, 1]
nil | 3 | nil | 0 | 2 | true | [3, 2, 1]
end
# rubocop: enable Layout/LineLength
with_them do
it_behaves_like 'returning the expected build infos'
end
end
context 'with many packages' do
let(:packages) { [package.id, other_package.id] }
# using after_index/before_index when receiving multiple packages doesn't
# make sense but we still verify here that the behavior is coherent.
# rubocop: disable Layout/LineLength
where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do
# F L AI BI MPS SNP
nil | nil | nil | nil | nil | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
nil | nil | nil | nil | 10 | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
nil | nil | nil | nil | 2 | false | [9, 8, 4, 3]
2 | nil | nil | nil | nil | false | [9, 8, 4, 3]
2 | nil | nil | nil | nil | true | [9, 8, 7, 4, 3, 2]
2 | nil | 3 | nil | nil | false | [2, 1]
2 | nil | 3 | nil | nil | true | [2, 1, 0]
3 | nil | 4 | nil | 2 | false | [3, 2]
3 | nil | 4 | nil | 2 | true | [3, 2, 1]
nil | 2 | nil | nil | nil | false | [6, 5, 1, 0]
nil | 2 | nil | nil | nil | true | [7, 6, 5, 2, 1, 0]
nil | 2 | nil | 1 | nil | false | [6, 5, 3, 2]
nil | 2 | nil | 1 | nil | true | [7, 6, 5, 4, 3, 2]
nil | 3 | nil | 0 | 2 | false | [6, 5, 2, 1]
nil | 3 | nil | 0 | 2 | true | [7, 6, 5, 3, 2, 1]
end
with_them do
it_behaves_like 'returning the expected build infos'
end
# rubocop: enable Layout/LineLength
end
end
end

View file

@ -1,136 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::BuildInfosForManyPackagesFinder do
using RSpec::Parameterized::TableSyntax
let_it_be(:package) { create(:package) }
let_it_be(:build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: package) }
let_it_be(:build_info_with_empty_pipeline) { create(:package_build_info, package: package) }
let_it_be(:other_package) { create(:package) }
let_it_be(:other_build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: other_package) }
let_it_be(:other_build_info_with_empty_pipeline) { create(:package_build_info, package: other_package) }
let_it_be(:all_build_infos) { build_infos + other_build_infos }
let(:finder) { described_class.new(packages, params) }
let(:packages) { nil }
let(:first) { nil }
let(:last) { nil }
let(:after) { nil }
let(:before) { nil }
let(:max_page_size) { nil }
let(:support_next_page) { false }
let(:params) do
{
first: first,
last: last,
after: after,
before: before,
max_page_size: max_page_size,
support_next_page: support_next_page
}
end
describe '#execute' do
subject { finder.execute }
shared_examples 'returning the expected build infos' do
let(:expected_build_infos) do
expected_build_infos_indexes.map do |idx|
all_build_infos[idx]
end
end
let(:after) do
all_build_infos[after_index].pipeline_id if after_index
end
let(:before) do
all_build_infos[before_index].pipeline_id if before_index
end
it { is_expected.to eq(expected_build_infos) }
end
context 'with nil packages' do
let(:packages) { nil }
it { is_expected.to be_empty }
end
context 'with [] packages' do
let(:packages) { [] }
it { is_expected.to be_empty }
end
context 'with empy scope packages' do
let(:packages) { Packages::Package.none }
it { is_expected.to be_empty }
end
context 'with a single package' do
let(:packages) { package.id }
# rubocop: disable Layout/LineLength
where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do
# F L AI BI MPS SNP
nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0]
nil | nil | nil | nil | 2 | false | [4, 3]
2 | nil | nil | nil | nil | false | [4, 3]
2 | nil | nil | nil | nil | true | [4, 3, 2]
2 | nil | 3 | nil | nil | false | [2, 1]
2 | nil | 3 | nil | nil | true | [2, 1, 0]
3 | nil | 4 | nil | 2 | false | [3, 2]
3 | nil | 4 | nil | 2 | true | [3, 2, 1]
nil | 2 | nil | nil | nil | false | [1, 0]
nil | 2 | nil | nil | nil | true | [2, 1, 0]
nil | 2 | nil | 1 | nil | false | [3, 2]
nil | 2 | nil | 1 | nil | true | [4, 3, 2]
nil | 3 | nil | 0 | 2 | false | [2, 1]
nil | 3 | nil | 0 | 2 | true | [3, 2, 1]
end
# rubocop: enable Layout/LineLength
with_them do
it_behaves_like 'returning the expected build infos'
end
end
context 'with many packages' do
let(:packages) { [package.id, other_package.id] }
# using after_index/before_index when receiving multiple packages doesn't
# make sense but we still verify here that the behavior is coherent.
# rubocop: disable Layout/LineLength
where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do
# F L AI BI MPS SNP
nil | nil | nil | nil | nil | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
nil | nil | nil | nil | 10 | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
nil | nil | nil | nil | 2 | false | [9, 8, 4, 3]
2 | nil | nil | nil | nil | false | [9, 8, 4, 3]
2 | nil | nil | nil | nil | true | [9, 8, 7, 4, 3, 2]
2 | nil | 3 | nil | nil | false | [2, 1]
2 | nil | 3 | nil | nil | true | [2, 1, 0]
3 | nil | 4 | nil | 2 | false | [3, 2]
3 | nil | 4 | nil | 2 | true | [3, 2, 1]
nil | 2 | nil | nil | nil | false | [6, 5, 1, 0]
nil | 2 | nil | nil | nil | true | [7, 6, 5, 2, 1, 0]
nil | 2 | nil | 1 | nil | false | [6, 5, 3, 2]
nil | 2 | nil | 1 | nil | true | [7, 6, 5, 4, 3, 2]
nil | 3 | nil | 0 | 2 | false | [6, 5, 2, 1]
nil | 3 | nil | 0 | 2 | true | [7, 6, 5, 3, 2, 1]
end
with_them do
it_behaves_like 'returning the expected build infos'
end
# rubocop: enable Layout/LineLength
end
end
end

View file

@ -1,4 +1,4 @@
import { GlForm } from '@gitlab/ui';
import { GlBadge, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
@ -18,11 +18,18 @@ import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
billingPlans,
billingPlanNames,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockIntegrationProps, mockField, mockSectionConnection } from '../mock_data';
import {
mockIntegrationProps,
mockField,
mockSectionConnection,
mockSectionJiraIssues,
} from '../mock_data';
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility');
@ -72,6 +79,7 @@ describe('IntegrationForm', () => {
const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findDynamicField = () => wrapper.findComponent(DynamicField);
@ -327,9 +335,21 @@ describe('IntegrationForm', () => {
expect(connectionSection.find('h4').text()).toBe(mockSectionConnection.title);
expect(connectionSection.find('p').text()).toBe(mockSectionConnection.description);
expect(findGlBadge().exists()).toBe(false);
expect(findConnectionSectionComponent().exists()).toBe(true);
});
it('renders GlBadge when `plan` is present', () => {
createComponent({
customStateProps: {
sections: [mockSectionConnection, mockSectionJiraIssues],
},
});
expect(findGlBadge().exists()).toBe(true);
expect(findGlBadge().text()).toMatchInterpolatedText(billingPlanNames[billingPlans.PREMIUM]);
});
it('passes only fields with section type', () => {
const sectionFields = [
{ name: 'username', type: 'text', section: mockSectionConnection.type },

View file

@ -10,7 +10,6 @@ describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
upgradePlanPath: 'https://gitlab.com',
};
@ -42,8 +41,6 @@ describe('JiraIssuesFields', () => {
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
@ -55,19 +52,16 @@ describe('JiraIssuesFields', () => {
describe('template', () => {
describe.each`
showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
${false} | ${false}
${false} | ${true}
${true} | ${false}
${true} | ${true}
showJiraIssuesIntegration
${false}
${true}
`(
'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities',
({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => {
'when showJiraIssuesIntegration = $showJiraIssuesIntegration',
({ showJiraIssuesIntegration }) => {
beforeEach(() => {
createComponent({
props: {
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
},
});
});
@ -77,39 +71,12 @@ describe('JiraIssuesFields', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
});
it('does not render the Premium CTA', () => {
expect(findPremiumUpgradeCTA().exists()).toBe(false);
});
if (!showJiraVulnerabilitiesIntegration) {
it.each`
scenario | enableJiraIssues
${'when "Enable Jira issues" is checked, renders Ultimate upgrade CTA'} | ${true}
${'when "Enable Jira issues" is unchecked, does not render Ultimate upgrade CTA'} | ${false}
`('$scenario', async ({ enableJiraIssues }) => {
if (enableJiraIssues) {
await setEnableCheckbox();
}
expect(findUltimateUpgradeCTA().exists()).toBe(enableJiraIssues);
});
}
} else {
it('does not render enable checkbox', () => {
expect(findEnableCheckbox().exists()).toBe(false);
});
it('renders the Premium CTA', () => {
const premiumUpgradeCTA = findPremiumUpgradeCTA();
expect(premiumUpgradeCTA.exists()).toBe(true);
expect(premiumUpgradeCTA.props('upgradePlanPath')).toBe(defaultProps.upgradePlanPath);
it('renders enable checkbox as disabled', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBe('disabled');
});
}
it('does not render the Ultimate CTA', () => {
expect(findUltimateUpgradeCTA().exists()).toBe(false);
});
},
);

View file

@ -37,3 +37,11 @@ export const mockSectionConnection = {
title: 'Connection details',
description: 'Learn more on how to configure this integration.',
};
export const mockSectionJiraIssues = {
type: 'jira_issues',
title: 'Issues',
description:
'Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. Learn more.',
plan: 'premium',
};

View file

@ -54,12 +54,6 @@ describe('User Popovers', () => {
.mockImplementation((userId) => userStatusCacheSpy(userId));
jest.spyOn(UsersCache, 'updateById');
window.gon = {
features: {
followInUserPopover: true,
},
};
popovers = initUserPopovers(document.querySelectorAll(selector));
});

View file

@ -51,9 +51,7 @@ describe('User Popover Component', () => {
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button');
const createWrapper = (props = {}, { followInUserPopover = true } = {}) => {
gon.features.followInUserPopover = followInUserPopover;
const createWrapper = (props = {}) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
@ -304,132 +302,122 @@ describe('User Popover Component', () => {
});
});
describe('follow actions with `followInUserPopover` flag enabled', () => {
describe("when current user doesn't follow the user", () => {
beforeEach(() => createWrapper());
describe("when current user doesn't follow the user", () => {
beforeEach(() => createWrapper());
it('renders the Follow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe('Follow');
expect(findToggleFollowButton().props('variant')).toBe('confirm');
it('renders the Follow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe('Follow');
expect(findToggleFollowButton().props('variant')).toBe('confirm');
});
describe('when clicking', () => {
it('follows the user', async () => {
followUser.mockResolvedValue({});
await findToggleFollowButton().trigger('click');
expect(findToggleFollowButton().props('loading')).toBe(true);
await axios.waitForAll();
expect(wrapper.emitted().follow.length).toBe(1);
expect(wrapper.emitted().unfollow).toBeFalsy();
});
describe('when clicking', () => {
it('follows the user', async () => {
followUser.mockResolvedValue({});
describe('when an error occurs', () => {
beforeEach(() => {
followUser.mockRejectedValue({});
await findToggleFollowButton().trigger('click');
expect(findToggleFollowButton().props('loading')).toBe(true);
await axios.waitForAll();
expect(wrapper.emitted().follow.length).toBe(1);
expect(wrapper.emitted().unfollow).toBeFalsy();
findToggleFollowButton().trigger('click');
});
describe('when an error occurs', () => {
beforeEach(() => {
followUser.mockRejectedValue({});
it('shows an error message', async () => {
await axios.waitForAll();
findToggleFollowButton().trigger('click');
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while trying to follow this user, please try again.',
error: {},
captureError: true,
});
});
it('shows an error message', async () => {
await axios.waitForAll();
it('emits no events', async () => {
await axios.waitForAll();
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while trying to follow this user, please try again.',
error: {},
captureError: true,
});
});
it('emits no events', async () => {
await axios.waitForAll();
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow).toBe(undefined);
});
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow).toBe(undefined);
});
});
});
});
describe('when current user follows the user', () => {
beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
describe('when current user follows the user', () => {
beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
it('renders the Unfollow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe('Unfollow');
expect(findToggleFollowButton().props('variant')).toBe('default');
it('renders the Unfollow button with the correct variant', () => {
expect(findToggleFollowButton().text()).toBe('Unfollow');
expect(findToggleFollowButton().props('variant')).toBe('default');
});
describe('when clicking', () => {
it('unfollows the user', async () => {
unfollowUser.mockResolvedValue({});
findToggleFollowButton().trigger('click');
await axios.waitForAll();
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow.length).toBe(1);
});
describe('when clicking', () => {
it('unfollows the user', async () => {
unfollowUser.mockResolvedValue({});
describe('when an error occurs', () => {
beforeEach(async () => {
unfollowUser.mockRejectedValue({});
findToggleFollowButton().trigger('click');
await axios.waitForAll();
});
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while trying to unfollow this user, please try again.',
error: {},
captureError: true,
});
});
it('emits no events', () => {
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow.length).toBe(1);
expect(wrapper.emitted().unfollow).toBe(undefined);
});
describe('when an error occurs', () => {
beforeEach(async () => {
unfollowUser.mockRejectedValue({});
findToggleFollowButton().trigger('click');
await axios.waitForAll();
});
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while trying to unfollow this user, please try again.',
error: {},
captureError: true,
});
});
it('emits no events', () => {
expect(wrapper.emitted().follow).toBe(undefined);
expect(wrapper.emitted().unfollow).toBe(undefined);
});
});
});
});
describe('when the current user is the user', () => {
beforeEach(() => {
gon.current_username = DEFAULT_PROPS.user.username;
createWrapper();
});
it("doesn't render the toggle follow button", () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
describe('when API does not support `isFollowed`', () => {
beforeEach(() => {
const user = {
...DEFAULT_PROPS.user,
isFollowed: undefined,
};
createWrapper({ user });
});
it('does not render the toggle follow button', () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
});
describe('follow actions with `followInUserPopover` flag disabled', () => {
beforeEach(() => createWrapper({}, { followInUserPopover: false }));
describe('when the current user is the user', () => {
beforeEach(() => {
gon.current_username = DEFAULT_PROPS.user.username;
createWrapper();
});
it('doesnt render the toggle follow button', () => {
it("doesn't render the toggle follow button", () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
describe('when API does not support `isFollowed`', () => {
beforeEach(() => {
const user = {
...DEFAULT_PROPS.user,
isFollowed: undefined,
};
createWrapper({ user });
});
it('does not render the toggle follow button', () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});

View file

@ -33,138 +33,115 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
end
end
shared_examples 'returning the expected pipelines' do
it 'contains the expected pipelines' do
expect_to_contain_exactly(*pipelines)
end
context 'with valid after' do
let(:pagination_args) { { first: 1, after: encode_cursor(id: pipelines[1].id) } }
it 'contains the expected pipelines' do
expect_to_contain_exactly(*pipelines)
expect_to_contain_exactly(pipelines[0])
end
end
context 'with valid before' do
let(:pagination_args) { { last: 1, before: encode_cursor(id: pipelines[1].id) } }
it 'contains the expected pipelines' do
expect_to_contain_exactly(pipelines[2])
end
end
context 'with invalid after' do
let(:pagination_args) { { first: 1, after: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid after key' do
let(:pagination_args) { { first: 1, after: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before' do
let(:pagination_args) { { last: 1, before: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before key' do
let(:pagination_args) { { last: 1, before: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
it 'returns nothing' do
expect(returned_pipelines).to be_nil
end
end
context 'with many packages' do
let_it_be_with_reload(:other_package) { create(:package, project: package.project) }
let_it_be(:other_pipelines) { create_list(:ci_pipeline, 3, project: package.project) }
let(:returned_pipelines) do
graphql_dig_at(subject, 'data', 'project', 'packages', 'nodes', 'pipelines', 'nodes')
end
context 'with valid after' do
let(:pagination_args) { { first: 1, after: encode_cursor(id: pipelines[1].id) } }
it 'contains the expected pipelines' do
expect_to_contain_exactly(pipelines[0])
end
end
context 'with valid before' do
let(:pagination_args) { { last: 1, before: encode_cursor(id: pipelines[1].id) } }
it 'contains the expected pipelines' do
expect_to_contain_exactly(pipelines[2])
end
end
context 'with invalid after' do
let(:pagination_args) { { first: 1, after: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid after key' do
let(:pagination_args) { { first: 1, after: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before' do
let(:pagination_args) { { last: 1, before: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before key' do
let(:pagination_args) { { last: 1, before: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
it 'returns nothing' do
expect(returned_pipelines).to be_nil
end
end
context 'with many packages' do
let_it_be_with_reload(:other_package) { create(:package, project: package.project) }
let_it_be(:other_pipelines) { create_list(:ci_pipeline, 3, project: package.project) }
let(:returned_pipelines) do
graphql_dig_at(subject, 'data', 'project', 'packages', 'nodes', 'pipelines', 'nodes')
end
let(:query) do
pipelines_query = query_graphql_field('pipelines', pagination_args, 'nodes { id }')
<<~QUERY
{
project(fullPath: "#{package.project.full_path}") {
packages {
nodes { #{pipelines_query} }
}
let(:query) do
pipelines_query = query_graphql_field('pipelines', pagination_args, 'nodes { id }')
<<~QUERY
{
project(fullPath: "#{package.project.full_path}") {
packages {
nodes { #{pipelines_query} }
}
}
QUERY
end
before do
other_pipelines.each do |pipeline|
create(:package_build_info, package: other_package, pipeline: pipeline)
end
end
it 'contains the expected pipelines' do
expect_to_contain_exactly(*(pipelines + other_pipelines))
end
it 'handles n+1 situations' do
control = ActiveRecord::QueryRecorder.new do
GitlabSchema.execute(query, context: { current_user: user })
end
create_package_with_pipelines(package.project)
expectation = expect { GitlabSchema.execute(query, context: { current_user: user }) }
if Feature.enabled?(:packages_graphql_pipelines_resolver)
expectation.not_to exceed_query_limit(control)
else
expectation.to exceed_query_limit(control)
end
end
def create_package_with_pipelines(project)
extra_package = create(:package, project: project)
create_list(:ci_pipeline, 3, project: project).each do |pipeline|
create(:package_build_info, package: extra_package, pipeline: pipeline)
end
end
}
QUERY
end
end
context 'with packages_graphql_pipelines_resolver enabled' do
before do
expect_detect_mode([:new_finder])
other_pipelines.each do |pipeline|
create(:package_build_info, package: other_package, pipeline: pipeline)
end
end
it_behaves_like 'returning the expected pipelines'
end
context 'with packages_graphql_pipelines_resolver disabled' do
before do
stub_feature_flags(packages_graphql_pipelines_resolver: false)
expect_detect_mode([:old_finder, :object_field])
it 'contains the expected pipelines' do
expect_to_contain_exactly(*(pipelines + other_pipelines))
end
it_behaves_like 'returning the expected pipelines'
it 'handles n+1 situations' do
control = ActiveRecord::QueryRecorder.new do
GitlabSchema.execute(query, context: { current_user: user })
end
create_package_with_pipelines(package.project)
expectation = expect { GitlabSchema.execute(query, context: { current_user: user }) }
expectation.not_to exceed_query_limit(control)
end
def create_package_with_pipelines(project)
extra_package = create(:package, project: project)
create_list(:ci_pipeline, 3, project: project).each do |pipeline|
create(:package_build_info, package: extra_package, pipeline: pipeline)
end
end
end
def encode_cursor(json)
@ -178,18 +155,6 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
entities = pipelines.map { |pipeline| a_graphql_entity_for(pipeline) }
expect(returned_pipelines).to match_array(entities)
end
def expect_detect_mode(modes)
allow_next_instance_of(described_class) do |resolver|
detect_mode_method = resolver.method(:detect_mode)
allow(resolver).to receive(:detect_mode) do
result = detect_mode_method.call
expect(modes).to include(result)
result
end
end
end
end
describe '.field options' do

View file

@ -62,6 +62,7 @@ RSpec.describe IntegrationsHelper do
:enable_comments,
:comment_detail,
:learn_more_path,
:about_pricing_url,
:trigger_events,
:fields,
:inherit_from_id,

View file

@ -2228,12 +2228,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
describe '#ensure_batched_background_migration_is_finished' do
let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' }
let(:table) { :events }
let(:column_name) { :id }
let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] }
let(:configuration) do
{
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
table_name: :events,
column_name: :id,
job_arguments: [["id"], ["id_convert_to_bigint"], nil]
job_class_name: job_class_name,
table_name: table,
column_name: column_name,
job_arguments: job_arguments
}
end
@ -2242,6 +2247,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'raises an error when migration exists and is not marked as finished' do
create(:batched_background_migration, :active, configuration)
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
allow(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(false)
end
expect { ensure_batched_background_migration_is_finished }
.to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':" \
"\t#{configuration}" \
@ -2269,6 +2278,28 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect { ensure_batched_background_migration_is_finished }
.not_to raise_error
end
it 'finalizes the migration' do
migration = create(:batched_background_migration, :active, configuration)
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
expect(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(migration.finish!)
end
ensure_batched_background_migration_is_finished
end
context 'when the flag finalize is false' do
it 'does not finalize the migration' do
create(:batched_background_migration, :active, configuration)
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
expect(runner).not_to receive(:finalize).with(job_class_name, table, column_name, job_arguments)
end
expect { model.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError)
end
end
end
describe '#index_exists_by_name?' do

View file

@ -163,4 +163,24 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
end
end
end
describe '#finalize_batched_background_migration' do
let!(:batched_migration) { create(:batched_background_migration, job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: []) }
it 'finalizes the migration' do
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
expect(runner).to receive(:finalize).with('MyClass', :projects, :id, [])
end
migration.finalize_batched_background_migration(job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: [])
end
context 'when the migration does not exist' do
it 'raises an exception' do
expect do
migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: [])
end.to raise_error(RuntimeError, 'Could not find batched background migration')
end
end
end
end

View file

@ -9,9 +9,11 @@ RSpec.describe FinalizeProjectNamespacesBackfill, :migration do
let_it_be(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'raises migration not finished exception' do
it 'raises exception' do
expect { migrate! }.to raise_error(/Expected batched background migration for the given configuration to be marked as 'finished'/)
shared_examples 'finalizes the migration' do
it 'finalizes the migration' do
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
expect(runner).to receive(:finalize).with('"ProjectNamespaces::BackfillProjectNamespaces"', :projects, :id, [nil, "up"])
end
end
end
@ -61,7 +63,7 @@ RSpec.describe FinalizeProjectNamespacesBackfill, :migration do
project_namespace_backfill.update!(status: status)
end
it_behaves_like 'raises migration not finished exception'
it_behaves_like 'finalizes the migration'
end
end
end

View file

@ -126,6 +126,11 @@ RSpec.describe Integrations::Jira do
it 'includes SECTION_TYPE_JIRA_ISSUES' do
expect(sections).to include(described_class::SECTION_TYPE_JIRA_ISSUES)
end
it 'section SECTION_TYPE_JIRA_ISSUES has `plan` attribute' do
jira_issues_section = integration.sections.find { |s| s[:type] == described_class::SECTION_TYPE_JIRA_ISSUES }
expect(jira_issues_section[:plan]).to eq('premium')
end
end
context 'when project_level? is false' do

View file

@ -151,6 +151,13 @@ RSpec.describe API::Environments do
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
it 'returns a 400 status code with invalid states' do
get api("/projects/#{project.id}/environments?states=test", user)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include('Requested states are invalid')
end
end
end

View file

@ -27,7 +27,7 @@ RSpec.describe 'projects/tags/index.html.haml' do
it 'renders links to the Releases page for tags associated with a release' do
render
expect(rendered).to have_link(release.name, href: project_releases_path(project, anchor: release.tag))
expect(rendered).to have_link(release.name, href: project_release_path(project, release.tag))
end
context 'when the most recent build for a tag has artifacts' do

View file

@ -963,15 +963,15 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.1.0"
"@gitlab/svgs@2.12.0":
version "2.12.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.12.0.tgz#f4825c3e9b95d219935b62f71143f9f2fc48b0fe"
integrity sha512-FHd0ImNyj8ZIiH3Ah2esxbxNQlXMSezqkcUe0nm+aWkSFsg6MbMcRYa6WvOKq5CbAgWpH1TbDqokkJ421YUtEg==
"@gitlab/svgs@2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.14.0.tgz#92b36bc98ccbed49a4dbca310862146275091cb2"
integrity sha512-U9EYmEIiTMl7R3X5DmCrw6fz7gz8c1kjvQtaF6HfJ15xDtR7trRAyCNbn3z7YGk1QJ8Cv/Ifw2/T5SxXwYd7dw==
"@gitlab/ui@40.0.0":
version "40.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-40.0.0.tgz#5a48a2c1ad509317ff0fde07070bd2070c82bd1a"
integrity sha512-LLnhju89i3usX66lSWQod222EFQjkBkiHG6frrDotW2PnCE6wB/xjlXumwpi93zfdyvIHS12cWPZwNa3ayiLfQ==
"@gitlab/ui@40.2.0":
version "40.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-40.2.0.tgz#798630112809816afa0d37f58d567e8f1ad53f8e"
integrity sha512-3AbCh0UVB5xEUoPrwr2YkzM9IrNOW3LFmyYCXEuVTp7whHyHG14T+sty3YDQWlOFjjEdMD4fU2iXveq2V3cq0A==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"