Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
48d9e7ff8d
commit
90726a8ccc
45 changed files with 647 additions and 731 deletions
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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 },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -249,6 +249,7 @@ export default {
|
|||
</gl-link>
|
||||
<gl-modal
|
||||
modal-id="time-tracking-report"
|
||||
size="lg"
|
||||
:title="__('Time tracking report')"
|
||||
:hide-footer="true"
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
5
app/policies/namespace_ci_cd_setting_policy.rb
Normal file
5
app/policies/namespace_ci_cd_setting_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NamespaceCiCdSettingPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
|
||||
delegate { @subject.namespace }
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
10
data/removals/15_0/15-0-type.yml
Normal file
10
data/removals/15_0/15-0-type.yml
Normal 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.
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')")
|
||||
|
|
|
@ -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}" \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 },
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
@ -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('doesn’t 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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -62,6 +62,7 @@ RSpec.describe IntegrationsHelper do
|
|||
:enable_comments,
|
||||
:comment_detail,
|
||||
:learn_more_path,
|
||||
:about_pricing_url,
|
||||
:trigger_events,
|
||||
:fields,
|
||||
:inherit_from_id,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue