Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3bdc719293
commit
143a33345c
52 changed files with 641 additions and 249 deletions
|
@ -524,7 +524,7 @@ export default {
|
|||
|
||||
if (
|
||||
window.gon?.features?.diffsVirtualScrolling ||
|
||||
window.gon?.features?.diffSearchingUsageData
|
||||
window.gon?.features?.usageDataDiffSearches
|
||||
) {
|
||||
let keydownTime;
|
||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||
|
@ -540,7 +540,7 @@ export default {
|
|||
if (delta >= 0 && delta < 1000) {
|
||||
this.disableVirtualScroller();
|
||||
|
||||
if (window.gon?.features?.diffSearchingUsageData) {
|
||||
if (window.gon?.features?.usageDataDiffSearches) {
|
||||
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||
api.trackRedisCounterEvent('diff_searches');
|
||||
}
|
||||
|
|
|
@ -43,9 +43,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
selectedNamespaceId() {
|
||||
return this.selectedId;
|
||||
},
|
||||
disableSubmitButton() {
|
||||
return this.isPaidGroup || !this.selectedId;
|
||||
},
|
||||
|
|
|
@ -4,6 +4,10 @@ import { parseBoolean } from '~/lib/utils/common_utils';
|
|||
import TransferGroupForm, { i18n } from './components/transfer_group_form.vue';
|
||||
|
||||
const prepareGroups = (rawGroups) => {
|
||||
if (!rawGroups) {
|
||||
return { group: [] };
|
||||
}
|
||||
|
||||
const group = JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
|
||||
id,
|
||||
humanName,
|
||||
|
@ -22,7 +26,7 @@ export default () => {
|
|||
targetFormId = null,
|
||||
buttonText: confirmButtonText = '',
|
||||
groupName = '',
|
||||
parentGroups = [],
|
||||
parentGroups,
|
||||
isPaidGroup,
|
||||
} = el.dataset;
|
||||
|
||||
|
|
|
@ -578,7 +578,7 @@ export default {
|
|||
:endpoint="mr.accessibilityReportPath"
|
||||
/>
|
||||
|
||||
<div class="mr-widget-section">
|
||||
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
|
||||
<component :is="componentName" :mr="mr" :service="service" />
|
||||
<ready-to-merge
|
||||
v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"
|
||||
|
|
|
@ -16,8 +16,13 @@ export const i18n = {
|
|||
USERS: __('Users'),
|
||||
};
|
||||
|
||||
const filterByName = (data, searchTerm = '') =>
|
||||
data.filter((d) => d.humanName.toLowerCase().includes(searchTerm));
|
||||
const filterByName = (data, searchTerm = '') => {
|
||||
if (!searchTerm) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'NamespaceSelect',
|
||||
|
@ -85,7 +90,15 @@ export default {
|
|||
},
|
||||
filteredEmptyNamespaceTitle() {
|
||||
const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this;
|
||||
return includeEmptyNamespace && emptyNamespaceTitle.toLowerCase().includes(searchTerm);
|
||||
|
||||
if (!includeEmptyNamespace) {
|
||||
return '';
|
||||
}
|
||||
if (!searchTerm) {
|
||||
return emptyNamespaceTitle;
|
||||
}
|
||||
|
||||
return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -47,7 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
# Usage data feature flags
|
||||
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:diff_searching_usage_data, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:usage_data_diff_searches, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
|
||||
|
|
48
app/graphql/mutations/work_items/delete.rb
Normal file
48
app/graphql/mutations/work_items/delete.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module WorkItems
|
||||
class Delete < BaseMutation
|
||||
description "Deletes a work item." \
|
||||
" Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
|
||||
|
||||
graphql_name 'WorkItemDelete'
|
||||
|
||||
authorize :delete_work_item
|
||||
|
||||
argument :id, ::Types::GlobalIDType[::WorkItem],
|
||||
required: true,
|
||||
description: 'Global ID of the work item.'
|
||||
|
||||
field :project, Types::ProjectType,
|
||||
null: true,
|
||||
description: 'Project the deleted work item belonged to.'
|
||||
|
||||
def resolve(id:)
|
||||
work_item = authorized_find!(id: id)
|
||||
|
||||
unless Feature.enabled?(:work_items, work_item.project)
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
result = ::WorkItems::DeleteService.new(
|
||||
project: work_item.project,
|
||||
current_user: current_user
|
||||
).execute(work_item)
|
||||
|
||||
{
|
||||
project: result.success? ? work_item.project : nil,
|
||||
errors: result.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(id:)
|
||||
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -126,6 +126,7 @@ module Types
|
|||
mount_mutation Mutations::Packages::DestroyFile
|
||||
mount_mutation Mutations::Echo
|
||||
mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
|
||||
mount_mutation Mutations::WorkItems::Delete
|
||||
mount_mutation Mutations::WorkItems::Update
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,4 +2,11 @@
|
|||
|
||||
class WorkItemPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
|
||||
desc 'User is author of the work item'
|
||||
condition(:author) do
|
||||
@user && @user == @subject.author
|
||||
end
|
||||
|
||||
rule { can?(:owner_access) | author }.enable :delete_work_item
|
||||
end
|
||||
|
|
|
@ -139,10 +139,7 @@ module Projects
|
|||
destroy_web_hooks!
|
||||
destroy_project_bots!
|
||||
destroy_ci_records!
|
||||
|
||||
if ::Feature.enabled?(:extract_mr_diff_commit_deletions, default_enabled: :yaml)
|
||||
destroy_mr_diff_commits!
|
||||
end
|
||||
destroy_mr_diff_commits!
|
||||
|
||||
# Rails attempts to load all related records into memory before
|
||||
# destroying: https://github.com/rails/rails/issues/22510
|
||||
|
|
17
app/services/work_items/delete_service.rb
Normal file
17
app/services/work_items/delete_service.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
class DeleteService < Issuable::DestroyService
|
||||
def execute(work_item)
|
||||
unless current_user.can?(:delete_work_item, work_item)
|
||||
return ::ServiceResponse.error(message: 'User not authorized to delete work item')
|
||||
end
|
||||
|
||||
if super
|
||||
::ServiceResponse.success
|
||||
else
|
||||
::ServiceResponse.error(message: work_item.errors.full_messages)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,9 +8,7 @@
|
|||
= form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
|
||||
= s_('ProjectSettings|Pipelines must succeed')
|
||||
.text-secondary
|
||||
- configuring_pipelines_for_merge_requests_help_link_url = help_page_path('ci/pipelines/merge_request_pipelines.md', anchor: 'prerequisites')
|
||||
- configuring_pipelines_for_merge_requests_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configuring_pipelines_for_merge_requests_help_link_url }
|
||||
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
|
||||
= s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
|
||||
.form-check.mb-2
|
||||
.gl-pl-6
|
||||
= form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'
|
||||
|
|
|
@ -6,36 +6,6 @@
|
|||
- elsif note.contributor?
|
||||
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
|
||||
|
||||
- if note.resolvable?
|
||||
- can_resolve = can?(current_user, :resolve_note, note)
|
||||
%resolve-btn{ "project-path" => project_path(note.project),
|
||||
"discussion-id" => note.discussion_id(@noteable),
|
||||
":note-id" => note.id,
|
||||
":resolved" => note.resolved?,
|
||||
":can-resolve" => can_resolve,
|
||||
":author-name" => "'#{j(note.author.name)}'",
|
||||
"author-avatar" => note.author.avatar_url,
|
||||
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
|
||||
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
|
||||
"v-show" => "#{can_resolve || note.resolved?}",
|
||||
"inline-template" => true,
|
||||
"ref" => "note_#{note.id}" }
|
||||
|
||||
.note-actions-item
|
||||
%button.note-action-button.line-resolve-btn{ type: "button",
|
||||
class: ("is-disabled" unless can_resolve),
|
||||
":class" => "{ 'is-active': isResolved }",
|
||||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
":ref" => "'button'" }
|
||||
|
||||
%div
|
||||
%template{ 'v-if' => 'isResolved' }
|
||||
= render 'shared/icons/icon_status_success_solid.svg'
|
||||
%template{ 'v-else' => '' }
|
||||
= render 'shared/icons/icon_resolve_discussion.svg'
|
||||
|
||||
- if can?(current_user, :award_emoji, note)
|
||||
- if note.emoji_awardable?
|
||||
.note-actions-item
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>
|
Before Width: | Height: | Size: 409 B |
|
@ -1 +0,0 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 362 B |
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: extract_mr_diff_commit_deletions
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75963
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347073
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: diff_searching_usage_data
|
||||
name: usage_data_diff_searches
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone: '14.2'
|
|
@ -15,10 +15,12 @@ if Gitlab.ee?
|
|||
else
|
||||
Gitlab::Database::Partitioning.register_tables([
|
||||
{
|
||||
limit_connection_names: %i[main],
|
||||
table_name: 'incident_management_pending_alert_escalations',
|
||||
partitioned_column: :process_at, strategy: :monthly
|
||||
},
|
||||
{
|
||||
limit_connection_names: %i[main],
|
||||
table_name: 'incident_management_pending_issue_escalations',
|
||||
partitioned_column: :process_at, strategy: :monthly
|
||||
}
|
||||
|
@ -31,6 +33,7 @@ unless Gitlab.jh?
|
|||
# This should be synchronized with the following model:
|
||||
# https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/app/models/phone/verification_code.rb
|
||||
{
|
||||
limit_connection_names: %i[main],
|
||||
table_name: 'verification_codes',
|
||||
partitioned_column: :created_at, strategy: :monthly
|
||||
}
|
||||
|
|
4
config/initializers/rubyzip.rb
Normal file
4
config/initializers/rubyzip.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
# Zip64 is needed to support archives with more than 65535 entries.
|
||||
Zip.write_zip64_support = true
|
|
@ -23,7 +23,6 @@ The following lists the currently supported OSs and their possible EOL dates.
|
|||
| Debian 9 | GitLab CE / GitLab EE 9.3.0 | amd64 | 2022 | <https://wiki.debian.org/LTS> |
|
||||
| Debian 10 | GitLab CE / GitLab EE 12.2.0 | amd64, arm64 | 2024 | <https://wiki.debian.org/LTS> |
|
||||
| Debian 11 | GitLab CE / GitLab EE 14.6.0 | amd64, arm64 | 2026 | <https://wiki.debian.org/LTS> |
|
||||
| OpenSUSE 15.2 | GitLab CE / GitLab EE 13.11.0 | x86_64, aarch64 | Dec 2021 | <https://en.opensuse.org/Lifetime> |
|
||||
| OpenSUSE 15.3 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | Nov 2022 | <https://en.opensuse.org/Lifetime> |
|
||||
| SLES 12 | GitLab EE 9.0.0 | x86_64 | Oct 2027 | <https://www.suse.com/lifecycle/> |
|
||||
| Ubuntu 18.04 | GitLab CE / GitLab EE 10.7.0 | amd64 | April 2023 | <https://wiki.ubuntu.com/Releases> |
|
||||
|
@ -81,8 +80,9 @@ release for them can be found below:
|
|||
| Raspbian Stretch | [June 2020](https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/) | [GitLab CE](https://packages.gitlab.com/app/gitlab/raspberry-pi2/search?q=gitlab-ce_13.2&dist=raspbian%2Fstretch) 13.3 |
|
||||
| Debian Jessie | [June 2020](https://www.debian.org/News/2020/20200709) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.2&dist=debian%2Fjessie) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.2&dist=debian%2Fjessie) 13.3 |
|
||||
| CentOS 6 | [November 2020](https://wiki.centos.org/About/Product) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=13.6&filter=all&filter=all&dist=el%2F6) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=13.6&filter=all&filter=all&dist=el%2F6) 13.6 |
|
||||
| OpenSUSE 15.1 | [November 2020](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-13.12&dist=opensuse%2F15.1) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-13.12&dist=opensuse%2F15.2) 13.12 |
|
||||
| OpenSUSE 15.1 | [November 2020](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-13.12&dist=opensuse%2F15.1) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-13.12&dist=opensuse%2F15.1) 13.12 |
|
||||
| Ubuntu 16.04 | [April 2021](https://ubuntu.com/info/release-end-of-life) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.12&dist=ubuntu%2Fxenial) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.12&dist=ubuntu%2Fxenial) 13.12 |
|
||||
| OpenSUSE 15.2 | [December 2021](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-14.7&dist=opensuse%2F15.2) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-14.7&dist=opensuse%2F15.2) 14.7 |
|
||||
|
||||
NOTE:
|
||||
An exception to this deprecation policy is when we are unable to provide
|
||||
|
|
|
@ -5145,6 +5145,27 @@ Input type: `WorkItemCreateInput`
|
|||
| <a id="mutationworkitemcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationworkitemcreateworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Created work item. |
|
||||
|
||||
### `Mutation.workItemDelete`
|
||||
|
||||
Deletes a work item. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
|
||||
|
||||
Input type: `WorkItemDeleteInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemdeleteid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationworkitemdeleteproject"></a>`project` | [`Project`](#project) | Project the deleted work item belonged to. |
|
||||
|
||||
### `Mutation.workItemUpdate`
|
||||
|
||||
Updates a work item by Global ID. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
|
||||
|
|
|
@ -6,8 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Metrics Dictionary Guide
|
||||
|
||||
[Service Ping](index.md) metrics are defined in the
|
||||
[Metrics Dictionary](https://metrics.gitlab.com/).
|
||||
[Service Ping](index.md) metrics are defined in individual YAML files definitions from which the
|
||||
[Metrics Dictionary](https://metrics.gitlab.com/) is built.
|
||||
This guide describes the dictionary and how it's implemented.
|
||||
|
||||
## Metrics Definition and validation
|
||||
|
@ -194,7 +194,7 @@ tier:
|
|||
- ultimate
|
||||
```
|
||||
|
||||
## Create a new metric definition
|
||||
### Create a new metric definition
|
||||
|
||||
The GitLab codebase provides a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_definition_generator.rb) to create new metric definitions.
|
||||
|
||||
|
@ -229,7 +229,7 @@ bundle exec rails generate gitlab:usage_metric_definition counts.issues --ee --d
|
|||
create ee/config/metrics/counts_7d/issues.yml
|
||||
```
|
||||
|
||||
## Metrics added dynamic to Service Ping payload
|
||||
### Metrics added dynamic to Service Ping payload
|
||||
|
||||
The [Redis HLL metrics](implement.md#known-events-are-added-automatically-in-service-data-payload) are added automatically to Service Ping payload.
|
||||
|
||||
|
@ -250,3 +250,13 @@ bundle exec rails generate gitlab:usage_metric_definition:redis_hll issues users
|
|||
create config/metrics/counts_7d/i_closed_weekly.yml
|
||||
create config/metrics/counts_28d/i_closed_monthly.yml
|
||||
```
|
||||
|
||||
## Metrics Dictionary
|
||||
|
||||
[Metrics Dictionary is a separate application](https://gitlab.com/gitlab-org/growth/product-intelligence/metric-dictionary).
|
||||
|
||||
All metrics available in Service Ping are in the [Metrics Dictionary](https://metrics.gitlab.com/).
|
||||
|
||||
### Copy query to clipboard
|
||||
|
||||
To check if a metric has data in Sisense, use the copy query to clipboard feature. This copies a query that's ready to use in Sisense. The query gets the last five service ping data for GitLab.com for a given metric. For information about how to check if a Service Ping metric has data in Sisense, see this [demo](https://www.youtube.com/watch?v=n4o65ivta48).
|
||||
|
|
|
@ -256,7 +256,7 @@ expiration date without a gap in available service. An invoice is
|
|||
generated for the renewal and available for viewing or download on the
|
||||
[View invoices](https://customers.gitlab.com/receipts) page.
|
||||
|
||||
#### Enable automatic renewal
|
||||
#### Enable or disable automatic renewal
|
||||
|
||||
To view or change automatic subscription renewal (at the same tier as the
|
||||
previous period), log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in), and:
|
||||
|
@ -292,7 +292,7 @@ for more information.
|
|||
### Purchase additional CI/CD minutes
|
||||
|
||||
You can [purchase additional minutes](../../ci/pipelines/cicd_minutes.md#purchase-additional-cicd-minutes)
|
||||
for your personal or group namespace.
|
||||
for your personal or group namespace. CI/CD minutes are a **one-time purchase**, so they do not renew.
|
||||
|
||||
## Add-on subscription for additional Storage and Transfer
|
||||
|
||||
|
@ -309,7 +309,11 @@ locked. Projects can only be unlocked by purchasing more storage subscription un
|
|||
|
||||
### Purchase more storage and transfer
|
||||
|
||||
You can purchase storage for your personal or group namespace.
|
||||
You can purchase a storage subscription for your personal or group namespace.
|
||||
|
||||
NOTE:
|
||||
Storage subscriptions **[renew automatically](#automatic-renewal) each year**.
|
||||
You can [cancel the subscription](#enable-or-disable-automatic-renewal) to disable the automatic renewal.
|
||||
|
||||
#### For your personal namespace
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ to Kubernetes clusters using the [GitLab Agent](../user/clusters/agent/install/i
|
|||
|
||||
#### GitOps deployments **(PREMIUM)**
|
||||
|
||||
With the [GitLab Agent](../user/clusters/agent/install/index.md), you can perform pull-based
|
||||
deployments using Kubernetes manifests. This provides a scalable, secure, and cloud-native
|
||||
With the [GitLab Agent](../user/clusters/agent/install/index.md), you can perform [pull-based
|
||||
deployments of Kubernetes manifests](../user/clusters/agent/repository.md#synchronize-manifest-projects). This provides a scalable, secure, and cloud-native
|
||||
approach to manage Kubernetes deployments.
|
||||
|
||||
#### Deploy to Kubernetes with the CI/CD Tunnel
|
||||
|
|
|
@ -82,7 +82,7 @@ For more details, refer to our [architecture documentation](https://gitlab.com/g
|
|||
|
||||
## Install the Agent in your cluster
|
||||
|
||||
See how to [install the Agent in your cluster](install/index.md).
|
||||
To connect your cluster to GitLab, [install the Agent on your cluster](install/index.md).
|
||||
|
||||
## GitOps deployments **(PREMIUM)**
|
||||
|
||||
|
|
|
@ -229,3 +229,16 @@ approval rule for certain branches:
|
|||
![Scoped to protected branch](img/scoped_to_protected_branch_v13_10.png)
|
||||
1. To enable this configuration, read
|
||||
[Code Owner's approvals for protected branches](../../protected_branches.md#require-code-owner-approval-on-a-protected-branch).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Approval rule name can't be blank
|
||||
|
||||
As a workaround for this validation error, you can delete the approval rule through
|
||||
the API.
|
||||
|
||||
1. [GET a project-level rule](../../../../api/merge_request_approvals.md#get-a-single-project-level-rule).
|
||||
1. [DELETE the rule](../../../../api/merge_request_approvals.md#delete-project-level-rule).
|
||||
|
||||
For more information about this validation error, read
|
||||
[issue 285129](https://gitlab.com/gitlab-org/gitlab/-/issues/285129).
|
||||
|
|
|
@ -14,18 +14,40 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def each_model_connection(models)
|
||||
def each_model_connection(models, &blk)
|
||||
models.each do |model|
|
||||
connection_name = model.connection.pool.db_config.name
|
||||
|
||||
with_shared_connection(model.connection, connection_name) do
|
||||
yield model, connection_name
|
||||
# If model is shared, iterate all available base connections
|
||||
# Example: `LooseForeignKeys::DeletedRecord`
|
||||
if model < ::Gitlab::Database::SharedModel
|
||||
with_shared_model_connections(model, &blk)
|
||||
else
|
||||
with_model_connection(model, &blk)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_shared_model_connections(shared_model, &blk)
|
||||
Gitlab::Database.database_base_models.each_pair do |connection_name, connection_model|
|
||||
if shared_model.limit_connection_names
|
||||
next unless shared_model.limit_connection_names.include?(connection_name.to_sym)
|
||||
end
|
||||
|
||||
with_shared_connection(connection_model.connection, connection_name) do
|
||||
yield shared_model, connection_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def with_model_connection(model, &blk)
|
||||
connection_name = model.connection.pool.db_config.name
|
||||
|
||||
with_shared_connection(model.connection, connection_name) do
|
||||
yield model, connection_name
|
||||
end
|
||||
end
|
||||
|
||||
def with_shared_connection(connection, connection_name)
|
||||
Gitlab::Database::SharedModel.using_connection(connection) do
|
||||
Gitlab::AppLogger.debug(message: 'Switched database connection', connection_name: connection_name)
|
||||
|
|
|
@ -3,19 +3,8 @@
|
|||
module Gitlab
|
||||
module Database
|
||||
module Partitioning
|
||||
class TableWithoutModel
|
||||
include PartitionedTable::ClassMethods
|
||||
|
||||
attr_reader :table_name
|
||||
|
||||
def initialize(table_name:, partitioned_column:, strategy:)
|
||||
@table_name = table_name
|
||||
partitioned_by(partitioned_column, strategy: strategy)
|
||||
end
|
||||
|
||||
def connection
|
||||
Gitlab::Database::SharedModel.connection
|
||||
end
|
||||
class TableWithoutModel < Gitlab::Database::SharedModel
|
||||
include PartitionedTable
|
||||
end
|
||||
|
||||
class << self
|
||||
|
@ -77,7 +66,15 @@ module Gitlab
|
|||
|
||||
def registered_for_sync
|
||||
registered_models + registered_tables.map do |table|
|
||||
TableWithoutModel.new(**table)
|
||||
table_without_model(**table)
|
||||
end
|
||||
end
|
||||
|
||||
def table_without_model(table_name:, partitioned_column:, strategy:, limit_connection_names: nil)
|
||||
Class.new(TableWithoutModel).tap do |klass|
|
||||
klass.table_name = table_name
|
||||
klass.partitioned_by(partitioned_column, strategy: strategy)
|
||||
klass.limit_connection_names = limit_connection_names
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,10 +12,15 @@ module Gitlab
|
|||
|
||||
def initialize(model)
|
||||
@model = model
|
||||
@connection_name = model.connection.pool.db_config.name
|
||||
end
|
||||
|
||||
def sync_partitions
|
||||
Gitlab::AppLogger.info(message: "Checking state of dynamic postgres partitions", table_name: model.table_name)
|
||||
Gitlab::AppLogger.info(
|
||||
message: "Checking state of dynamic postgres partitions",
|
||||
table_name: model.table_name,
|
||||
connection_name: @connection_name
|
||||
)
|
||||
|
||||
# Double-checking before getting the lease:
|
||||
# The prevailing situation is no missing partitions and no extra partitions
|
||||
|
@ -29,10 +34,13 @@ module Gitlab
|
|||
detach(partitions_to_detach) unless partitions_to_detach.empty?
|
||||
end
|
||||
rescue StandardError => e
|
||||
Gitlab::AppLogger.error(message: "Failed to create / detach partition(s)",
|
||||
table_name: model.table_name,
|
||||
exception_class: e.class,
|
||||
exception_message: e.message)
|
||||
Gitlab::AppLogger.error(
|
||||
message: "Failed to create / detach partition(s)",
|
||||
table_name: model.table_name,
|
||||
exception_class: e.class,
|
||||
exception_message: e.message,
|
||||
connection_name: @connection_name
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -98,9 +106,12 @@ module Gitlab
|
|||
Postgresql::DetachedPartition.create!(table_name: partition.partition_name,
|
||||
drop_after: RETAIN_DETACHED_PARTITIONS_FOR.from_now)
|
||||
|
||||
Gitlab::AppLogger.info(message: "Detached Partition",
|
||||
partition_name: partition.partition_name,
|
||||
table_name: partition.table)
|
||||
Gitlab::AppLogger.info(
|
||||
message: "Detached Partition",
|
||||
partition_name: partition.partition_name,
|
||||
table_name: partition.table,
|
||||
connection_name: @connection_name
|
||||
)
|
||||
end
|
||||
|
||||
def assert_partition_detachable!(partition)
|
||||
|
|
|
@ -6,6 +6,10 @@ module Gitlab
|
|||
class SharedModel < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
|
||||
# if shared model is used, this allows to limit connections
|
||||
# on which this model is being shared
|
||||
class_attribute :limit_connection_names, default: nil
|
||||
|
||||
class << self
|
||||
def using_connection(connection)
|
||||
previous_connection = self.overriding_connection
|
||||
|
|
|
@ -10,10 +10,19 @@ module Gitlab
|
|||
Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
|
||||
|
||||
# Returns [stdout + stderr, status]
|
||||
# status is either the exit code or the signal that killed the process
|
||||
def popen(cmd, path = nil, vars = {}, &block)
|
||||
result = popen_with_detail(cmd, path, vars, &block)
|
||||
|
||||
["#{result.stdout}#{result.stderr}", result.status&.exitstatus]
|
||||
# Process#waitpid returns Process::Status, which holds a 16-bit value.
|
||||
# The higher-order 8 bits hold the exit() code (`exitstatus`).
|
||||
# The lower-order bits holds whether the process was terminated.
|
||||
# If the process didn't exit normally, `exitstatus` will be `nil`,
|
||||
# but we still want a non-zero code, even if the value is
|
||||
# platform-dependent.
|
||||
status = result.status&.exitstatus || result.status.to_i
|
||||
|
||||
["#{result.stdout}#{result.stderr}", status]
|
||||
end
|
||||
|
||||
# Returns Result
|
||||
|
|
|
@ -236,7 +236,7 @@
|
|||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
feature_flag: diff_searching_usage_data
|
||||
feature_flag: usage_data_diff_searches
|
||||
- name: i_code_review_total_suggestions_applied
|
||||
redis_slot: code_review
|
||||
category: code_review
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module LearnGitlab
|
||||
class Project
|
||||
PROJECT_NAME = 'Learn GitLab'
|
||||
PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
|
||||
BOARD_NAME = 'GitLab onboarding'
|
||||
LABEL_NAME = 'Novice'
|
||||
|
||||
|
@ -15,7 +16,7 @@ module LearnGitlab
|
|||
end
|
||||
|
||||
def project
|
||||
@project ||= current_user.projects.find_by_name(PROJECT_NAME)
|
||||
@project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL])
|
||||
end
|
||||
|
||||
def board
|
||||
|
|
|
@ -28213,6 +28213,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Merge requests approved for merge are queued, and pipelines validate the combined results of the source and target branches before merge. %{link_start}What are merge trains?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Merge suggestions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28339,9 +28342,6 @@ msgstr ""
|
|||
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Transfer project"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.2.0",
|
||||
"@gitlab/ui": "34.0.0",
|
||||
"@gitlab/ui": "35.0.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "6.1.4-1",
|
||||
"@rails/ujs": "6.1.4-1",
|
||||
|
|
|
@ -157,6 +157,7 @@ module QA
|
|||
end
|
||||
|
||||
def redirect_to_login_page(address)
|
||||
Menu.perform(&:sign_out_if_signed_in)
|
||||
desired_host = URI(Runtime::Scenario.send("#{address}_address")).host
|
||||
Runtime::Browser.visit(address, Page::Main::Login) if desired_host != current_host
|
||||
end
|
||||
|
|
|
@ -83,10 +83,18 @@ module QA
|
|||
element :merge_immediately_menu_item
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
|
||||
element :head_mismatch_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do
|
||||
element :squash_checkbox
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue' do
|
||||
element :mr_widget_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do
|
||||
element :apply_suggestion_dropdown
|
||||
element :commit_message_field
|
||||
|
@ -269,13 +277,29 @@ module QA
|
|||
has_element?(:merge_button, disabled: false)
|
||||
end
|
||||
|
||||
# Waits up 60 seconds and raises an error if unable to merge
|
||||
def wait_until_ready_to_merge
|
||||
has_element?(:merge_button)
|
||||
# Waits up 60 seconds and raises an error if unable to merge.
|
||||
#
|
||||
# If a state is encountered in which a user would typically refresh the page, this will refresh the page and
|
||||
# then check again if it's ready to merge. For example, it will refresh if a new change was pushed and the page
|
||||
# needs to be refreshed to show the change.
|
||||
#
|
||||
# @param [Boolean] transient_test true if the current test is a transient test (default: false)
|
||||
def wait_until_ready_to_merge(transient_test: false)
|
||||
wait_until do
|
||||
has_element?(:merge_button)
|
||||
|
||||
# The merge button is enabled via JS
|
||||
wait_until(reload: false) do
|
||||
!find_element(:merge_button).disabled?
|
||||
break true unless find_element(:merge_button).disabled?
|
||||
|
||||
# If the widget shows "Merge blocked: new changes were just added" we can refresh the page and check again
|
||||
next false if has_element?(:head_mismatch_content)
|
||||
|
||||
# Stop waiting if we're in a transient test. By this point we're in an unexpected state and should let the
|
||||
# test fail so we can investigate. If we're not in a transient test we keep trying until we reach timeout.
|
||||
next true unless transient_test
|
||||
|
||||
QA::Runtime::Logger.debug("MR widget text: #{mr_widget_text}")
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -385,6 +409,10 @@ module QA
|
|||
def cancel_auto_merge!
|
||||
click_element(:cancel_auto_merge_button)
|
||||
end
|
||||
|
||||
def mr_widget_text
|
||||
find_element(:mr_widget_content).text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,8 @@ module QA
|
|||
let!(:source_comment) { source_mr.add_comment('This is a test comment!') }
|
||||
|
||||
let(:imported_mrs) { imported_project.merge_requests }
|
||||
let(:imported_mr_comments) { imported_mr.comments }
|
||||
let(:imported_mr_comments) { imported_mr.comments.map { |note| note.except(:id, :noteable_id) } }
|
||||
let(:source_mr_comments) { source_mr.comments.map { |note| note.except(:id, :noteable_id) } }
|
||||
|
||||
let(:imported_mr) do
|
||||
Resource::MergeRequest.init do |mr|
|
||||
|
@ -53,8 +54,7 @@ module QA
|
|||
aggregate_failures do
|
||||
expect(imported_mr).to eq(source_mr.reload!)
|
||||
|
||||
expect(imported_mr_comments.count).to eq(1)
|
||||
expect(imported_mr_comments.first.except(:id, :noteable_id)).to eq(source_comment.except(:id, :noteable_id))
|
||||
expect(imported_mr_comments).to eq(source_mr_comments)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,25 +20,6 @@ module QA
|
|||
end
|
||||
|
||||
before do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~EOF
|
||||
test:
|
||||
tags: ["runner-for-#{project.name}"]
|
||||
script: sleep 20
|
||||
only:
|
||||
- merge_requests
|
||||
EOF
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
|
@ -48,8 +29,10 @@ module QA
|
|||
end
|
||||
|
||||
it 'merges after pipeline succeeds' do
|
||||
transient_test = repeat > 1
|
||||
|
||||
repeat.times do |i|
|
||||
QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if repeat > 1
|
||||
QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if transient_test
|
||||
|
||||
branch_name = "mr-test-#{SecureRandom.hex(6)}-#{i}"
|
||||
|
||||
|
@ -68,19 +51,59 @@ module QA
|
|||
merge_request.no_preparation = true
|
||||
end
|
||||
|
||||
# Load the page so that the browser is as prepared as possible to display the pipeline in progress when we
|
||||
# start it.
|
||||
merge_request.visit!
|
||||
|
||||
Page::MergeRequest::Show.perform do |mr|
|
||||
mr.merge_when_pipeline_succeeds!
|
||||
# Push a new pipeline config file
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.branch = branch_name
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~EOF
|
||||
test:
|
||||
tags: ["runner-for-#{project.name}"]
|
||||
script: sleep 20
|
||||
only:
|
||||
- merge_requests
|
||||
EOF
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
Support::Waiter.wait_until(sleep_interval: 5) do
|
||||
Page::MergeRequest::Show.perform do |mr|
|
||||
refresh
|
||||
|
||||
# Part of the challenge with this test is that the MR widget has many components that could be displayed
|
||||
# and many errors states that those components could encounter. Most of the time few of those
|
||||
# possible components will be relevant, so it would be inefficient for this test to check for each of
|
||||
# them. Instead, we fail on anything but the expected state.
|
||||
#
|
||||
# The following method allows us to handle and ignore states (as we find them) that users could safely ignore.
|
||||
mr.wait_until_ready_to_merge(transient_test: transient_test)
|
||||
|
||||
mr.retry_until(reload: true, message: 'Wait until ready to click MWPS') do
|
||||
merge_request = merge_request.reload!
|
||||
merge_request.state == 'merged'
|
||||
|
||||
# Don't try to click MWPS if the MR is merged or the pipeline is complete
|
||||
break if merge_request.state == 'merged' || project.pipelines.last[:status] == 'success'
|
||||
|
||||
# Try to click MWPS if this is a transient test, or if the MWPS button is visible,
|
||||
# otherwise reload the page and retry
|
||||
next false unless transient_test || mr.has_element?(:merge_button, text: 'Merge when pipeline succeeds')
|
||||
|
||||
# No need to keep retrying if we can click MWPS
|
||||
break mr.merge_when_pipeline_succeeds!
|
||||
end
|
||||
|
||||
aggregate_failures do
|
||||
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
|
||||
expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear."
|
||||
expect(merge_request.reload!.merge_when_pipeline_succeeds).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,20 +10,20 @@ if [ $# -eq 0 ]; then
|
|||
exit
|
||||
fi
|
||||
|
||||
files=( $@ )
|
||||
files=( "$@" )
|
||||
len=${#files[@]}
|
||||
target=${files[$len-1]}
|
||||
|
||||
# Trap interrupts and exit instead of continuing the loop
|
||||
trap "echo Exited!; exit 2;" SIGINT SIGTERM
|
||||
|
||||
# Show which set of specs are running
|
||||
set -x
|
||||
# Show which set of specs are running and exit immediately if they fail.
|
||||
set -xe
|
||||
|
||||
# Do the speedy case first, run each spec with our failing spec
|
||||
for file in "${files[@]}"; do
|
||||
bin/rspec $file $target
|
||||
bin/rspec "$file" "$target"
|
||||
done
|
||||
|
||||
# Do a full bisect given we did not find candidates with speedy cases
|
||||
bin/rspec --bisect=verbose $@
|
||||
bin/rspec --bisect=verbose "$@"
|
||||
|
|
|
@ -17,7 +17,6 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
|
|||
<gl-tabs-stub
|
||||
contentclass="pt-0"
|
||||
queryparamname="tab"
|
||||
theme="indigo"
|
||||
value="0"
|
||||
>
|
||||
<gl-tab-stub
|
||||
|
|
|
@ -14,7 +14,6 @@ exports[`Code navigation popover component renders popover 1`] = `
|
|||
contentclass="gl-py-0"
|
||||
navclass="gl-hidden"
|
||||
queryparamname="tab"
|
||||
theme="indigo"
|
||||
value="0"
|
||||
>
|
||||
<gl-tab-stub
|
||||
|
|
|
@ -40,7 +40,6 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
|
|||
>
|
||||
<gl-tabs-stub
|
||||
queryparamname="tab"
|
||||
theme="indigo"
|
||||
value="0"
|
||||
>
|
||||
<!---->
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import NamespaceSelect, {
|
||||
i18n,
|
||||
|
@ -7,6 +7,10 @@ import NamespaceSelect, {
|
|||
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
|
||||
import { user, group, namespaces } from './mock_data';
|
||||
|
||||
const FLAT_NAMESPACES = [...group, ...user];
|
||||
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
|
||||
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
|
||||
|
||||
describe('Namespace Select', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -16,67 +20,97 @@ describe('Namespace Select', () => {
|
|||
data: namespaces,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
// We have to "full" mount GlDropdown so that slot children will render
|
||||
GlDropdown,
|
||||
},
|
||||
});
|
||||
|
||||
const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
|
||||
const flatNamespaces = () => [...group, ...user];
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDropdownAttributes = (attr) => findDropdown().attributes(attr);
|
||||
const selectedDropdownItemText = () => findDropdownAttributes('text');
|
||||
const findDropdownText = () => findDropdown().props('text');
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
|
||||
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const search = (term) => findSearchBox().vm.$emit('input', term);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders the dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
it('renders the dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders each dropdown item', () => {
|
||||
expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
|
||||
});
|
||||
|
||||
it('renders default dropdown text', () => {
|
||||
expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
|
||||
});
|
||||
|
||||
it('splits group and user namespaces', () => {
|
||||
const headers = findSectionHeaders();
|
||||
expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
|
||||
});
|
||||
|
||||
it('does not render wrapper as full width', () => {
|
||||
expect(findDropdown().attributes('block')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('can override the default text', () => {
|
||||
it('with defaultText, it overrides dropdown text', () => {
|
||||
const textOverride = 'Select an option';
|
||||
|
||||
wrapper = createComponent({ defaultText: textOverride });
|
||||
expect(selectedDropdownItemText()).toBe(textOverride);
|
||||
|
||||
expect(findDropdownText()).toBe(textOverride);
|
||||
});
|
||||
|
||||
it('renders each dropdown item', () => {
|
||||
const items = findDropdownItems().wrappers;
|
||||
expect(items).toHaveLength(flatNamespaces().length);
|
||||
});
|
||||
|
||||
it('renders the human name for each item', () => {
|
||||
const dropdownItems = wrappersText(findDropdownItems());
|
||||
const flatNames = flatNamespaces().map(({ humanName }) => humanName);
|
||||
expect(dropdownItems).toEqual(flatNames);
|
||||
});
|
||||
|
||||
it('sets the initial dropdown text', () => {
|
||||
expect(selectedDropdownItemText()).toBe(i18n.DEFAULT_TEXT);
|
||||
});
|
||||
|
||||
it('splits group and user namespaces', () => {
|
||||
const headers = findSectionHeaders();
|
||||
expect(headers).toHaveLength(2);
|
||||
expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
|
||||
});
|
||||
|
||||
it('can hide the group / user headers', () => {
|
||||
it('with includeHeaders=false, hides group/user headers', () => {
|
||||
wrapper = createComponent({ includeHeaders: false });
|
||||
|
||||
expect(findSectionHeaders()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('sets the dropdown to full width', () => {
|
||||
expect(findDropdownAttributes('block')).toBeUndefined();
|
||||
|
||||
it('with fullWidth=true, sets the dropdown to full width', () => {
|
||||
wrapper = createComponent({ fullWidth: true });
|
||||
|
||||
expect(findDropdownAttributes('block')).not.toBeUndefined();
|
||||
expect(findDropdownAttributes('block')).toBe('true');
|
||||
expect(findDropdown().attributes('block')).toBe('true');
|
||||
});
|
||||
|
||||
describe('with search', () => {
|
||||
it.each`
|
||||
term | includeEmptyNamespace | expectedItems
|
||||
${''} | ${false} | ${[...namespaces.group, ...namespaces.user]}
|
||||
${'sub'} | ${false} | ${[namespaces.group[1]]}
|
||||
${'User'} | ${false} | ${[...namespaces.user]}
|
||||
${'User'} | ${true} | ${[...namespaces.user]}
|
||||
${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...namespaces.user]}
|
||||
`(
|
||||
'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
|
||||
async ({ term, includeEmptyNamespace, expectedItems }) => {
|
||||
wrapper = createComponent({
|
||||
includeEmptyNamespace,
|
||||
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
|
||||
});
|
||||
|
||||
search(term);
|
||||
|
||||
await nextTick();
|
||||
|
||||
const expected = expectedItems.map((x) => x.humanName);
|
||||
|
||||
expect(findDropdownItemsTexts()).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with a selected namespace', () => {
|
||||
|
@ -84,11 +118,13 @@ describe('Namespace Select', () => {
|
|||
const selectedItem = group[selectedGroupIndex];
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
|
||||
findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
|
||||
});
|
||||
|
||||
it('sets the dropdown text', () => {
|
||||
expect(selectedDropdownItemText()).toBe(selectedItem.humanName);
|
||||
expect(findDropdownText()).toBe(selectedItem.humanName);
|
||||
});
|
||||
|
||||
it('emits the `select` event when a namespace is selected', () => {
|
||||
|
@ -98,27 +134,35 @@ describe('Namespace Select', () => {
|
|||
});
|
||||
|
||||
describe('with an empty namespace option', () => {
|
||||
const emptyNamespaceTitle = 'No namespace selected';
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
includeEmptyNamespace: true,
|
||||
emptyNamespaceTitle,
|
||||
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
|
||||
});
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('includes the empty namespace', () => {
|
||||
const first = findDropdownItems().at(0);
|
||||
expect(first.text()).toBe(emptyNamespaceTitle);
|
||||
|
||||
expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
|
||||
});
|
||||
|
||||
it('emits the `select` event when a namespace is selected', () => {
|
||||
findDropdownItems().at(0).vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([
|
||||
[{ id: EMPTY_NAMESPACE_ID, humanName: emptyNamespaceTitle }],
|
||||
]);
|
||||
expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
|
||||
});
|
||||
|
||||
it.each`
|
||||
desc | term | shouldShow
|
||||
${'should hide empty option'} | ${'group'} | ${false}
|
||||
${'should show empty option'} | ${'Empty'} | ${true}
|
||||
`('when search for $term, $desc', async ({ term, shouldShow }) => {
|
||||
search(term);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,45 +4,97 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Database::EachDatabase do
|
||||
describe '.each_database_connection' do
|
||||
let(:expected_connections) do
|
||||
Gitlab::Database.database_base_models.map { |name, model| [model.connection, name] }
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:database_base_models)
|
||||
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'yields each connection after connecting SharedModel' do
|
||||
expected_connections.each do |connection, _|
|
||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
|
||||
end
|
||||
it 'yields each connection after connecting SharedModel', :add_ci_connection do
|
||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
||||
.with(ActiveRecord::Base.connection).ordered.and_yield
|
||||
|
||||
yielded_connections = []
|
||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
|
||||
.with(Ci::ApplicationRecord.connection).ordered.and_yield
|
||||
|
||||
described_class.each_database_connection do |connection, name|
|
||||
yielded_connections << [connection, name]
|
||||
end
|
||||
|
||||
expect(yielded_connections).to match_array(expected_connections)
|
||||
expect { |b| described_class.each_database_connection(&b) }
|
||||
.to yield_successive_args(
|
||||
[ActiveRecord::Base.connection, 'main'],
|
||||
[Ci::ApplicationRecord.connection, 'ci']
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.each_model_connection' do
|
||||
let(:model1) { double(connection: double, table_name: 'table1') }
|
||||
let(:model2) { double(connection: double, table_name: 'table2') }
|
||||
context 'when the model inherits from SharedModel', :add_ci_connection do
|
||||
let(:model1) { Class.new(Gitlab::Database::SharedModel) }
|
||||
let(:model2) { Class.new(Gitlab::Database::SharedModel) }
|
||||
|
||||
before do
|
||||
allow(model1.connection).to receive_message_chain('pool.db_config.name').and_return('name1')
|
||||
allow(model2.connection).to receive_message_chain('pool.db_config.name').and_return('name2')
|
||||
end
|
||||
|
||||
it 'yields each model after connecting SharedModel' do
|
||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield
|
||||
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield
|
||||
|
||||
yielded_models = []
|
||||
|
||||
described_class.each_model_connection([model1, model2]) do |model, name|
|
||||
yielded_models << [model, name]
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:database_base_models)
|
||||
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
|
||||
end
|
||||
|
||||
expect(yielded_models).to match_array([[model1, 'name1'], [model2, 'name2']])
|
||||
it 'yields each model with SharedModel connected to each database connection' do
|
||||
expect_yielded_models([model1, model2], [
|
||||
{ model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
|
||||
{ model: model1, connection: Ci::ApplicationRecord.connection, name: 'ci' },
|
||||
{ model: model2, connection: ActiveRecord::Base.connection, name: 'main' },
|
||||
{ model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
|
||||
])
|
||||
end
|
||||
|
||||
context 'when the model limits connection names' do
|
||||
before do
|
||||
model1.limit_connection_names = %i[main]
|
||||
model2.limit_connection_names = %i[ci]
|
||||
end
|
||||
|
||||
it 'only yields the model with SharedModel connected to the limited connections' do
|
||||
expect_yielded_models([model1, model2], [
|
||||
{ model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
|
||||
{ model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the model does not inherit from SharedModel' do
|
||||
let(:main_model) { Class.new(ActiveRecord::Base) }
|
||||
let(:ci_model) { Class.new(Ci::ApplicationRecord) }
|
||||
|
||||
let(:main_connection) { double(:connection) }
|
||||
let(:ci_connection) { double(:connection) }
|
||||
|
||||
before do
|
||||
allow(main_model).to receive(:connection).and_return(main_connection)
|
||||
allow(ci_model).to receive(:connection).and_return(ci_connection)
|
||||
|
||||
allow(main_connection).to receive_message_chain('pool.db_config.name').and_return('main')
|
||||
allow(ci_connection).to receive_message_chain('pool.db_config.name').and_return('ci')
|
||||
end
|
||||
|
||||
it 'yields each model after connecting SharedModel' do
|
||||
expect_yielded_models([main_model, ci_model], [
|
||||
{ model: main_model, connection: main_connection, name: 'main' },
|
||||
{ model: ci_model, connection: ci_connection, name: 'ci' }
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
def expect_yielded_models(models_to_iterate, expected_values)
|
||||
times_yielded = 0
|
||||
|
||||
described_class.each_model_connection(models_to_iterate) do |model, name|
|
||||
expected = expected_values[times_yielded]
|
||||
|
||||
expect(model).to be(expected[:model])
|
||||
expect(model.connection).to be(expected[:connection])
|
||||
expect(name).to eq(expected[:name])
|
||||
|
||||
times_yielded += 1
|
||||
end
|
||||
|
||||
expect(times_yielded).to eq(expected_values.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do
|
|||
it { expect(@output).to include('No such file or directory') }
|
||||
end
|
||||
|
||||
context 'non-zero status with a kill' do
|
||||
let(:cmd) { [Gem.ruby, "-e", "thr = Thread.new { sleep 5 }; Process.kill(9, Process.pid); thr.join"] }
|
||||
|
||||
before do
|
||||
@output, @status = @klass.new.popen(cmd)
|
||||
end
|
||||
|
||||
it { expect(@status).to eq(9) }
|
||||
it { expect(@output).to be_empty }
|
||||
end
|
||||
|
||||
context 'unsafe string command' do
|
||||
it 'raises an error when it gets called with a string argument' do
|
||||
expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError)
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe LearnGitlab::Project do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) }
|
||||
let_it_be(:learn_gitlab_ultimate_trial_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
|
||||
let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) }
|
||||
let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) }
|
||||
|
||||
|
@ -45,6 +46,12 @@ RSpec.describe LearnGitlab::Project do
|
|||
subject { described_class.new(current_user).project }
|
||||
|
||||
it { is_expected.to eq learn_gitlab_project }
|
||||
|
||||
context 'when it is created during trial signup' do
|
||||
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
|
||||
|
||||
it { is_expected.to eq learn_gitlab_project }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.board' do
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Delete a work item' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
|
||||
|
||||
let(:current_user) { developer }
|
||||
let(:mutation) { graphql_mutation(:workItemDelete, { 'id' => work_item.to_global_id.to_s }) }
|
||||
let(:mutation_response) { graphql_mutation_response(:work_item_delete) }
|
||||
|
||||
context 'when the user is not allowed to delete a work item' do
|
||||
let(:work_item) { create(:work_item, project: project) }
|
||||
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
context 'when user has permissions to delete a work item' do
|
||||
let_it_be(:authored_work_item, refind: true) { create(:work_item, project: project, author: developer, assignees: [developer]) }
|
||||
|
||||
let(:work_item) { authored_work_item }
|
||||
|
||||
it 'deletes the work item' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end.to change(WorkItem, :count).by(-1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['project']).to include('id' => work_item.project.to_global_id.to_s)
|
||||
end
|
||||
|
||||
context 'when the work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it 'does not delete the work item' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end.to not_change(WorkItem, :count)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -27,6 +27,10 @@ RSpec.describe Pages::ZipDirectoryService do
|
|||
let(:archive) { result[:archive_path] }
|
||||
let(:entries_count) { result[:entries_count] }
|
||||
|
||||
it 'returns true if ZIP64 is enabled' do
|
||||
expect(::Zip.write_zip64_support).to be true
|
||||
end
|
||||
|
||||
shared_examples 'handles invalid public directory' do
|
||||
it 'returns success' do
|
||||
expect(status).to eq(:success)
|
||||
|
@ -35,7 +39,7 @@ RSpec.describe Pages::ZipDirectoryService do
|
|||
end
|
||||
end
|
||||
|
||||
context "when work direcotry doesn't exist" do
|
||||
context "when work directory doesn't exist" do
|
||||
let(:service_directory) { "/tmp/not/existing/dir" }
|
||||
|
||||
include_examples 'handles invalid public directory'
|
||||
|
|
|
@ -97,9 +97,13 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples_for "deleting a project with merge requests" do
|
||||
context "deleting a project with merge requests" do
|
||||
let!(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:destroy!).and_return(true)
|
||||
end
|
||||
|
||||
it "deletes merge request and related records" do
|
||||
merge_request_diffs = merge_request.merge_request_diffs
|
||||
expect(merge_request_diffs.size).to eq(1)
|
||||
|
@ -119,25 +123,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
destroy_project(project, user, {})
|
||||
end
|
||||
|
||||
context "extract_mr_diff_commit_deletions feature flag" do
|
||||
context "with flag enabled" do
|
||||
before do
|
||||
stub_feature_flags(extract_mr_diff_commit_deletions: true)
|
||||
allow(project).to receive(:destroy!).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like "deleting a project with merge requests"
|
||||
end
|
||||
|
||||
context "with flag disabled" do
|
||||
before do
|
||||
stub_feature_flags(extract_mr_diff_commit_deletions: false)
|
||||
end
|
||||
|
||||
it_behaves_like "deleting a project with merge requests"
|
||||
end
|
||||
end
|
||||
|
||||
context 'with running pipelines' do
|
||||
let!(:pipelines) { create_list(:ci_pipeline, 3, :running, project: project) }
|
||||
let(:destroy_pipeline_service) { double('DestroyPipelineService', execute: nil) }
|
||||
|
|
50
spec/services/work_items/delete_service_spec.rb
Normal file
50
spec/services/work_items/delete_service_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::DeleteService do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: guest) }
|
||||
|
||||
let(:user) { guest }
|
||||
|
||||
before_all do
|
||||
project.add_guest(guest)
|
||||
# note necessary to test note removal as part of work item deletion
|
||||
create(:note, project: project, noteable: work_item)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
|
||||
|
||||
context 'when user can delete the work item' do
|
||||
it { is_expected.to be_success }
|
||||
|
||||
# currently we don't expect destroy to fail. Mocking here for coverage and keeping
|
||||
# the service's return type consistent
|
||||
context 'when there are errors preventing to delete the work item' do
|
||||
before do
|
||||
allow(work_item).to receive(:destroy).and_return(false)
|
||||
work_item.errors.add(:title)
|
||||
end
|
||||
|
||||
it { is_expected.to be_error }
|
||||
|
||||
it 'returns error messages' do
|
||||
expect(result.errors).to contain_exactly('Title is invalid')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot delete the work item' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it { is_expected.to be_error }
|
||||
|
||||
it 'returns error messages' do
|
||||
expect(result.errors).to contain_exactly('User not authorized to delete work item')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -962,10 +962,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.2.0.tgz#95cf58d6ae634d535145159f08f5cff6241d4013"
|
||||
integrity sha512-mCwR3KfNPsxRoojtTjMIZwdd4FFlBh5DlR9AeodP+7+k8rILdWGYxTZbJMPNXoPbZx16R94nG8c5bR7toD4QBw==
|
||||
|
||||
"@gitlab/ui@34.0.0":
|
||||
version "34.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-34.0.0.tgz#0fe9574df2c38aeb63add94e4549ed4e65975ef8"
|
||||
integrity sha512-BFh3x+GCqWAoWhNJhJUunW3eHQLQkBOTBwZFJWSS+1+9ZtetqU3t0/OoqYjJuyTsqdra7A/e6BZsU0j7CnbY+Q==
|
||||
"@gitlab/ui@35.0.0":
|
||||
version "35.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-35.0.0.tgz#9fb89babddc337830f1245044fe7946b266395b4"
|
||||
integrity sha512-iGGsLFgy/BOnmym2VBT+ByiP7mY/DtJPDSoYjd7QtJbOF17A+MyvOwBFGTUXAJxDtWTYSkMZkEuwZVA3VOEwyQ==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
bootstrap-vue "2.20.1"
|
||||
|
|
Loading…
Reference in a new issue