Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
51c18a25f2
commit
c15582526d
46 changed files with 684 additions and 352 deletions
|
@ -1,4 +1,3 @@
|
|||
import $ from 'jquery';
|
||||
import { memoize, throttle } from 'lodash';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
|
@ -34,7 +33,6 @@ class DirtySubmitForm {
|
|||
|
||||
this.form.addEventListener('input', throttledUpdateDirtyInput);
|
||||
this.form.addEventListener('change', throttledUpdateDirtyInput);
|
||||
$(this.form).on('change.select2', throttledUpdateDirtyInput);
|
||||
this.form.addEventListener('submit', (event) => this.formSubmit(event));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export function confirmAction(
|
||||
message,
|
||||
{
|
||||
primaryBtnVariant,
|
||||
primaryBtnText,
|
||||
secondaryBtnVariant,
|
||||
secondaryBtnText,
|
||||
cancelBtnVariant,
|
||||
cancelBtnText,
|
||||
modalHtmlMessage,
|
||||
title,
|
||||
hideCancel,
|
||||
} = {},
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
let confirmed = false;
|
||||
let component;
|
||||
|
||||
const ConfirmAction = Vue.extend({
|
||||
components: {
|
||||
ConfirmModal: () => import('./confirm_modal.vue'),
|
||||
},
|
||||
render(h) {
|
||||
return h(
|
||||
'confirm-modal',
|
||||
{
|
||||
props: {
|
||||
secondaryText: secondaryBtnText,
|
||||
secondaryVariant: secondaryBtnVariant,
|
||||
primaryVariant: primaryBtnVariant,
|
||||
primaryText: primaryBtnText,
|
||||
cancelVariant: cancelBtnVariant,
|
||||
cancelText: cancelBtnText,
|
||||
title,
|
||||
modalHtmlMessage,
|
||||
hideCancel,
|
||||
},
|
||||
on: {
|
||||
confirmed() {
|
||||
confirmed = true;
|
||||
},
|
||||
closed() {
|
||||
component.$destroy();
|
||||
resolve(confirmed);
|
||||
},
|
||||
},
|
||||
},
|
||||
[message],
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
component = new ConfirmAction().$mount();
|
||||
});
|
||||
}
|
|
@ -1,75 +1,21 @@
|
|||
import Vue from 'vue';
|
||||
import { confirmAction } from './confirm_action';
|
||||
|
||||
export function confirmAction(
|
||||
message,
|
||||
{
|
||||
primaryBtnVariant,
|
||||
primaryBtnText,
|
||||
secondaryBtnVariant,
|
||||
secondaryBtnText,
|
||||
cancelBtnVariant,
|
||||
cancelBtnText,
|
||||
modalHtmlMessage,
|
||||
title,
|
||||
hideCancel,
|
||||
} = {},
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
let confirmed = false;
|
||||
|
||||
const component = new Vue({
|
||||
components: {
|
||||
ConfirmModal: () => import('./confirm_modal.vue'),
|
||||
},
|
||||
render(h) {
|
||||
return h(
|
||||
'confirm-modal',
|
||||
{
|
||||
props: {
|
||||
secondaryText: secondaryBtnText,
|
||||
secondaryVariant: secondaryBtnVariant,
|
||||
primaryVariant: primaryBtnVariant,
|
||||
primaryText: primaryBtnText,
|
||||
cancelVariant: cancelBtnVariant,
|
||||
cancelText: cancelBtnText,
|
||||
title,
|
||||
modalHtmlMessage,
|
||||
hideCancel,
|
||||
},
|
||||
on: {
|
||||
confirmed() {
|
||||
confirmed = true;
|
||||
},
|
||||
closed() {
|
||||
component.$destroy();
|
||||
resolve(confirmed);
|
||||
},
|
||||
},
|
||||
},
|
||||
[message],
|
||||
);
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
}
|
||||
|
||||
export function confirmViaGlModal(message, element) {
|
||||
const primaryBtnConfig = {};
|
||||
|
||||
const { confirmBtnVariant } = element.dataset;
|
||||
|
||||
if (confirmBtnVariant) {
|
||||
primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
|
||||
}
|
||||
function confirmViaGlModal(message, element) {
|
||||
const { confirmBtnVariant, title, isHtmlMessage } = element.dataset;
|
||||
|
||||
const screenReaderText =
|
||||
element.querySelector('.gl-sr-only')?.textContent ||
|
||||
element.querySelector('.sr-only')?.textContent ||
|
||||
element.getAttribute('aria-label');
|
||||
|
||||
if (screenReaderText) {
|
||||
primaryBtnConfig.primaryBtnText = screenReaderText;
|
||||
}
|
||||
const config = {
|
||||
...(screenReaderText && { primaryBtnText: screenReaderText }),
|
||||
...(confirmBtnVariant && { primaryBtnVariant: confirmBtnVariant }),
|
||||
...(title && { title }),
|
||||
...(isHtmlMessage && { modalHtmlMessage: message }),
|
||||
};
|
||||
|
||||
return confirmAction(message, primaryBtnConfig);
|
||||
return confirmAction(message, config);
|
||||
}
|
||||
|
||||
export { confirmAction, confirmViaGlModal };
|
||||
|
|
|
@ -207,7 +207,7 @@ body {
|
|||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.logged-out-marketing-header-candidate {
|
||||
.logged-out-marketing-header {
|
||||
--header-height: 72px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -603,6 +603,11 @@ svg {
|
|||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.logged-out-marketing-header {
|
||||
--header-height: 72px;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
|
@ -2054,9 +2059,19 @@ body.gl-dark {
|
|||
.gl-display-none {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.gl-sm-display-none {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.gl-display-flex {
|
||||
display: flex;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.gl-lg-display-flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.gl-sm-display-block {
|
||||
display: block;
|
||||
|
|
|
@ -584,6 +584,11 @@ svg {
|
|||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.logged-out-marketing-header {
|
||||
--header-height: 72px;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
|
@ -1700,9 +1705,19 @@ svg.s16 {
|
|||
.gl-display-none {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.gl-sm-display-none {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.gl-display-flex {
|
||||
display: flex;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.gl-lg-display-flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.gl-sm-display-block {
|
||||
display: block;
|
||||
|
|
|
@ -94,3 +94,5 @@ class Profiles::PasswordsController < Profiles::ApplicationController
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
Profiles::PasswordsController.prepend_mod
|
||||
|
|
|
@ -40,7 +40,6 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
persist_accepted_terms_if_required(new_user)
|
||||
set_role_required(new_user)
|
||||
track_experiment_event(new_user)
|
||||
send_custom_confirmation_instructions(new_user, token)
|
||||
|
||||
if pending_approval?
|
||||
|
@ -239,14 +238,6 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
current_user
|
||||
end
|
||||
|
||||
def track_experiment_event(new_user)
|
||||
# Track signed up event to relate it with click "Sign up" button events from
|
||||
# the experimental logged out header with marketing links. This allows us to
|
||||
# have a funnel of visitors clicking on the header and those visitors
|
||||
# signing up and becoming users
|
||||
experiment(:logged_out_marketing_header, actor: new_user).track(:signed_up) if new_user.persisted?
|
||||
end
|
||||
|
||||
def identity_verification_redirect_path
|
||||
# overridden by EE module
|
||||
end
|
||||
|
|
|
@ -315,7 +315,7 @@ module ApplicationHelper
|
|||
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
|
||||
class_names << 'with-performance-bar' if performance_bar_enabled?
|
||||
class_names << system_message_class
|
||||
class_names << marketing_header_experiment_class
|
||||
class_names << 'logged-out-marketing-header' unless current_user
|
||||
class_names
|
||||
end
|
||||
|
||||
|
@ -456,18 +456,6 @@ module ApplicationHelper
|
|||
def appearance
|
||||
::Appearance.current
|
||||
end
|
||||
|
||||
def marketing_header_experiment_class
|
||||
return if current_user
|
||||
|
||||
experiment(:logged_out_marketing_header, actor: nil) do |e|
|
||||
html_class = 'logged-out-marketing-header-candidate'
|
||||
e.candidate { html_class }
|
||||
e.variant(:trial_focused) { html_class }
|
||||
e.control {}
|
||||
e.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ApplicationHelper.prepend_mod
|
||||
|
|
|
@ -36,6 +36,7 @@ class UserPolicy < BasePolicy
|
|||
rule { (private_profile | blocked_user | unconfirmed_user) & ~(user_is_self | admin) }.prevent :read_user_profile
|
||||
rule { user_is_self | admin }.enable :disable_two_factor
|
||||
rule { (user_is_self | admin) & ~blocked }.enable :create_user_personal_access_token
|
||||
rule { (user_is_self | admin) & ~blocked }.enable :get_user_associations_count
|
||||
end
|
||||
|
||||
UserPolicy.prepend_mod_with('UserPolicy')
|
||||
|
|
|
@ -57,7 +57,7 @@ module Groups
|
|||
end
|
||||
|
||||
def after_create_hook
|
||||
track_experiment_event
|
||||
# overridden in EE
|
||||
end
|
||||
|
||||
def remove_unallowed_params
|
||||
|
@ -109,15 +109,6 @@ module Groups
|
|||
@group.shared_runners_enabled = @group.parent.shared_runners_enabled
|
||||
@group.allow_descendants_override_disabled_shared_runners = @group.parent.allow_descendants_override_disabled_shared_runners
|
||||
end
|
||||
|
||||
def track_experiment_event
|
||||
return unless group.persisted?
|
||||
|
||||
# Track namespace created events to relate them with signed up events for
|
||||
# the same experiment. This will let us associate created namespaces to
|
||||
# users that signed up from the experimental logged out header.
|
||||
experiment(:logged_out_marketing_header, actor: current_user).track(:namespace_created, namespace: group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -19,14 +19,7 @@
|
|||
.gl-display-none.gl-sm-display-block
|
||||
= render "layouts/nav/top_nav"
|
||||
- else
|
||||
- experiment(:logged_out_marketing_header, actor: nil) do |e|
|
||||
- e.candidate do
|
||||
= render 'layouts/header/marketing_links'
|
||||
- e.try(:trial_focused) do
|
||||
= render 'layouts/header/marketing_links'
|
||||
- e.control do
|
||||
.gl-display-none.gl-sm-display-block
|
||||
= render "layouts/nav/top_nav"
|
||||
= render 'layouts/header/marketing_links'
|
||||
|
||||
- if top_nav_show_search
|
||||
.navbar-collapse.gl-transition-medium.collapse.gl-mr-auto.global-search-container.hide-when-top-nav-responsive-open
|
||||
|
@ -110,14 +103,8 @@
|
|||
.dropdown-menu.dropdown-menu-right
|
||||
= render 'layouts/header/help_dropdown'
|
||||
- unless current_user
|
||||
- experiment(:logged_out_marketing_header, actor: nil) do |e|
|
||||
- e.candidate do
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= render "layouts/nav/top_nav"
|
||||
- e.try(:trial_focused) do
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= render "layouts/nav/top_nav"
|
||||
- e.control {}
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= render "layouts/nav/top_nav"
|
||||
- if header_link?(:user_dropdown)
|
||||
%li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_action: "click_dropdown", track_value: "", qa_selector: 'user_menu', testid: 'user-menu' }, class: ('mr-0' if has_impersonation_link) }
|
||||
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
|
||||
|
@ -131,23 +118,11 @@
|
|||
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body', qa_selector: 'stop_impersonation_link' } do
|
||||
= sprite_icon('incognito', size: 18)
|
||||
- if header_link?(:sign_in)
|
||||
- experiment(:logged_out_marketing_header, actor: nil) do |e|
|
||||
- e.candidate do
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up_now' }
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
|
||||
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'
|
||||
- e.try(:trial_focused) do
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Get a free trial'), 'https://about.gitlab.com/free-trial/', class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'get_a_free_trial' }
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Sign up'), new_user_registration_path, data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up' }
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
|
||||
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'
|
||||
- e.control do
|
||||
= render 'layouts/header/sign_in_register_button'
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in'
|
||||
%li.nav-item.gl-display-none.gl-sm-display-block
|
||||
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
|
||||
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'
|
||||
|
||||
%button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: 'gl-border-none!', data: { testid: 'top-nav-responsive-toggle', qa_selector: 'mobile_navbar_button' } }
|
||||
%span.sr-only= _('Toggle navigation')
|
||||
|
|
|
@ -71,7 +71,10 @@
|
|||
- else
|
||||
= link_to _('Cancel'), polymorphic_path([@project, issuable]), class: 'gl-button btn btn-default js-reset-autosave'
|
||||
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
|
||||
= link_to 'Delete', polymorphic_path([@project, issuable], params: { destroy_confirm: true }), data: { confirm: _('%{issuableType} will be removed! Are you sure?') % { issuableType: issuable.human_class_name } }, method: :delete, class: 'btn gl-button btn-danger btn-danger-secondary gl-float-right js-reset-autosave'
|
||||
- confirm_title = _('Delete %{issuableType}?') % { issuableType: issuable.human_class_name }
|
||||
- confirm_body = _('You’re about to permanently delete the %{issuableType} ‘%{strongOpen}%{issuableTitle}%{strongClose}’. To avoid data loss, consider %{strongOpen}closing this %{issuableType}%{strongClose} instead. Once deleted, it cannot be undone or recovered.') % { issuableType: issuable.human_class_name, issuableTitle: issuable.title, strongOpen: '<strong>', strongClose: '</strong>' }
|
||||
- confirm_primary_btn_text = _('Delete %{issuableType}') % { issuableType: issuable.human_class_name }
|
||||
= link_to _('Delete'), polymorphic_path([@project, issuable], params: { destroy_confirm: true }), data: { title: confirm_title, confirm: confirm_body, is_html_message: true, confirm_btn_variant: 'danger'}, method: :delete, class: 'btn gl-button btn-danger btn-danger-secondary gl-float-right js-reset-autosave', "aria-label": confirm_primary_btn_text
|
||||
|
||||
- if issuable.respond_to?(:issue_type)
|
||||
= form.hidden_field :issue_type
|
||||
|
|
|
@ -12,7 +12,11 @@ module LooseForeignKeys
|
|||
idempotent!
|
||||
|
||||
def perform
|
||||
in_lock(self.class.name.underscore, ttl: ModificationTracker::MAX_RUNTIME, retries: 0) do
|
||||
# Add small buffer on MAX_RUNTIME to account for single long running
|
||||
# query or extra worker time after the cleanup.
|
||||
lock_ttl = ModificationTracker::MAX_RUNTIME + 20.seconds
|
||||
|
||||
in_lock(self.class.name.underscore, ttl: lock_ttl, retries: 0) do
|
||||
stats = {}
|
||||
|
||||
connection_name, base_model = current_connection_name_and_base_model
|
||||
|
|
|
@ -22,6 +22,3 @@ module Onboarding
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
|
||||
Namespaces::OnboardingIssueCreatedWorker = Onboarding::IssueCreatedWorker
|
||||
|
|
|
@ -22,6 +22,3 @@ module Onboarding
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
|
||||
Namespaces::OnboardingPipelineCreatedWorker = Onboarding::PipelineCreatedWorker
|
||||
|
|
|
@ -23,6 +23,3 @@ module Onboarding
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
|
||||
Namespaces::OnboardingProgressWorker = Onboarding::ProgressWorker
|
||||
|
|
|
@ -19,6 +19,3 @@ module Onboarding
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
|
||||
Namespaces::OnboardingUserAddedWorker = Onboarding::UserAddedWorker
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: logged_out_marketing_header
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76076
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348525
|
||||
milestone: '14.7'
|
||||
type: experiment
|
||||
group: group::activation
|
||||
default_enabled: false
|
|
@ -1,30 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateRoutingTableForBuildsMetadata < Gitlab::Database::Migration[2.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers
|
||||
def up; end
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = :ci_builds_metadata
|
||||
PARENT_TABLE_NAME = :p_ci_builds_metadata
|
||||
FIRST_PARTITION = 100
|
||||
PARTITION_COLUMN = :partition_id
|
||||
|
||||
def up
|
||||
convert_table_to_first_list_partition(
|
||||
table_name: TABLE_NAME,
|
||||
partitioning_column: PARTITION_COLUMN,
|
||||
parent_table_name: PARENT_TABLE_NAME,
|
||||
initial_partitioning_value: FIRST_PARTITION
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
revert_converting_table_to_first_list_partition(
|
||||
table_name: TABLE_NAME,
|
||||
partitioning_column: PARTITION_COLUMN,
|
||||
parent_table_name: PARENT_TABLE_NAME,
|
||||
initial_partitioning_value: FIRST_PARTITION
|
||||
)
|
||||
end
|
||||
def down; end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateRoutingTableForBuildsMetadataV2 < Gitlab::Database::Migration[2.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = :ci_builds_metadata
|
||||
PARENT_TABLE_NAME = :p_ci_builds_metadata
|
||||
FIRST_PARTITION = 100
|
||||
PARTITION_COLUMN = :partition_id
|
||||
|
||||
def up
|
||||
return if connection.table_exists?(PARENT_TABLE_NAME) && partition_attached?
|
||||
|
||||
convert_table_to_first_list_partition(
|
||||
table_name: TABLE_NAME,
|
||||
partitioning_column: PARTITION_COLUMN,
|
||||
parent_table_name: PARENT_TABLE_NAME,
|
||||
initial_partitioning_value: FIRST_PARTITION,
|
||||
lock_tables: [:ci_builds, :ci_builds_metadata]
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
revert_converting_table_to_first_list_partition(
|
||||
table_name: TABLE_NAME,
|
||||
partitioning_column: PARTITION_COLUMN,
|
||||
parent_table_name: PARENT_TABLE_NAME,
|
||||
initial_partitioning_value: FIRST_PARTITION
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def partition_attached?
|
||||
connection.select_value(<<~SQL)
|
||||
SELECT true FROM postgres_partitions WHERE name = '#{TABLE_NAME}';
|
||||
SQL
|
||||
end
|
||||
end
|
1
db/schema_migrations/20221021145820
Normal file
1
db/schema_migrations/20221021145820
Normal file
|
@ -0,0 +1 @@
|
|||
e9fd4d60833624e20fcf9b01b883dca15e6c135aa99f1afd1c7a365eebac17fb
|
|
@ -968,6 +968,33 @@ Example response:
|
|||
|
||||
Please refer to the [List of user projects](projects.md#list-user-projects).
|
||||
|
||||
## List associations count for user
|
||||
|
||||
Get a list of a specified user's count of projects, groups, issues and merge requests.
|
||||
|
||||
Administrators can query any user, but non-administrators can only query themselves.
|
||||
|
||||
```plaintext
|
||||
GET /users/:id/associations_count
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|------------------|
|
||||
| `id` | integer | yes | ID of a user |
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"groups_count": 2,
|
||||
"projects_count": 3,
|
||||
"issues_count": 8,
|
||||
"merge_requests_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
## List SSH keys
|
||||
|
||||
Get a list of currently authenticated user's SSH keys.
|
||||
|
|
|
@ -1733,15 +1733,18 @@ there are no problems we need to inform the user of.
|
|||
|
||||
#### Failure (relevant to the user)
|
||||
|
||||
An error that affects the **user** occurred. We refer to these as _mutation errors_. In
|
||||
this case there is typically no `thing` to return:
|
||||
An error that affects the **user** occurred. We refer to these as _mutation errors_.
|
||||
|
||||
In a _create_ mutation there is typically no `thing` to return.
|
||||
|
||||
In an _update_ mutation we return the current true state of `thing`. Developers may need to call `#reset` on the `thing` instance to ensure this happens.
|
||||
|
||||
```javascript
|
||||
{
|
||||
data: {
|
||||
doTheThing: {
|
||||
errors: ["you cannot touch the thing"],
|
||||
thing: null
|
||||
thing: { .. }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
lib/api/entities/user_associations_count.rb
Normal file
23
lib/api/entities/user_associations_count.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class UserAssociationsCount < Grape::Entity
|
||||
expose :groups_count do |user|
|
||||
user.groups.size
|
||||
end
|
||||
|
||||
expose :projects_count do |user|
|
||||
user.projects.size
|
||||
end
|
||||
|
||||
expose :issues_count do |user|
|
||||
user.issues.size
|
||||
end
|
||||
|
||||
expose :merge_requests_count do |user|
|
||||
user.merge_requests.size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,6 +18,13 @@ module API
|
|||
error_messages[:bio] = error_messages.delete(:"user_detail.bio") if error_messages.has_key?(:"user_detail.bio")
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_user_by_id(params)
|
||||
id = params[:user_id] || params[:id]
|
||||
User.find_by(id: id) || not_found!('User')
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,9 +8,18 @@ module API
|
|||
|
||||
allow_access_with_scope :read_user, if: -> (request) { request.get? || request.head? }
|
||||
|
||||
feature_category :users, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
|
||||
feature_category :users,
|
||||
%w[
|
||||
/users/:id/custom_attributes
|
||||
/users/:id/custom_attributes/:key
|
||||
/users/:id/associations_count
|
||||
]
|
||||
|
||||
urgency :medium, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
|
||||
urgency :medium,
|
||||
%w[
|
||||
/users/:id/custom_attributes
|
||||
/users/:id/custom_attributes/:key
|
||||
]
|
||||
|
||||
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
|
||||
include CustomAttributesEndpoints
|
||||
|
@ -22,13 +31,6 @@ module API
|
|||
helpers Helpers::UsersHelpers
|
||||
|
||||
helpers do
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_user_by_id(params)
|
||||
id = params[:user_id] || params[:id]
|
||||
User.find_by(id: id) || not_found!('User')
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def reorder_users(users)
|
||||
if params[:order_by] && params[:sort]
|
||||
|
@ -75,6 +77,31 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
resources ':id/associations_count' do
|
||||
helpers do
|
||||
def present_entity(result)
|
||||
present result,
|
||||
with: ::API::Entities::UserAssociationsCount
|
||||
end
|
||||
end
|
||||
|
||||
desc "Returns a list of a specified user's count of projects, groups, issues and merge requests."
|
||||
params do
|
||||
requires :id,
|
||||
type: Integer,
|
||||
desc: 'ID of the user to query.'
|
||||
end
|
||||
get do
|
||||
authenticate!
|
||||
|
||||
user = find_user_by_id(params)
|
||||
forbidden! unless can?(current_user, :get_user_associations_count, user)
|
||||
not_found!('User') unless user
|
||||
|
||||
present_entity(user)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Get the list of users' do
|
||||
success Entities::UserBasic
|
||||
end
|
||||
|
|
|
@ -10,13 +10,17 @@ module Gitlab
|
|||
|
||||
attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value
|
||||
|
||||
def initialize(migration_context:, table_name:, parent_table_name:, partitioning_column:, zero_partition_value:)
|
||||
def initialize(
|
||||
migration_context:, table_name:, parent_table_name:, partitioning_column:,
|
||||
zero_partition_value:, lock_tables: [])
|
||||
|
||||
@migration_context = migration_context
|
||||
@connection = migration_context.connection
|
||||
@table_name = table_name
|
||||
@parent_table_name = parent_table_name
|
||||
@partitioning_column = partitioning_column
|
||||
@zero_partition_value = zero_partition_value
|
||||
@lock_tables = Array.wrap(lock_tables)
|
||||
end
|
||||
|
||||
def prepare_for_partitioning
|
||||
|
@ -35,7 +39,12 @@ module Gitlab
|
|||
create_parent_table
|
||||
attach_foreign_keys_to_parent
|
||||
|
||||
migration_context.with_lock_retries(raise_on_exhaustion: true) do
|
||||
lock_args = {
|
||||
raise_on_exhaustion: true,
|
||||
timing_configuration: lock_timing_configuration
|
||||
}
|
||||
|
||||
migration_context.with_lock_retries(**lock_args) do
|
||||
migration_context.execute(sql_to_convert_table)
|
||||
end
|
||||
end
|
||||
|
@ -74,6 +83,7 @@ module Gitlab
|
|||
# but they acquire the same locks so it's much faster to incude them
|
||||
# here.
|
||||
[
|
||||
lock_tables_statement,
|
||||
attach_table_to_parent_statement,
|
||||
alter_sequence_statements(old_table: table_name, new_table: parent_table_name),
|
||||
remove_constraint_statement
|
||||
|
@ -162,6 +172,16 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def lock_tables_statement
|
||||
return if @lock_tables.empty?
|
||||
|
||||
table_names = @lock_tables.map { |name| quote_table_name(name) }.join(', ')
|
||||
|
||||
<<~SQL
|
||||
LOCK #{table_names} IN ACCESS EXCLUSIVE MODE
|
||||
SQL
|
||||
end
|
||||
|
||||
def attach_table_to_parent_statement
|
||||
<<~SQL
|
||||
ALTER TABLE #{quote_table_name(parent_table_name)}
|
||||
|
@ -235,6 +255,13 @@ module Gitlab
|
|||
ALTER TABLE #{connection.quote_table_name(table_name)} OWNER TO CURRENT_USER
|
||||
SQL
|
||||
end
|
||||
|
||||
def lock_timing_configuration
|
||||
iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION
|
||||
aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] }
|
||||
|
||||
iterations + aggressive_iterations
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -275,7 +275,7 @@ module Gitlab
|
|||
).revert_preparation_for_partitioning
|
||||
end
|
||||
|
||||
def convert_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:)
|
||||
def convert_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:, lock_tables: [])
|
||||
validate_not_in_transaction!(:convert_table_to_first_list_partition)
|
||||
|
||||
Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
|
||||
|
@ -283,7 +283,8 @@ module Gitlab
|
|||
table_name: table_name,
|
||||
parent_table_name: parent_table_name,
|
||||
partitioning_column: partitioning_column,
|
||||
zero_partition_value: initial_partitioning_value
|
||||
zero_partition_value: initial_partitioning_value,
|
||||
lock_tables: lock_tables
|
||||
).partition
|
||||
end
|
||||
|
||||
|
|
|
@ -59,7 +59,11 @@ module Gitlab
|
|||
routing_rules_queues = mappings.values.uniq
|
||||
logger&.info("List of queues based on routing rules: #{routing_rules_queues}")
|
||||
Sidekiq.redis do |conn|
|
||||
conn.scan_each(match: "queue:*", type: 'list') do |key|
|
||||
# Redis 6 supports conn.scan_each(match: "queue:*", type: 'list')
|
||||
conn.scan_each(match: "queue:*") do |key|
|
||||
# Redis 5 compatibility
|
||||
next unless conn.type(key) == 'list'
|
||||
|
||||
queue_from = key.split(':', 2).last
|
||||
next if routing_rules_queues.include?(queue_from)
|
||||
|
||||
|
|
|
@ -10692,10 +10692,10 @@ msgstr ""
|
|||
msgid "Contribution Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{created_count} created, %{closed_count} closed."
|
||||
msgid "ContributionAnalytics|%{created} created, %{closed} closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{created_count} created, %{merged_count} merged, %{closed_count} closed."
|
||||
msgid "ContributionAnalytics|%{created} created, %{merged} merged, %{closed} closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{pushes}, more than %{commits} by %{contributors}."
|
||||
|
@ -12742,6 +12742,9 @@ msgstr ""
|
|||
msgid "Delete %{issuableType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete %{issuableType}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete %{name}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17999,9 +18002,6 @@ msgstr ""
|
|||
msgid "Get a free instance review"
|
||||
msgstr ""
|
||||
|
||||
msgid "Get a free trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Get a support subscription"
|
||||
msgstr ""
|
||||
|
||||
|
@ -47143,6 +47143,9 @@ msgstr ""
|
|||
msgid "Your username is %{username}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You’re about to permanently delete the %{issuableType} ‘%{strongOpen}%{issuableTitle}%{strongClose}’. To avoid data loss, consider %{strongOpen}closing this %{issuableType}%{strongClose} instead. Once deleted, it cannot be undone or recovered."
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|Failed to load ZenTao issue. View the issue in ZenTao, or reload the page."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -478,28 +478,6 @@ RSpec.describe RegistrationsController do
|
|||
subject
|
||||
end
|
||||
|
||||
describe 'logged_out_marketing_header experiment', :experiment do
|
||||
before do
|
||||
stub_experiments(logged_out_marketing_header: :candidate)
|
||||
end
|
||||
|
||||
it 'tracks signed_up event' do
|
||||
expect(experiment(:logged_out_marketing_header)).to track(:signed_up).on_next_instance
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when registration fails' do
|
||||
let_it_be(:user_params) { { user: base_user_params.merge({ username: '' }) } }
|
||||
|
||||
it 'does not track signed_up event' do
|
||||
expect(experiment(:logged_out_marketing_header)).not_to track(:signed_up)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the password is weak' do
|
||||
render_views
|
||||
let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
|
||||
|
|
|
@ -6,50 +6,14 @@ RSpec.describe 'User sees experimental lmarketing header' do
|
|||
let_it_be(:project) { create(:project, :public) }
|
||||
|
||||
context 'when not logged in' do
|
||||
context 'when experiment candidate' do
|
||||
it 'shows marketing header links', :aggregate_failures do
|
||||
stub_experiments(logged_out_marketing_header: :candidate)
|
||||
it 'shows marketing header links', :aggregate_failures do
|
||||
visit project_path(project)
|
||||
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_text "About GitLab"
|
||||
expect(page).to have_text "Pricing"
|
||||
expect(page).to have_text "Talk to an expert"
|
||||
expect(page).to have_text "Sign up now"
|
||||
expect(page).to have_text "Login"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when experiment candidate (trial focused variant)' do
|
||||
it 'shows marketing header links', :aggregate_failures do
|
||||
stub_experiments(logged_out_marketing_header: :trial_focused)
|
||||
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_text "About GitLab"
|
||||
expect(page).to have_text "Pricing"
|
||||
expect(page).to have_text "Talk to an expert"
|
||||
expect(page).to have_text "Get a free trial"
|
||||
expect(page).to have_text "Sign up"
|
||||
expect(page).to have_text "Login"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when experiment control' do
|
||||
it 'does not show marketing header links', :aggregate_failures do
|
||||
stub_experiments(logged_out_marketing_header: :control)
|
||||
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).not_to have_text "About GitLab"
|
||||
expect(page).not_to have_text "Pricing"
|
||||
expect(page).not_to have_text "Talk to an expert"
|
||||
expect(page).not_to have_text "Sign up now"
|
||||
expect(page).not_to have_text "Login"
|
||||
expect(page).not_to have_text "Get a free trial"
|
||||
expect(page).not_to have_text "Sign up"
|
||||
expect(page).to have_text "Sign in / Register"
|
||||
end
|
||||
expect(page).to have_text "About GitLab"
|
||||
expect(page).to have_text "Pricing"
|
||||
expect(page).to have_text "Talk to an expert"
|
||||
expect(page).to have_text "Sign up now"
|
||||
expect(page).to have_text "Login"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -57,8 +21,6 @@ RSpec.describe 'User sees experimental lmarketing header' do
|
|||
it 'does not show marketing header links', :aggregate_failures do
|
||||
sign_in(create(:user))
|
||||
|
||||
stub_experiments(logged_out_marketing_header: :candidate)
|
||||
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).not_to have_text "About GitLab"
|
||||
|
|
|
@ -10,11 +10,7 @@ import actionMutation from '~/environments/graphql/mutations/action.mutation.gra
|
|||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||
return {
|
||||
confirmAction: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
|
||||
const scheduledJobAction = {
|
||||
name: 'scheduled action',
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
|
||||
import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
|
||||
|
||||
const originalMount = Vue.prototype.$mount;
|
||||
|
||||
describe('confirmAction', () => {
|
||||
let modalWrapper;
|
||||
let confirActionPromise;
|
||||
let modal;
|
||||
|
||||
const findConfirmModal = () => modalWrapper.findComponent(ConfirmModal);
|
||||
const renderRootComponent = async (message, opts) => {
|
||||
confirActionPromise = confirmAction(message, opts);
|
||||
// We have to wait for two ticks here.
|
||||
// The first one is to wait for rendering of the root component
|
||||
// The second one to wait for rendering of the dynamically
|
||||
// loaded confirm-modal component
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
modal = findConfirmModal();
|
||||
};
|
||||
const mockMount = (vm, el) => {
|
||||
originalMount.call(vm, el);
|
||||
modalWrapper = createWrapper(vm);
|
||||
return vm;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture('<div id="component"></div>');
|
||||
const el = document.getElementById('component');
|
||||
// We mock the implementation only once to make sure that we mock
|
||||
// it only for the root component in confirm_action.
|
||||
// Mounting other components (like confirm-modal) should not be affected with
|
||||
// this mock
|
||||
jest.spyOn(Vue.prototype, '$mount').mockImplementationOnce(function mock() {
|
||||
return mockMount(this, el);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
Vue.prototype.$mount.mockRestore();
|
||||
modalWrapper?.destroy();
|
||||
modalWrapper = null;
|
||||
modal?.destroy();
|
||||
modal = null;
|
||||
});
|
||||
|
||||
it('creats a ConfirmModal with message as slot', async () => {
|
||||
const message = 'Bonjour le monde!';
|
||||
await renderRootComponent(message);
|
||||
|
||||
expect(modal.vm.$slots.default[0].text).toBe(message);
|
||||
});
|
||||
|
||||
it('creats a ConfirmModal with props', async () => {
|
||||
const options = {
|
||||
primaryBtnText: 'primaryBtnText',
|
||||
primaryBtnVariant: 'info',
|
||||
secondaryBtnText: 'secondaryBtnText',
|
||||
secondaryBtnVariant: 'success',
|
||||
cancelBtnText: 'cancelBtnText',
|
||||
cancelBtnVariant: 'danger',
|
||||
modalHtmlMessage: '<strong>Hello</strong>',
|
||||
title: 'title',
|
||||
hideCancel: true,
|
||||
};
|
||||
await renderRootComponent('', options);
|
||||
expect(modal.props()).toEqual(
|
||||
expect.objectContaining({
|
||||
primaryText: options.primaryBtnText,
|
||||
primaryVariant: options.primaryBtnVariant,
|
||||
secondaryText: options.secondaryBtnText,
|
||||
secondaryVariant: options.secondaryBtnVariant,
|
||||
cancelText: options.cancelBtnText,
|
||||
cancelVariant: options.cancelBtnVariant,
|
||||
modalHtmlMessage: options.modalHtmlMessage,
|
||||
title: options.title,
|
||||
hideCancel: options.hideCancel,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves promise when modal emit `closed`', async () => {
|
||||
await renderRootComponent('');
|
||||
|
||||
modal.vm.$emit('closed');
|
||||
|
||||
await expect(confirActionPromise).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('confirms when modal emit `confirmed` before `closed`', async () => {
|
||||
await renderRootComponent('');
|
||||
|
||||
modal.vm.$emit('confirmed');
|
||||
modal.vm.$emit('closed');
|
||||
|
||||
await expect(confirActionPromise).resolves.toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { confirmViaGlModal } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
|
||||
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action');
|
||||
|
||||
describe('confirmViaGlModal', () => {
|
||||
let el;
|
||||
|
||||
afterEach(() => {
|
||||
el = undefined;
|
||||
resetHTMLFixture();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const createElement = (html) => {
|
||||
setHTMLFixture(html);
|
||||
return document.body.firstChild;
|
||||
};
|
||||
|
||||
it('returns confirmAction result', async () => {
|
||||
confirmAction.mockReturnValue(Promise.resolve(true));
|
||||
el = createElement(`<div/>`);
|
||||
|
||||
await expect(confirmViaGlModal('', el)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('calls confirmAction with message', () => {
|
||||
el = createElement(`<div/>`);
|
||||
|
||||
confirmViaGlModal('message', el);
|
||||
|
||||
expect(confirmAction).toHaveBeenCalledWith('message', {});
|
||||
});
|
||||
|
||||
it.each(['gl-sr-only', 'sr-only'])(
|
||||
`uses slot.%s contentText as primaryBtnText`,
|
||||
(srOnlyClass) => {
|
||||
el = createElement(
|
||||
`<a href="#"><span class="${srOnlyClass}">Delete merge request</span></a>`,
|
||||
);
|
||||
|
||||
confirmViaGlModal('', el);
|
||||
|
||||
expect(confirmAction).toHaveBeenCalledWith('', {
|
||||
primaryBtnText: 'Delete merge request',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('uses `aria-label` value as `primaryBtnText`', () => {
|
||||
el = createElement(`<a aria-label="Delete merge request" href="#"></a>`);
|
||||
|
||||
confirmViaGlModal('', el);
|
||||
|
||||
expect(confirmAction).toHaveBeenCalledWith('', {
|
||||
primaryBtnText: 'Delete merge request',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['title', 'title', 'Delete?'],
|
||||
['confirm-btn-variant', `primaryBtnVariant`, 'danger'],
|
||||
])('uses data-%s value as confirmAction config', (dataKey, configKey, value) => {
|
||||
el = createElement(`<a data-${dataKey}="${value}" href="#"></a>`);
|
||||
|
||||
confirmViaGlModal('message', el);
|
||||
|
||||
expect(confirmAction).toHaveBeenCalledWith('message', { [configKey]: value });
|
||||
});
|
||||
|
||||
it('uses message as modalHtmlMessage value when data-is-html-message is true', () => {
|
||||
el = createElement(`<a data-is-html-message="true" href="#"></a>`);
|
||||
const message = 'Hola mundo!';
|
||||
|
||||
confirmViaGlModal(message, el);
|
||||
|
||||
expect(confirmAction).toHaveBeenCalledWith(message, { modalHtmlMessage: message });
|
||||
});
|
||||
});
|
|
@ -13,11 +13,7 @@ import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
|
|||
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||
return {
|
||||
confirmAction: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
|
||||
describe('Pipelines Actions dropdown', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -23,11 +23,7 @@ import {
|
|||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||
return {
|
||||
confirmAction: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
|
||||
describe('DeploymentAction component', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -18,11 +18,7 @@ import {
|
|||
workItemQueryResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||
return {
|
||||
confirmAction: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
jest.mock('~/lib/utils/autosave');
|
||||
|
||||
const workItemId = workItemQueryResponse.data.workItem.id;
|
||||
|
|
|
@ -506,42 +506,24 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
|
||||
describe '#page_class' do
|
||||
context 'when logged_out_marketing_header experiment is enabled' do
|
||||
let_it_be(:expected_class) { 'logged-out-marketing-header-candidate' }
|
||||
let_it_be(:expected_class) { 'logged-out-marketing-header' }
|
||||
|
||||
let(:current_user) { nil }
|
||||
let(:variant) { :candidate }
|
||||
let(:current_user) { nil }
|
||||
|
||||
subject do
|
||||
helper.page_class.flatten
|
||||
end
|
||||
subject do
|
||||
helper.page_class.flatten
|
||||
end
|
||||
|
||||
before do
|
||||
stub_experiments(logged_out_marketing_header: variant)
|
||||
allow(helper).to receive(:current_user) { current_user }
|
||||
end
|
||||
before do
|
||||
allow(helper).to receive(:current_user) { current_user }
|
||||
end
|
||||
|
||||
context 'when candidate' do
|
||||
it { is_expected.to include(expected_class) }
|
||||
end
|
||||
it { is_expected.to include(expected_class) }
|
||||
|
||||
context 'when candidate (:trial_focused variant)' do
|
||||
let(:variant) { :trial_focused }
|
||||
context 'when a user is logged in' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it { is_expected.to include(expected_class) }
|
||||
end
|
||||
|
||||
context 'when control' do
|
||||
let(:variant) { :control }
|
||||
|
||||
it { is_expected.not_to include(expected_class) }
|
||||
end
|
||||
|
||||
context 'when a user is logged in' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it { is_expected.not_to include(expected_class) }
|
||||
end
|
||||
it { is_expected.not_to include(expected_class) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
|
|||
let(:referenced_table_name) { '_test_referenced_table' }
|
||||
let(:other_referenced_table_name) { '_test_other_referenced_table' }
|
||||
let(:parent_table_name) { "#{table_name}_parent" }
|
||||
let(:lock_tables) { [] }
|
||||
|
||||
let(:model) { define_batchable_model(table_name, connection: connection) }
|
||||
|
||||
|
@ -27,7 +28,8 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
|
|||
table_name: table_name,
|
||||
partitioning_column: partitioning_column,
|
||||
parent_table_name: parent_table_name,
|
||||
zero_partition_value: partitioning_default
|
||||
zero_partition_value: partitioning_default,
|
||||
lock_tables: lock_tables
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -168,6 +170,16 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
|
|||
end
|
||||
end
|
||||
|
||||
context 'with locking tables' do
|
||||
let(:lock_tables) { [table_name] }
|
||||
|
||||
it 'locks the table' do
|
||||
recorder = ActiveRecord::QueryRecorder.new { partition }
|
||||
|
||||
expect(recorder.log).to include(/LOCK "_test_table_to_partition" IN ACCESS EXCLUSIVE MODE/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an error occurs during the conversion' do
|
||||
def fail_first_time
|
||||
# We can't directly use a boolean here, as we need something that will be passed by-reference to the proc
|
||||
|
|
|
@ -43,6 +43,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
|
|||
|
||||
context 'list partitioning conversion helpers' do
|
||||
shared_examples_for 'delegates to ConvertTableToFirstListPartition' do
|
||||
let(:extra_options) { {} }
|
||||
it 'throws an error if in a transaction' do
|
||||
allow(migration).to receive(:transaction_open?).and_return(true)
|
||||
expect { migrate }.to raise_error(/cannot be run inside a transaction/)
|
||||
|
@ -54,7 +55,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
|
|||
table_name: source_table,
|
||||
parent_table_name: partitioned_table,
|
||||
partitioning_column: partition_column,
|
||||
zero_partition_value: min_date) do |converter|
|
||||
zero_partition_value: min_date,
|
||||
**extra_options) do |converter|
|
||||
expect(converter).to receive(expected_method)
|
||||
end
|
||||
|
||||
|
@ -64,12 +66,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
|
|||
|
||||
describe '#convert_table_to_first_list_partition' do
|
||||
it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
|
||||
let(:lock_tables) { [source_table] }
|
||||
let(:extra_options) { { lock_tables: lock_tables } }
|
||||
let(:expected_method) { :partition }
|
||||
let(:migrate) do
|
||||
migration.convert_table_to_first_list_partition(table_name: source_table,
|
||||
partitioning_column: partition_column,
|
||||
parent_table_name: partitioned_table,
|
||||
initial_partitioning_value: min_date)
|
||||
initial_partitioning_value: min_date,
|
||||
lock_tables: lock_tables)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe CreateRoutingTableForBuildsMetadataV2, :migration do
|
||||
let_it_be(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
context 'when the table is already partitioned' do
|
||||
before do
|
||||
# `convert_table_to_first_list_partition` checks if it's being executed
|
||||
# inside a transaction, but we're using transactional fixtures here so we
|
||||
# need to tell it that it's not inside a transaction.
|
||||
# We toggle the behavior depending on how many transactions we have open
|
||||
# instead of just returning `false` because the migration could have the
|
||||
# DDL transaction enabled.
|
||||
#
|
||||
open_transactions = ActiveRecord::Base.connection.open_transactions
|
||||
allow(migration).to receive(:transaction_open?) do
|
||||
ActiveRecord::Base.connection.open_transactions > open_transactions
|
||||
end
|
||||
|
||||
migration.convert_table_to_first_list_partition(
|
||||
table_name: :ci_builds_metadata,
|
||||
partitioning_column: :partition_id,
|
||||
parent_table_name: :p_ci_builds_metadata,
|
||||
initial_partitioning_value: 100)
|
||||
end
|
||||
|
||||
it 'skips the migration' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -84,6 +84,50 @@ RSpec.describe UserPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
describe "reading a user's associations count" do
|
||||
context 'when current_user is not an admin' do
|
||||
context 'fetching their own data' do
|
||||
subject { described_class.new(current_user, current_user) }
|
||||
|
||||
context 'when current_user is not blocked' do
|
||||
it { is_expected.to be_allowed(:get_user_associations_count ) }
|
||||
end
|
||||
|
||||
context 'when current_user is blocked' do
|
||||
let(:current_user) { create(:user, :blocked) }
|
||||
|
||||
it { is_expected.not_to be_allowed(:get_user_associations_count) }
|
||||
end
|
||||
end
|
||||
|
||||
context "fetching a different user's data" do
|
||||
it { is_expected.not_to be_allowed(:get_user_associations_count) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user is an admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'fetching their own data', :enable_admin_mode do
|
||||
subject { described_class.new(current_user, current_user) }
|
||||
|
||||
context 'when current_user is not blocked' do
|
||||
it { is_expected.to be_allowed(:get_user_associations_count ) }
|
||||
end
|
||||
|
||||
context 'when current_user is blocked' do
|
||||
let(:current_user) { create(:admin, :blocked) }
|
||||
|
||||
it { is_expected.not_to be_allowed(:get_user_associations_count) }
|
||||
end
|
||||
end
|
||||
|
||||
context "fetching a different user's data", :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:get_user_associations_count) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'changing a user' do |ability|
|
||||
context "when a regular user tries to destroy another regular user" do
|
||||
it { is_expected.not_to be_allowed(ability) }
|
||||
|
|
|
@ -4395,6 +4395,74 @@ RSpec.describe API::Users do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /users/:id/associations_count' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :public, group: group) }
|
||||
let(:associations) do
|
||||
{
|
||||
groups_count: 1,
|
||||
projects_count: 1,
|
||||
issues_count: 2,
|
||||
merge_requests_count: 1
|
||||
}.as_json
|
||||
end
|
||||
|
||||
before :all do
|
||||
group.add_member(user, Gitlab::Access::OWNER)
|
||||
project.add_member(user, Gitlab::Access::OWNER)
|
||||
create(:merge_request, source_project: project, source_branch: "my-personal-branch-1", author: user)
|
||||
create_list(:issue, 2, project: project, author: user)
|
||||
end
|
||||
|
||||
context 'as an unauthorized user' do
|
||||
it 'returns 401 unauthorized' do
|
||||
get api("/users/#{user.id}/associations_count", nil)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a non-admin user' do
|
||||
context 'with a different user id' do
|
||||
it 'returns 403 Forbidden' do
|
||||
get api("/users/#{omniauth_user.id}/associations_count", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the current user id' do
|
||||
it 'returns valid JSON response' do
|
||||
get api("/users/#{user.id}/associations_count", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_a Hash
|
||||
expect(json_response).to match(associations)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an admin user' do
|
||||
context 'with invalid user id' do
|
||||
it 'returns 404 User Not Found' do
|
||||
get api("/users/#{non_existing_record_id}/associations_count", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid user id' do
|
||||
it 'returns valid JSON response' do
|
||||
get api("/users/#{user.id}/associations_count", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_a Hash
|
||||
expect(json_response).to match(associations)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'custom attributes endpoints', 'users' do
|
||||
let(:attributable) { user }
|
||||
let(:other_attributable) { admin }
|
||||
|
|
|
@ -271,33 +271,4 @@ RSpec.describe Groups::CreateService, '#execute' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'logged_out_marketing_header experiment', :experiment do
|
||||
let(:service) { described_class.new(user, group_params) }
|
||||
|
||||
subject { service.execute }
|
||||
|
||||
before do
|
||||
stub_experiments(logged_out_marketing_header: :candidate)
|
||||
end
|
||||
|
||||
it 'tracks signed_up event' do
|
||||
expect(experiment(:logged_out_marketing_header)).to track(
|
||||
:namespace_created,
|
||||
namespace: an_instance_of(Group)
|
||||
).on_next_instance.with_context(actor: user)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when group has not been persisted' do
|
||||
let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
|
||||
|
||||
it 'does not track signed_up event' do
|
||||
expect(experiment(:logged_out_marketing_header)).not_to track(:namespace_created)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue