Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-23 21:10:28 +00:00
parent f6b95a66bc
commit 4e7abe540d
63 changed files with 1044 additions and 694 deletions

View File

@ -229,14 +229,6 @@
- *node-modules-cache # We don't push this cache as it's already rebuilt by `update-assets-compile-*-cache`
- *storybook-node-modules-cache-push
.use-pg11:
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:5.0-alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
PG_VERSION: "11"
.use-pg12:
services:
@ -256,21 +248,6 @@
POSTGRES_HOST_AUTH_METHOD: trust
PG_VERSION: "13"
.use-pg11-es7-ee:
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:5.0-alpine
- name: elasticsearch:7.17.6
command: ["elasticsearch", "-E", "discovery.type=single-node", "-E", "xpack.security.enabled=false"]
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.0
alias: zoekt-ci-image
variables:
POSTGRES_HOST_AUTH_METHOD: trust
PG_VERSION: "11"
ZOEKT_INDEX_BASE_URL: http://zoekt-ci-image:6060
ZOEKT_SEARCH_BASE_URL: http://zoekt-ci-image:6070
.use-pg12-es7-ee:
services:
- name: postgres:12

View File

@ -191,16 +191,6 @@ rspec system pg12 praefect:
- .praefect-with-db
- .rails:rules:praefect-with-db
# Dedicated job to test DB library code against PG11.
# Note that these are already tested against PG12 in the `rspec unit pg12` / `rspec-ee unit pg12` jobs.
rspec db-library-code pg11:
extends:
- .rspec-base-pg11
- .rails:rules:ee-and-foss-db-library-code
script:
- !reference [.base-script, script]
- rspec_db_library_code
rspec fast_spec_helper:
extends:
- .rspec-base-pg12
@ -616,39 +606,6 @@ rspec-ee system pg12 single-db:
##########################################
# EE/FOSS: default branch nightly scheduled jobs #
# PG11
rspec migration pg11:
extends:
- .rspec-base-pg11
- .rspec-base-migration
- .rails:rules:rspec-on-pg11
- .rspec-migration-parallel
rspec background_migration pg11:
extends:
- .rspec-base-pg11
- .rspec-base-migration
- .rails:rules:rspec-on-pg11
- .rspec-background-migration-parallel
rspec unit pg11:
extends:
- .rspec-base-pg11
- .rails:rules:rspec-on-pg11
- .rspec-unit-parallel
rspec integration pg11:
extends:
- .rspec-base-pg11
- .rails:rules:rspec-on-pg11
- .rspec-integration-parallel
rspec system pg11:
extends:
- .rspec-base-pg11
- .rails:rules:rspec-on-pg11
- .rspec-system-parallel
# PG13
rspec migration pg13:
extends:
@ -687,39 +644,6 @@ rspec system pg13:
#####################################
# EE: default branch nightly scheduled jobs #
# PG11
rspec-ee migration pg11:
extends:
- .rspec-ee-base-pg11
- .rspec-base-migration
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-migration-parallel
rspec-ee background_migration pg11:
extends:
- .rspec-ee-base-pg11
- .rspec-base-migration
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-background-migration-parallel
rspec-ee unit pg11:
extends:
- .rspec-ee-base-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-unit-parallel
rspec-ee integration pg11:
extends:
- .rspec-ee-base-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-integration-parallel
rspec-ee system pg11:
extends:
- .rspec-ee-base-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-system-parallel
# PG12
rspec-ee unit pg12 opensearch1:
extends:

View File

@ -92,11 +92,6 @@ include:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag ~zoekt"
.rspec-base-pg11:
extends:
- .rspec-base
- .use-pg11
.rspec-base-pg12:
extends:
- .rspec-base
@ -119,11 +114,6 @@ include:
- .rspec-base
- .use-pg13
.rspec-ee-base-pg11:
extends:
- .rspec-base
- .use-pg11-es7-ee
.rspec-ee-base-pg12:
extends:
- .rspec-base

View File

@ -85,9 +85,6 @@
.if-merge-request-labels-run-review-app: &if-merge-request-labels-run-review-app
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-review-app/'
.if-merge-request-labels-run-on-pg11: &if-merge-request-labels-run-on-pg11
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-on-pg11/'
.if-merge-request-labels-skip-undercoverage: &if-merge-request-labels-skip-undercoverage
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:skip-undercoverage/'
@ -1605,7 +1602,6 @@
- <<: *if-default-refs
changes: *db-library-patterns
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request-labels-run-on-pg11
.rails:rules:ee-mr-and-default-branch-only:
rules:
@ -1695,11 +1691,6 @@
- <<: *if-merge-request
changes: *backend-patterns
.rails:rules:rspec-on-pg11:
rules:
- <<: *if-merge-request-labels-run-on-pg11
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules]
.rails:rules:default-branch-schedule-nightly--code-backstage-default-rules:
rules:
- <<: *if-default-branch-schedule-nightly

View File

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 15.9.1 (2023-02-23)
### Fixed (2 changes)
- [Fix Broadcast messages not showing in admin console](gitlab-org/gitlab@f50dfdfe43231b4bb52378eaaa515ee76c918d03) ([merge request](gitlab-org/gitlab!112831))
- [Fix dependency check in license approval policies](gitlab-org/gitlab@ff5a77036fdb74c4b410fbb954428dbf8736ffd8) ([merge request](gitlab-org/gitlab!112831)) **GitLab Enterprise Edition**
## 15.9.0 (2023-02-21)
### Added (223 changes)

View File

@ -82,7 +82,7 @@ export default {
eventHub.$on('skip-beforeunload', this.handleSkipBeforeUnload);
if (this.themeName)
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
document.querySelector('.navbar-gitlab')?.classList.add(`theme-${this.themeName}`);
},
destroyed() {
eventHub.$off('skip-beforeunload', this.handleSkipBeforeUnload);

View File

@ -71,14 +71,14 @@ export default {
required: true,
},
},
clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0'],
clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0 gl-m-0! gl-ml-3!'],
inputTypes: {
key: 'key',
value: 'value',
},
i18n: {
cancel: s__('CiVariables|Cancel'),
clearInputs: s__('CiVariables|Clear inputs'),
removeInputs: s__('CiVariables|Remove inputs'),
formHelpText: s__(
'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.',
),
@ -209,7 +209,7 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.id"
class="gl-display-flex gl-align-items-center gl-mb-4"
class="gl-display-flex gl-align-items-center gl-mb-5"
data-testid="ci-variable-row"
>
<gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
@ -244,12 +244,11 @@ export default {
<gl-button
v-if="canRemove(index)"
v-gl-tooltip
:aria-label="$options.i18n.clearInputs"
:title="$options.i18n.clearInputs"
:aria-label="$options.i18n.removeInputs"
:title="$options.i18n.removeInputs"
:class="$options.clearBtnSharedClasses"
category="tertiary"
variant="danger"
icon="clear"
icon="remove"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
@ -260,8 +259,7 @@ export default {
:class="$options.clearBtnSharedClasses"
data-testid="delete-variable-btn-placeholder"
category="tertiary"
variant="danger"
icon="clear"
icon="remove"
/>
</div>

View File

@ -2,6 +2,7 @@
import $ from 'jquery';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
import { insertText } from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import axios from '~/lib/utils/axios_utils';
const LINK_TAG_PATTERN = '[{text}](url)';
@ -520,7 +521,7 @@ function continueOlText(listLineMatch, nextLineMatch) {
function handleContinueList(e, textArea) {
if (!gon.markdown_automatic_lists) return;
if (!(e.key === 'Enter')) return;
if (!(e.key === ENTER_KEY)) return;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (textArea.selectionStart !== textArea.selectionEnd) return;
@ -577,6 +578,25 @@ function handleContinueList(e, textArea) {
}
}
/**
* Adds a Markdown hard break when `Shift+Enter` is pressed
*
* @param {Object} e - the event
* @param {Object} textArea - the targeted text area
*/
function handleHardBreak(e, textArea) {
if (!(e.key === ENTER_KEY)) return;
if (!e.shiftKey) return;
if (e.altKey || e.ctrlKey || e.metaKey) return;
// prevent unintended line breaks inserted using Japanese IME on MacOS
if (compositioningNoteText) return;
e.preventDefault();
insertText(textArea, '\\\n');
}
export function keypressNoteText(e) {
const textArea = this;
@ -584,6 +604,7 @@ export function keypressNoteText(e) {
handleContinueList(e, textArea);
handleSurroundSelectedText(e, textArea);
handleHardBreak(e, textArea);
}
export function compositionStartNoteText() {

View File

@ -20,7 +20,9 @@ import { initReportAbuse } from '~/projects/report_abuse';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
initDiffStatsDropdown(
(document.querySelector('.navbar-gitlab')?.offsetHeight ?? 0) + performanceHeight,
);
new ZenMode();
new ShortcutsNavigation();

View File

@ -70,7 +70,7 @@ export default {
return this.approvalDetails.length;
},
detailsPath() {
return `${this.branchRulesPath}?branch=${this.name}`;
return `${this.branchRulesPath}?branch=${encodeURIComponent(this.name)}`;
},
statusChecksText() {
return sprintf(this.$options.i18n.statusChecks, {

View File

@ -99,7 +99,7 @@ export default {
>
<gl-icon name="users" />
<gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm">
<span v-else class="gl-pt-2 gl-px-3 gl-font-sm">
{{ participantCount }}
</span>
</div>
@ -133,7 +133,6 @@ export default {
<gl-button
variant="link"
button-text-classes="gl-text-secondary"
data-testid="more-participants"
@click="toggleMoreParticipants"
>{{ toggleLabel }}</gl-button
>

View File

@ -0,0 +1,30 @@
<script>
import { MountingPortal } from 'portal-vue';
import { SIDEBAR_PORTAL_ID, portalState } from '../constants';
/**
* Use this component to render content into the sidebar.
*
* Arbitrary content is allowed, but nav items should be added using a Ruby
* Sidebars::Panel subclass instead.
*
* Only one instance of this component on a given page is supported. This is to
* avoid ordering issues and cluttering the sidebar.
*/
export default {
components: {
MountingPortal,
},
data() {
// This is shared state, by design. Do not mutate this state here.
return portalState;
},
mountSelector: `#${SIDEBAR_PORTAL_ID}`,
};
</script>
<template>
<mounting-portal v-if="ready" :mount-to="$options.mountSelector" append>
<slot></slot>
</mounting-portal>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { SIDEBAR_PORTAL_ID, portalState } from '../constants';
export default {
mounted() {
portalState.ready = true;
},
beforeDestroy() {
portalState.ready = false;
},
mountId: SIDEBAR_PORTAL_ID,
};
</script>
<template>
<div v-once :id="$options.mountId"></div>
</template>

View File

@ -1,6 +1,7 @@
<script>
import { GlCollapse } from '@gitlab/ui';
import UserBar from './user_bar.vue';
import SidebarPortalTarget from './sidebar_portal_target.vue';
import ContextSwitcherToggle from './context_switcher_toggle.vue';
import ContextSwitcher from './context_switcher.vue';
import HelpCenter from './help_center.vue';
@ -14,6 +15,7 @@ export default {
ContextSwitcher,
HelpCenter,
SidebarMenu,
SidebarPortalTarget,
},
props: {
sidebarData: {
@ -53,6 +55,7 @@ export default {
</gl-collapse>
<gl-collapse :visible="!contextSwitcherOpened">
<sidebar-menu :items="menuItems" />
<sidebar-portal-target />
</gl-collapse>
</div>
<div class="gl-p-3">

View File

@ -0,0 +1,11 @@
// Note: all constants defined here are considered internal implementation
// details for the sidebar. They should not be imported by anything outside of
// the super_sidebar directory.
import Vue from 'vue';
export const SIDEBAR_PORTAL_ID = 'sidebar-portal-mount';
export const portalState = Vue.observable({
ready: false,
});

View File

@ -133,6 +133,7 @@
.page-with-super-sidebar {
padding-left: 0;
transition: padding-left $gl-transition-duration-medium;
@include media-breakpoint-up(xl) {
padding-left: $contextual-sidebar-width;

View File

@ -1082,6 +1082,9 @@ $tabs-holder-z-index: 250;
.merge-request-sticky-header {
z-index: 204;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
}
.page-with-contextual-sidebar .merge-request-sticky-header {
--width: calc(100% - #{$contextual-sidebar-width});
@include media-breakpoint-down(lg) {
@ -1093,6 +1096,18 @@ $tabs-holder-z-index: 250;
--width: calc(100% - #{$contextual-sidebar-collapsed-width});
}
.page-with-super-sidebar .merge-request-sticky-header {
@include media-breakpoint-up(xl) {
--width: calc(100% - #{$super-sidebar-width});
}
}
.page-with-super-sidebar-collapsed .merge-request-sticky-header {
@include media-breakpoint-up(xl) {
--width: 100%;
}
}
.merge-request-notification-toggle {
.gl-toggle {
@include gl-ml-auto;

View File

@ -272,20 +272,30 @@ ul.related-merge-requests > li gl-emoji {
@include media-breakpoint-up(md) {
// collapsed left sidebar + collapsed right sidebar
.issue-sticky-header {
.page-with-contextual-sidebar .issue-sticky-header {
left: $contextual-sidebar-collapsed-width;
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-collapsed-width});
}
// collapsed left sidebar + expanded right sidebar
.right-sidebar-expanded .issue-sticky-header {
.page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header {
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width});
}
// collapsed super sidebar + collapsed right sidebar
.page-with-super-sidebar .issue-sticky-header {
--width: calc(100% - #{$gutter-collapsed-width});
}
// collapsed super sidebar + expanded right sidebar
.page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header {
--width: calc(100% - #{$gutter-width});
}
}
@include media-breakpoint-up(xl) {
// expanded left sidebar + collapsed right sidebar
.issue-sticky-header {
.page-with-contextual-sidebar .issue-sticky-header {
left: $contextual-sidebar-width;
--width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-collapsed-width});
}
@ -297,14 +307,38 @@ ul.related-merge-requests > li gl-emoji {
}
// expanded left sidebar + expanded right sidebar
.right-sidebar-expanded .issue-sticky-header {
.page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header {
--width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-width});
}
// collapsed left sidebar + expanded right sidebar
.right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header {
.page-with-contextual-sidebar.right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header {
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width});
}
// expanded super sidebar + collapsed right sidebar
.page-with-super-sidebar .issue-sticky-header {
left: $super-sidebar-width;
--width: calc(100% - #{$super-sidebar-width} - #{$gutter-collapsed-width});
}
// collapsed super sidebar + collapsed right sidebar
.page-with-super-sidebar-collapsed .issue-sticky-header {
left: 0;
--width: calc(100% - #{$gutter-collapsed-width});
}
// expanded super sidebar + expanded right sidebar
.page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header {
left: $super-sidebar-width;
--width: calc(100% - #{$super-sidebar-width} - #{$gutter-width});
}
// collapsed super sidebar + expanded right sidebar
.page-with-super-sidebar-collapsed.right-sidebar-expanded .issue-sticky-header {
left: 0;
--width: calc(100% - #{$gutter-width});
}
}
.issuable-header-slide-enter-active,

View File

@ -90,6 +90,10 @@ module SortingPreference
return false unless sort_order
return can_sort_by_issue_weight?(action_name == 'issues') if sort_order.include?('weight')
if sort_order.include?('merged_at')
return can_sort_by_merged_date?(controller_name == 'merge_requests' || action_name == 'merge_requests')
end
true
end
end

View File

@ -22,8 +22,6 @@ module JiraConnect
end
def public_key_storage_enabled?
return true if Gitlab.config.jira_connect.enable_public_keys_storage
Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
end
end

View File

@ -12,7 +12,7 @@ module JiraConnectHelper
users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in
gitlab_user_path: current_user ? user_path(current_user) : nil,
oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data(installation).to_json : nil,
public_key_storage_enabled: Gitlab.config.jira_connect.enable_public_keys_storage || Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
public_key_storage_enabled: Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
}
end

View File

@ -11,10 +11,15 @@ module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-super-sidebar' if show_super_sidebar? && @left_sidebar
class_name << 'page-with-super-sidebar-collapsed' if show_super_sidebar? && collapsed_super_sidebar? && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar && !show_super_sidebar?
if show_super_sidebar?
class_name << 'page-with-super-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-super-sidebar-collapsed' if collapsed_super_sidebar? && @left_sidebar
else
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
end
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name

View File

@ -227,7 +227,7 @@ module SortingHelper
options.concat([due_date_option]) if viewing_issues
options.concat([popularity_option, label_priority_option])
options.concat([merged_option]) if viewing_merge_requests
options.concat([merged_option]) if can_sort_by_merged_date?(viewing_merge_requests)
options.concat([relative_position_option]) if viewing_issues
options.concat([title_option])
@ -237,6 +237,10 @@ module SortingHelper
false
end
def can_sort_by_merged_date?(viewing_merge_requests)
viewing_merge_requests && %w[all merged].include?(params[:state])
end
def due_date_option
{ value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) }
end

View File

@ -58,15 +58,15 @@
= c.body do
= link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= label_tag :pin_code, _('Enter verification code'), class: "label-bold"
= text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
- if current_password_required?
.form-group
= label_tag :current_password, _('Current password'), class: 'label-bold'
= password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
%p.form-text.text-muted
= _('Your current password is required to register a two-factor authenticator app.')
.form-group
= label_tag :pin_code, _('Enter verification code'), class: "label-bold"
= text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }

View File

@ -11,6 +11,9 @@
= c.body do
= s_('ExternalIssueIntegration|Only one issue tracker integration can be active at a time. Please disable the active tracker first and try again.')
- if integration.to_param === 'slack'
= render 'shared/integrations/slack_notifications_deprecation_alert'
%h2.gl-mb-4
= integration.title
- if integration.operating?

View File

@ -495,7 +495,6 @@ production: &base
## To switch to a Jira connect development environment
jira_connect:
# atlassian_js_url: 'http://localhost:9292/atlassian.js'
# enable_public_keys_storage: true
# enforce_jira_base_url_https: false
# additional_iframe_ancestors: ['localhost:*']

View File

@ -449,8 +449,6 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
Settings['jira_connect'] ||= Settingslogic.new({})
Settings.jira_connect['atlassian_js_url'] ||= 'https://connect-cdn.atl-paas.net/all.js'
Settings.jira_connect['enable_public_keys_storage'] ||= false
Settings.jira_connect['enable_public_keys_storage'] = true if Gitlab.com?
Settings.jira_connect['enforce_jira_base_url_https'] = true if Settings.jira_connect['enforce_jira_base_url_https'].nil?
Settings.jira_connect['additional_iframe_ancestors'] ||= []
@ -830,7 +828,7 @@ Gitlab.ee do
Settings.cron_jobs['abandoned_trial_emails']['cron'] ||= "0 1 * * *"
Settings.cron_jobs['abandoned_trial_emails']['job_class'] = 'Emails::AbandonedTrialEmailsCronWorker'
Settings.cron_jobs['package_metadata_sync_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 1 * * *"
Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 * * * *"
Settings.cron_jobs['package_metadata_sync_worker']['job_class'] = 'PackageMetadata::SyncWorker'
Gitlab.com do
Settings.cron_jobs['free_user_cap_backfill_notification_jobs_worker'] ||= Settingslogic.new({})

View File

@ -0,0 +1,10 @@
---
table_name: ci_cost_settings
classes:
- Ci::Minutes::CostSetting
feature_categories:
- continuous_integration
description: A set of cost factors per runner which are applied to ci job duration based on project type.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111977
milestone: '15.10'
gitlab_schema: gitlab_ci

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class CreateCiRunnerCostSettings < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def change
create_table :ci_cost_settings, id: false do |t|
t.timestamps_with_timezone null: false
t.references :runner, null: false, primary_key: true, index: false,
foreign_key: { to_table: :ci_runners, on_delete: :cascade },
type: :bigint, default: nil
t.float :standard_factor, null: false, default: 1.00
t.float :os_contribution_factor, null: false, default: 0.008
t.float :os_plan_factor, null: false, default: 0.5
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
class CreateInitialPartitionForCiRunnerMachineBuilds < Gitlab::Database::Migration[2.1]
PARTITION_NAME = 'gitlab_partitions_dynamic.ci_runner_machine_builds_100'
TABLE_NAME = 'p_ci_runner_machine_builds'
FIRST_PARTITION = 100
BUILDS_TABLE = 'ci_builds'
disable_ddl_transaction!
def up
with_lock_retries(**lock_args) do
connection.execute(<<~SQL)
LOCK TABLE #{BUILDS_TABLE} IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE ONLY #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE;
SQL
connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS #{PARTITION_NAME}
PARTITION OF #{TABLE_NAME}
FOR VALUES IN (#{FIRST_PARTITION});
SQL
end
end
def down
# no-op
#
# The migration should not remove the partition table since it might
# have been created by 20230215074223_add_ci_runner_machine_builds_partitioned_table.rb.
# In that case, the rollback would result in a different state.
end
private
def lock_args
{
raise_on_exhaustion: true,
timing_configuration: lock_timing_configuration
}
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

View File

@ -0,0 +1 @@
ee00d6aba8a310c236dd16749228a42589657d060bbf1785c4358bf886fd59cc

View File

@ -0,0 +1 @@
661fdc00029ab9bae8b4da6a8d92f172db89087aecc13f3ad65b2b3e8ad501d3

View File

@ -543,6 +543,13 @@ CREATE TABLE batched_background_migration_job_transition_logs (
)
PARTITION BY RANGE (created_at);
CREATE TABLE p_ci_runner_machine_builds (
partition_id bigint NOT NULL,
build_id bigint NOT NULL,
runner_machine_id bigint NOT NULL
)
PARTITION BY LIST (partition_id);
CREATE TABLE incident_management_pending_alert_escalations (
id bigint NOT NULL,
rule_id bigint NOT NULL,
@ -13060,6 +13067,15 @@ CREATE SEQUENCE ci_builds_runner_session_id_seq
ALTER SEQUENCE ci_builds_runner_session_id_seq OWNED BY ci_builds_runner_session.id;
CREATE TABLE ci_cost_settings (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
runner_id bigint NOT NULL,
standard_factor double precision DEFAULT 1.0 NOT NULL,
os_contribution_factor double precision DEFAULT 0.008 NOT NULL,
os_plan_factor double precision DEFAULT 0.5 NOT NULL
);
CREATE TABLE ci_daily_build_group_report_results (
id bigint NOT NULL,
date date NOT NULL,
@ -19018,13 +19034,6 @@ CREATE SEQUENCE operations_user_lists_id_seq
ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id;
CREATE TABLE p_ci_runner_machine_builds (
partition_id bigint NOT NULL,
build_id bigint NOT NULL,
runner_machine_id bigint NOT NULL
)
PARTITION BY LIST (partition_id);
CREATE TABLE packages_build_infos (
id bigint NOT NULL,
package_id integer NOT NULL,
@ -26219,6 +26228,9 @@ ALTER TABLE ONLY ci_builds
ALTER TABLE ONLY ci_builds_runner_session
ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ci_cost_settings
ADD CONSTRAINT ci_cost_settings_pkey PRIMARY KEY (runner_id);
ALTER TABLE ONLY ci_daily_build_group_report_results
ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
@ -35618,6 +35630,9 @@ ALTER TABLE ONLY geo_hashed_storage_migrated_events
ALTER TABLE ONLY plan_limits
ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_cost_settings
ADD CONSTRAINT fk_rails_6a70651f75 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE;
ALTER TABLE ONLY operations_feature_flags_issues
ADD CONSTRAINT fk_rails_6a8856ca4f FOREIGN KEY (feature_flag_id) REFERENCES operations_feature_flags(id) ON DELETE CASCADE;

View File

@ -113,6 +113,8 @@ To disable the inbound job token scope allowlist:
1. Toggle **Allow access to this project with a CI_JOB_TOKEN** to disabled.
Enabled by default in new projects.
You can also disable the allowlist [with the API](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate).
### Add a project to the inbound job token scope allowlist
You can add projects to the inbound allowlist for a project. Projects added to the allowlist
@ -133,6 +135,8 @@ To add a project:
1. Under **Allow CI job tokens from the following projects to access this project**,
add projects to the allowlist.
You can also add a target project to the allowlist [with the API](../../api/graphql/reference/index.md#mutationcijobtokenscopeaddproject).
### Limit your project's job token access
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1. [Deployed behind the `:ci_scoped_job_token` feature flag](../../user/feature_flags.md), disabled by default.

View File

@ -207,6 +207,14 @@ class Boards::ListsController < ApplicationController
end
```
A custom RSpec matcher is available to check endpoint's request urgency in the controller specs:
```ruby
specify do
expect(get(:index, params: request_params)).to have_request_urgency(:medium)
end
```
### Grape endpoints
To specify the urgency for an entire API class:
@ -240,6 +248,15 @@ get 'client/features', urgency: :low do
end
```
A custom RSpec matcher is also compatible with grape endpoints' specs:
```ruby
specify do
expect(get(api('/avatar'), params: { email: 'public@example.com' })).to have_request_urgency(:medium)
end
```
WARNING:
We can't specify the urgency at the namespace level. The directive is ignored when doing so.

View File

@ -0,0 +1,38 @@
---
stage: Manage
group: Foundations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Navigation sidebar
Follow these guidelines when contributing additions or changes to the
[redesigned](https://gitlab.com/groups/gitlab-org/-/epics/9044) navigation
sidebar.
These guidelines reflect the current state of the navigation sidebar. However,
the sidebar is a work in progress, and so is this documentation.
## Enable the new navigation sidebar
To enable the new navigation sidebar:
- Enable the `super_sidebar_nav` feature flag.
- Select your avatar, then turn on the **New navigation** toggle.
## Adding page-specific Vue content
Pages can render arbitrary content into the sidebar using the `SidebarPortal`
component. Content passed to its default slot is rendered below that
page's navigation items in the sidebar.
NOTE:
Only one instance of this component on a given page is supported. This is to
avoid ordering issues and cluttering the sidebar.
NOTE:
Arbitrary content is allowed, but nav items should be implemented by
subclassing `::Sidebars::Panel`.
NOTE:
Do not use the `SidebarPortalTarget` component. It is internal to the sidebar.

View File

@ -484,19 +484,17 @@ This should let us:
Our test suite runs against PG12 as GitLab.com runs on PG12 and
[Omnibus defaults to PG12 for new installs and upgrades](../../administration/package_information/postgresql_versions.md).
We do run our test suite against PG11 and PG13 on nightly scheduled pipelines.
We also run our test suite against PG11 upon specific database library changes in MRs and `main` pipelines (with the `rspec db-library-code pg11` job).
We do run our test suite against PG13 on nightly scheduled pipelines.
#### Current versions testing
| Where? | PostgreSQL version | Ruby version |
|------------------------------------------------------------------------------------------------|-------------------------------------------------|--------------|
| Merge requests | 12 (default version), 11 for DB library changes | 3.0 (default version) |
| `master` branch commits | 12 (default version), 11 for DB library changes | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version), 11 for DB library changes | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version), 11 for DB library changes | 2.7 |
| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 11, 13 | 3.0 (default version) |
| Where? | PostgreSQL version | Ruby version |
|------------------------------------------------------------------------------------------------|--------------------------|-----------------------|
| Merge requests | 12 (default version) | 3.0 (default version) |
| `master` branch commits | 12 (default version) | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version) | 3.0 (default version) |
| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version) | 2.7 |
| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 13 | 3.0 (default version) |
There are 2 pipeline schedules used for testing Ruby 2.7. One is triggering a
pipeline in `ruby2-sync` branch, which updates the `ruby2` branch with latest
@ -518,7 +516,6 @@ We follow the [PostgreSQL versions shipped with Omnibus GitLab](../../administra
| PostgreSQL version | 14.1 (July 2021) | 14.2 (August 2021) | 14.3 (September 2021) | 14.4 (October 2021) | 14.5 (November 2021) | 14.6 (December 2021) |
| -------------------| ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- |
| PG12 | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` |
| PG11 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` |
| PG13 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` |
### Redis versions testing

View File

@ -136,8 +136,6 @@ that are scoped to a single [configuration keyword](../../ci/yaml/index.md#job-k
| `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. |
| `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. |
| `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. |
| `.use-pg11` | Allows a job to run the `postgres` 11 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg11-ee` | Same as `.use-pg11` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
| `.use-pg12` | Allows a job to use the `postgres` 12 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg12-ee` | Same as `.use-pg12` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
| `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |

View File

@ -6,220 +6,312 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Publish packages with Yarn
Publish npm packages in your project's Package Registry using Yarn. Then install the
packages whenever you need to use them as a dependency.
You can publish packages with [Yarn 1 (Classic)](https://classic.yarnpkg.com) and [Yarn 2+](https://yarnpkg.com).
Learn how to build a [yarn](../workflows/build_packages.md#yarn) package.
To find the Yarn version used in the deployment container, run `yarn --version` in the `script` block of the CI
script job block that is responsible for calling `yarn publish`**`. The Yarn version is shown in the pipeline output.
You can get started with Yarn 2 by following the [Yarn documentation](https://yarnpkg.com/getting-started/install/).
Learn how to build a [Yarn](../workflows/build_packages.md#yarn) package.
You can use the Yarn documentation to get started with
[Yarn Classic](https://classic.yarnpkg.com/en/docs/getting-started) and
[Yarn 2+](https://yarnpkg.com/getting-started/).
## Publish to GitLab Package Registry
You can use Yarn to publish to the GitLab Package Registry.
### Authentication to the Package Registry
You need a token to publish a package. Different tokens are available depending on what you're trying to
achieve. For more information, review the [guidance on tokens](../../../user/packages/package_registry/index.md#authenticate-with-the-registry).
- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`.
- If you publish a package via CI/CD pipelines, you must use a CI job token.
- If your organization uses two-factor authentication (2FA), you must use a
personal access token with the scope set to `api`.
- If you publish a package via CI/CD pipelines, you can use a CI job token in
private runners or you can register a variable for shared runners.
Create a token and save it to use later in the process.
### Publish configuration
### Naming convention
Depending on how you install the package, you may need to adhere to the naming convention.
You can use one of two API endpoints to install packages:
- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
If you plan to install a package through the [project level](#install-from-the-project-level), you do not have to
adhere to the naming convention.
If you plan to install a package through the [instance level](#install-from-the-instance-level), then you must name
your package with a [scope](https://docs.npmjs.com/misc/scope/). Scoped packages begin with a `@` and have the
`@owner/package-name` format. You can set up the scope for your package in the `.yarnrc.yml` file and by using the
`publishConfig` option in the `package.json`.
- The value used for the `@scope` is the root of the project that hosts the packages and not the root
of the project with the package's source code. The scope should be lowercase.
- The package name can be anything you want
| Project URL | Package Registry in | Scope | Full package name |
| ------------------------------------------------------- | ------------------- | --------- | ---------------------- |
| `https://gitlab.com/my-org/engineering-group/analytics` | Analytics | `@my-org` | `@my-org/package-name` |
### Configuring `.yarnrc.yml` to publish from the project level
To publish with the project-level npm endpoint, set the following configuration in
`.yarnrc.yml`:
To publish, set the following configuration in `.yarnrc.yml`. This file should be
located in the root directory of your package project source where `package.json` is found.
```yaml
npmScopes:
foo:
npmRegistryServer: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
<my-org>:
npmPublishRegistry: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
npmRegistries:
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:
npmAlwaysAuth: true
npmAuthToken: '<your_token>'
```
In this configuration:
- Replace `<my-org>` with your organization scope, exclude the `@` symbol.
- Replace `<your_domain>` with your domain name.
- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page.
- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
- Replace `<your_token>` with a deployment token, group access token, project access token, or personal access token.
### Configuring `.yarnrc.yml` to publish from the instance level
Scoped registry does not work in Yarn Classic in `package.json` file, based on
this [issue](https://github.com/yarnpkg/yarn/pull/7829).
Therefore, under `publishConfig` there should be `registry` and not `@scope:registry` for Yarn Classic.
You can publish using your command line or a CI/CD pipeline to the GitLab Package Registry.
For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`:
```yaml
npmScopes:
<scope>:
npmRegistryServer: 'https://<your_domain>/api/v4/packages/npm/'
npmRegistries:
//gitlab.example.com/api/v4/packages/npm/:
npmAlwaysAuth: true
npmAuthToken: '<your_token>'
```
In this configuration:
- Replace `<your_domain>` with your domain name.
- Your scope is `<scope>`, without `@`.
- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
### Publishing a package via the command line
Publish a package:
### Publishing via the command line - Manual Publish
```shell
npm publish
# Yarn 1 (Classic)
yarn publish
# Yarn 2+
yarn npm publish
```
Your package should now publish to the Package Registry.
### Publishing via a CI/CD pipeline
### Publishing via a CI/CD pipeline - Automated Publish
In the GitLab project that houses your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example:
You can use pipeline variables when you use this method.
You can use **Shared Runners** *(Default)* or **Private Runners** (Advanced).
#### Shared runners
Third party images such as `node:latest` or `node:current` do not have direct access
to the `CI_JOB_TOKEN` when operating in a shared runner. You must configure an
authentication token or use a private runner.
To create a authentication token:
1. On the top bar, select **Main menu**, and:
- For a project, select **Projects** and find your project.
- For a group, select **Groups** and find your group.
1. On the left sidebar, select **Settings > Repository > Deploy Tokens**.
1. Create a deployment token with `read_package_registry` and `write_package_registry` scopes and copy the generated token.
1. On the left sidebar, select **Settings > CI/CD > Variables**.
1. Select `Add variable` and use the following settings:
| Field | Value |
|--------------------|------------------------------|
| key | `NPM_AUTH_TOKEN` |
| value | `<DEPLOY-TOKEN-FROM-STEP-3>` |
| type | Variable |
| Protected variable | `CHECKED` |
| Mask variable | `CHECKED` |
| Expand variable | `CHECKED` |
To use any **Protected variable**:
1. Go to the repository that contains the Yarn package source code.
1. On the left sidebar, select **Settings > Repository**.
- If you are building from branches with tags, select **Protected Tags** and add `v*` (wildcard) for semantic versioning.
- If you are building from branches without tags, select **Protected Branches**.
Then add the `NPM_AUTH_TOKEN` created above, to the `.yarnrc.yml` configuration
in your package project root directory where `package.json` is found:
```yaml
image: node:latest
npmScopes:
esp-code:
npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
npmAlwaysAuth: true
npmAuthToken: "${NPM_AUTH_TOKEN}"
```
#### Private runners
Add the `CI_JOB_TOKEN` to the `.yarnrc.yml` configuration in your package project
root directory where `package.json` is found:
```yaml
npmScopes:
esp-code:
npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
npmAlwaysAuth: true
npmAuthToken: "${CI_JOB_TOKEN}"
```
To publish the package using CI/CD pipeline, In the GitLab project that houses
your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example to trigger
only on any tag push:
```yaml
# Yarn 1
image: node:lts
stages:
- deploy
rules:
- if: $CI_COMMIT_TAG
deploy:
stage: deploy
script:
- npm publish
- yarn publish
```
```yaml
# Yarn 2+
image: node:lts
stages:
- deploy
rules:
- if: $CI_COMMIT_TAG
deploy:
stage: deploy
before_script:
- corepack enable
- yarn set version stable
script:
- yarn npm publish
```
Your package should now publish to the Package Registry when the pipeline runs.
## Install a package
If multiple packages have the same name and version, the most recently-published package is retrieved when you install a package.
NOTE:
If multiple packages have the same name and version, the most recently-published
package is retrieved when you install a package.
You can install a package from a GitLab project or instance:
You can use one of two API endpoints to install packages:
- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
- **Instance-level**: Best used when working with many packages in an organization scope.
- If you plan to install a package through the [instance level](#install-from-the-instance-level),
then you must name your package with a [scope](https://docs.npmjs.com/misc/scope/).
Scoped packages begin with a `@` and have the `@owner/package-name` format. You can set up
the scope for your package in the `.yarnrc.yml` file and by using the `publishConfig`
option in the `package.json`.
- The value used for the `@scope` is the organization root (top-level project) `...com/my-org`
*(@my-org)* that hosts the packages, not the root of the project with the package's source code.
- The scope is always lowercase.
- The package name can be anything you want `@my-org/any-name`.
- **Project-level**: For when you have a one-off package.
If you plan to install a package through the [project level](#install-from-the-project-level),
you do not have to adhere to the naming convention.
| Project URL | Package Registry | Organization Scope | Full package name |
|-------------------------------------------------------------------|----------------------|--------------------|-----------------------------|
| `https://gitlab.com/<my-org>/<group-name>/<package-name-example>` | Package Name Example | `@my-org` | `@my-org/package-name` |
| `https://gitlab.com/<example-org>/<group-name>/<project-name>` | Project Name | `@example-org` | `@example-org/project-name` |
You can install from the instance level or from the project level.
The configurations for `.yarnrc.yml` can be added per package consuming project
root where `package.json` is located, or you can use a global
configuration located in your system user home directory.
### Install from the instance level
WARNING:
You must use packages published with the scoped [naming convention](#naming-convention) when you install a package from the instance level.
Use these steps for global configuration in the `.yarnrc.yml` file:
1. Authenticate to the Package Registry
1. [Configure organization scope](#configure-organization-scope).
1. [Set the registry](#set-the-registry).
If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
#### Configure organization scope
```shell
npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token
```
```yaml
npmScopes:
<my-org>:
npmRegistryServer: "https://<your_domain_name>/api/v4/packages/npm"
```
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol.
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
1. Set the registry
#### Set the registry
```shell
npm config set @scope:registry https://your_domain_name.com/api/v4/packages/npm/
```
Skip this step if your package is public not private.
- Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
```yaml
npmRegistries:
//<your_domain_name>/api/v4/packages/npm:
npmAlwaysAuth: true
npmAuthToken: "<your_token>"
```
1. Install the package
```shell
yarn add @scope/my-package
```
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token.
### Install from the project level
1. Authenticate to the Package Registry
Use these steps for each project in the `.yarnrc.yml` file:
If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
1. [Configure project scope](#configure-project-scope).
1. [Set the registry](#set-the-registry-project-level).
```shell
npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token
```
#### Configure project scope
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_project_id` is your project ID, found on the project's home page.
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
```yaml
npmScopes:
<my-org>:
npmRegistryServer: "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm"
```
1. Set the registry
- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol.
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
- Replace `<your_project_id>` with your project ID, found on the project's home page.
```shell
npm config set @scope:registry=https://your_domain_name/api/v4/projects/your_project_id/packages/npm/
```
#### Set the registry (project level)
- Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_project_id` is your project ID, found on the project's home page.
Skip this step if your package is public not private.
1. Install the package
```yaml
npmRegistries:
//<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm:
npmAlwaysAuth: true
npmAuthToken: "<your_token>"
```
```shell
yarn add @scope/my-package
```
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token.
- Replace `<your_project_id>` with your project ID, found on the project's home page.
## Helpful hints
### Install the package
For full helpful hints information, refer to the [npm documentation](../npm_registry/index.md#helpful-hints).
For Yarn 2+, use `yarn add` either in the command line or in the CI/CD pipelines to install your packages:
### Supported CLI commands
```shell
yarn add @scope/my-package
```
The GitLab npm repository supports the following commands for the npm CLI (`npm`) and yarn CLI
(`yarn`):
#### For Yarn Classic
- `npm install`: Install npm packages.
- `npm publish`: Publish an npm package to the registry.
- `npm dist-tag add`: Add a dist-tag to an npm package.
- `npm dist-tag ls`: List dist-tags for a package.
- `npm dist-tag rm`: Delete a dist-tag.
- `npm ci`: Install npm packages directly from your `package-lock.json` file.
- `npm view`: Show package metadata.
- `yarn add`: Install an npm package.
- `yarn update`: Update your dependencies.
The Yarn Classic setup, requires both `.npmrc` and `.yarnrc` files as
[mentioned in issue](https://github.com/yarnpkg/yarn/issues/4451#issuecomment-753670295):
- Place credentials in the `.npmrc` file.
- Place the scoped registry in the `.yarnrc` file.
```shell
# .npmrc
//<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/:_authToken="<your_token>"
# .yarnrc
"@scope:registry" "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/"
```
Then you can use `yarn add` to install your packages.
## Related topics
- For full helpful hints information, see the
[npm documentation](../npm_registry/index.md#helpful-hints).
- For Yarn 1 to Yarn 2+ migration information see the
[Yarn Migration Guide](https://yarnpkg.com/getting-started/migration).
## Troubleshooting
For full troubleshooting information, refer to the [npm documentation](../npm_registry/index.md#troubleshooting).
### Error running Yarn with the Package Registry for the npm registry
If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get
an error message like:
If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get an error message like:
```shell
yarn install v1.15.2
@ -233,14 +325,7 @@ info If you think this is a bug, please open a bug report with the information p
info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command
```
In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
with your personal access token or deploy token):
```plaintext
//gitlab.example.com/api/v4/projects/:_authToken=<your_token>
```
You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically:
In this case, the following commands creates a file called `.yarnrc` in the current directory. Make sure to be in either your user home directory for global configuration or your project root for per-project configuration:
```shell
yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"

View File

@ -39,9 +39,9 @@ These shortcuts are available in most areas of GitLab:
| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. |
| <kbd>Shift</kbd> + <kbd>r</kbd> | Go to your Review requests page. |
| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. |
| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. |
| <kbd>Escape</kbd> | Hide tooltips or popovers. |
| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
| <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). |
Additionally, the following shortcuts are available when editing text in text
@ -55,9 +55,10 @@ descriptions):
| <kbd>Command</kbd> + <kbd>b</kbd> | <kbd>Control</kbd> + <kbd>b</kbd> | Bold the selected text (surround it with `**`). |
| <kbd>Command</kbd> + <kbd>i</kbd> | <kbd>Control</kbd> + <kbd>i</kbd> | Italicize the selected text (surround it with `_`). |
| <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | Strike through the selected text (surround it with `~~`). |
| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). |
| <kbd>Command</kbd> + <kbd>&#93;</kbd> | <kbd>Control</kbd> + <kbd>&#93;</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
| <kbd>Command</kbd> + <kbd>&#91;</kbd> | <kbd>Control</kbd> + <kbd>&#91;</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). |
| <kbd>Command</kbd> + <kbd>&#93;</kbd> | <kbd>Control</kbd> + <kbd>&#93;</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
| <kbd>Command</kbd> + <kbd>&#91;</kbd> | <kbd>Control</kbd> + <kbd>&#91;</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
| <kbd>Shift</kbd> + <kbd>Enter</kbd> | <kbd>Shift</kbd> + <kbd>Enter</kbd> | Add a [line break](markdown.md#newlines). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21916) in GitLab 15.10. |
The shortcuts for editing in text fields are always enabled, even if other
keyboard shortcuts are disabled.
@ -112,7 +113,7 @@ These shortcuts are available when viewing [merge requests](project/merge_reques
| macOS shortcut | Windows shortcut | Description |
|-----------------------------------|---------------------|-------------|
| <kbd>]</kbd> or <kbd>j</kbd> | | Move to next file. |
| <kbd>&#91;</kbd> or <kbd>k</kbd> | | Move to previous file. |
| <kbd>&#91;</kbd> or <kbd>k</kbd> | | Move to previous file. |
| <kbd>Command</kbd> + <kbd>p</kbd> | <kbd>Control</kbd> + <kbd>p</kbd> | Search for, and then jump to a file for review. |
| <kbd>n</kbd> | | Move to next unresolved discussion. |
| <kbd>p</kbd> | | Move to previous unresolved discussion. |
@ -277,7 +278,7 @@ These shortcuts are available when editing a file with the
| <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | Highlight |
| <kbd>Command</kbd> + <kbd>,</kbd> | <kbd>Control</kbd> + <kbd>,</kbd> | Subscript |
| <kbd>Command</kbd> + <kbd>.</kbd> | <kbd>Control</kbd> + <kbd>.</kbd> | Superscript |
| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list |
| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list |
| <kbd>Shift</kbd> + <kbd>Tab</kbd> | <kbd>Shift</kbd> + <kbd>Tab</kbd> | Outdent list |
#### Text selection

View File

@ -45,6 +45,11 @@ module Gitlab
return gitlab_schema
end
# Partitions that belong to the CI domain
if table_name.start_with?('ci_') && gitlab_schema = views_and_tables_to_schema["p_#{table_name}"]
return gitlab_schema
end
# All tables from `information_schema.` are marked as `internal`
return :gitlab_internal if schema_name == 'information_schema'

View File

@ -8869,9 +8869,6 @@ msgstr ""
msgid "CiVariables|Cannot use Masked Variable with current value"
msgstr ""
msgid "CiVariables|Clear inputs"
msgstr ""
msgid "CiVariables|Environments"
msgstr ""
@ -8899,6 +8896,9 @@ msgstr ""
msgid "CiVariables|Protected"
msgstr ""
msgid "CiVariables|Remove inputs"
msgstr ""
msgid "CiVariables|Remove variable"
msgstr ""

View File

@ -26,11 +26,14 @@ RSpec.describe SortingPreference do
describe '#set_sort_order' do
let(:group) { build(:group) }
let(:controller_name) { 'issues' }
let(:action_name) { 'issues' }
let(:issue_weights_available) { true }
before do
allow(controller).to receive(:default_sort_order).and_return('updated_desc')
allow(controller).to receive(:action_name).and_return('issues')
allow(controller).to receive(:controller_name).and_return(controller_name)
allow(controller).to receive(:action_name).and_return(action_name)
allow(controller).to receive(:can_sort_by_issue_weight?).and_return(issue_weights_available)
user.user_preference.update!(issues_sort: sorting_field)
end
@ -62,6 +65,42 @@ RSpec.describe SortingPreference do
end
end
end
context 'when user preference contains merged date sorting' do
let(:sorting_field) { 'merged_at_desc' }
let(:can_sort_by_merged_date?) { false }
before do
allow(controller)
.to receive(:can_sort_by_merged_date?)
.with(can_sort_by_merged_date?)
.and_return(can_sort_by_merged_date?)
end
it 'sets default sort order' do
is_expected.to eq('updated_desc')
end
shared_examples 'user can sort by merged date' do
it 'sets sort order from user_preference' do
is_expected.to eq('merged_at_desc')
end
end
context 'when controller_name is merge_requests' do
let(:controller_name) { 'merge_requests' }
let(:can_sort_by_merged_date?) { true }
it_behaves_like 'user can sort by merged date'
end
context 'when action_name is merge_requests' do
let(:action_name) { 'merge_requests' }
let(:can_sort_by_merged_date?) { true }
it_behaves_like 'user can sort by merged date'
end
end
end
describe '#set_sort_order_from_user_preference' do

View File

@ -37,6 +37,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
project.add_developer(user)
end
specify { expect(get(:index, params: request_params)).to have_request_urgency(:medium) }
it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do
last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds
@ -244,6 +246,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
sign_in(user)
end
specify { expect(create!).to have_request_urgency(:low) }
describe 'making the creation request' do
before do
create!
@ -732,19 +736,21 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
end
describe 'PUT update' do
context "should update the note with a valid issue" do
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
id: note,
format: :json,
note: {
note: "New comment"
}
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
id: note,
format: :json,
note: {
note: "New comment"
}
end
}
end
specify { expect(put(:update, params: request_params)).to have_request_urgency(:low) }
context "should update the note with a valid issue" do
before do
sign_in(note.author)
project.add_developer(note.author)
@ -790,6 +796,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
}
end
specify { expect(delete(:destroy, params: request_params)).to have_request_urgency(:low) }
context 'user is the author of a note' do
before do
sign_in(note.author)
@ -831,6 +839,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
let(:emoji_name) { 'thumbsup' }
it { is_expected.to have_request_urgency(:low) }
it "toggles the award emoji" do
expect do
subject
@ -866,6 +876,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
sign_in user
end
specify { expect(post(:resolve, params: request_params)).to have_request_urgency(:low) }
context "when the user is not authorized to resolve the note" do
it "returns status 404" do
post :resolve, params: request_params
@ -929,6 +941,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
note.resolve!(user)
end
specify { expect(delete(:unresolve, params: request_params)).to have_request_urgency(:low) }
context "when the user is not authorized to resolve the note" do
it "returns status 404" do
delete :unresolve, params: request_params
@ -998,6 +1012,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(json_response.count).to eq(1)
expect(json_response.first).to include({ "line_text" => "Test" })
end
specify { expect(get(:outdated_line_change, params: request_params)).to have_request_urgency(:low) }
end
# Convert a time to an integer number of microseconds

View File

@ -6,7 +6,7 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
describe "single commit view" do
shared_examples "single commit view" do
let(:commit) do
project.repository.commits(nil, limit: 100).find do |commit|
commit.diffs.size > 1
@ -69,4 +69,15 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
end
end
end
it_behaves_like "single commit view"
context "when super sidebar is enabled" do
before do
user.update!(use_new_navigation: true)
stub_feature_flags(super_sidebar_nav: true)
end
it_behaves_like "single commit view"
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
@fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.seconds.ago)
@fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 20.seconds.ago)
@markdown = create(:merge_request,
title: 'markdown',
@ -33,7 +33,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
reviewers: [user, user2, user3, user4],
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
updated_at: 2.minutes.ago,
state: 'merged')
@markdown.metrics.update!(merged_at: 10.minutes.ago, latest_closed_at: 10.seconds.ago)
@merge_test = create(:merge_request,
@ -49,7 +50,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
source_project: project,
source_branch: 'feautre',
created_at: 2.minutes.ago,
updated_at: 1.minute.ago)
updated_at: 1.minute.ago,
state: 'merged')
@feature.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.minutes.ago)
end
@ -79,10 +81,9 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(page).to have_current_path(project_merge_requests_path(project), ignore_query: true)
expect(page).to have_content 'merge-test'
expect(page).to have_content 'feature'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
expect(count_merge_requests).to eq(2)
expect(count_merge_requests).to eq(1)
end
it 'filters on a specific assignee' do
@ -90,8 +91,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(page).not_to have_content 'merge-test'
expect(page).to have_content 'fix'
expect(page).to have_content 'markdown'
expect(count_merge_requests).to eq(2)
expect(count_merge_requests).to eq(1)
end
it 'sorts by newest' do
@ -99,35 +99,35 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(first_merge_request).to include('fix')
expect(last_merge_request).to include('merge-test')
expect(count_merge_requests).to eq(4)
expect(count_merge_requests).to eq(2)
end
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
expect(first_merge_request).to include('merge-test')
expect(count_merge_requests).to eq(4)
expect(count_merge_requests).to eq(2)
end
it 'sorts by milestone due date' do
visit_merge_requests(project, sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(4)
expect(count_merge_requests).to eq(2)
end
it 'sorts by merged at' do
it 'ignores sorting by merged at' do
visit_merge_requests(project, sort: sort_value_merged_date)
expect(first_merge_request).to include('markdown')
expect(count_merge_requests).to eq(4)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(2)
end
it 'sorts by closed at' do
visit_merge_requests(project, sort: sort_value_closed_date)
expect(first_merge_request).to include('feature')
expect(count_merge_requests).to eq(4)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(2)
end
it 'filters on one label and sorts by milestone due date' do
@ -141,6 +141,15 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(count_merge_requests).to eq(1)
end
context 'when viewing merged merge requests' do
it 'sorts by merged at' do
visit_merge_requests(project, state: 'merged', sort: sort_value_merged_date)
expect(first_merge_request).to include('markdown')
expect(count_merge_requests).to eq(2)
end
end
context 'while filtering on two labels' do
let(:label) { create(:label, project: project) }
let(:label2) { create(:label, project: project) }

View File

@ -1,160 +1,61 @@
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
const createComponent = (milestone = mockMilestone) => {
const Component = Vue.extend(IssueMilestone);
return shallowMount(Component, {
propsData: {
milestone,
},
});
};
describe('IssueMilestoneComponent', () => {
describe('IssueMilestone component', () => {
let wrapper;
let vm;
beforeEach(async () => {
const findTooltip = () => wrapper.findComponent(GlTooltip);
const createComponent = (milestone = mockMilestone) =>
shallowMount(IssueMilestone, { propsData: { milestone } });
beforeEach(() => {
wrapper = createComponent();
({ vm } = wrapper);
await nextTick();
});
afterEach(() => {
wrapper.destroy();
it('renders milestone icon', () => {
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
describe('computed', () => {
describe('isMilestoneStarted', () => {
it('should return `false` when milestoneStart prop is not defined', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '' },
});
await nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(false);
});
it('should return `true` when milestone start date is past current date', async () => {
await wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22' },
});
await nextTick();
expect(wrapper.vm.isMilestoneStarted).toBe(true);
});
});
describe('isMilestonePastDue', () => {
it('should return `false` when milestoneDue prop is not defined', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
await nextTick();
expect(wrapper.vm.isMilestonePastDue).toBe(false);
});
it('should return `true` when milestone due is past current date', () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '1990-07-22' },
});
expect(wrapper.vm.isMilestonePastDue).toBe(true);
});
});
describe('milestoneDatesAbsolute', () => {
it('returns string containing absolute milestone due date', () => {
expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
});
it('returns string containing absolute milestone start date when due date is not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: '' },
});
await nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
});
it('returns empty string when both milestone start and due dates are not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
await nextTick();
expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
});
});
describe('milestoneDatesHuman', () => {
it('returns string containing milestone due date when date is yet to be due', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
});
await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
});
it('returns string containing milestone start date when date has already started and due date is not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
});
await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
});
it('returns string containing milestone start date when date is yet to start and due date is not present', async () => {
wrapper.setProps({
milestone: {
...mockMilestone,
start_date: `${new Date().getFullYear() + 10}-01-01`,
due_date: '',
},
});
await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
});
it('returns empty string when milestone start and due dates are not present', async () => {
wrapper.setProps({
milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
await nextTick();
expect(wrapper.vm.milestoneDatesHuman).toBe('');
});
});
it('renders milestone title', () => {
expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title);
});
describe('template', () => {
it('renders component root element with class `issue-milestone-details`', () => {
expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
});
it('renders milestone icon', () => {
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
describe('tooltip', () => {
it('renders `Milestone`', () => {
expect(findTooltip().text()).toContain('Milestone');
});
it('renders milestone title', () => {
expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
expect(findTooltip().text()).toContain(mockMilestone.title);
});
it('renders milestone tooltip', () => {
expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
mockMilestone.title,
);
describe('humanized dates', () => {
it('renders `Expired` when there is a due date in the past', () => {
wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' });
expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)');
});
it('renders `remaining` when there is a due date in the future', () => {
wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' });
expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)');
});
it('renders `Started` when there is a start date in the past', () => {
wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' });
expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)');
});
it('renders `Starts` when there is a start date in the future', () => {
wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' });
expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)');
});
});
});
});

View File

@ -10,6 +10,7 @@ import {
} from '~/lib/utils/text_markdown';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/lib/utils/jquery_at_who';
import { ENTER_KEY } from '~/lib/utils/keys';
import axios from '~/lib/utils/axios_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@ -208,7 +209,7 @@ describe('init markdown', () => {
let enterEvent;
beforeEach(() => {
enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true });
enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, cancelable: true });
textArea.addEventListener('keydown', keypressNoteText);
textArea.addEventListener('compositionstart', compositionStartNoteText);
textArea.addEventListener('compositionend', compositionEndNoteText);
@ -492,6 +493,53 @@ describe('init markdown', () => {
});
});
describe('adding a hard break using Shift+Enter', () => {
let enterEvent;
beforeEach(() => {
enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true });
textArea.addEventListener('keydown', keypressNoteText);
textArea.addEventListener('compositionstart', compositionStartNoteText);
textArea.addEventListener('compositionend', compositionEndNoteText);
});
it.each`
selectionStart | selectionEnd | expected | expectedSelectionStart
${0} | ${0} | ${'\\\n0123456789'} | ${2}
${3} | ${3} | ${'012\\\n3456789'} | ${5}
${3} | ${6} | ${'012\\\n6789'} | ${5}
`(
'adds a hard break',
({ selectionStart, selectionEnd, expected, expectedSelectionStart }) => {
const text = '0123456789';
textArea.value = text;
textArea.setSelectionRange(selectionStart, selectionEnd);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
expect(textArea.selectionStart).toEqual(expectedSelectionStart);
expect(textArea.selectionEnd).toEqual(expectedSelectionStart);
},
);
it.each`
keyEvent
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: false })}
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, metaKey: true })}
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, altKey: true })}
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, ctrlKey: true })}
`('does not add when shift is pressed with other keys', ({ keyEvent }) => {
const text = '0123456789';
textArea.value = text;
textArea.setSelectionRange(0, 0);
textArea.dispatchEvent(keyEvent);
expect(textArea.value).toEqual(text);
});
});
describe('with selection', () => {
let text = 'initial selected value';
let selected = 'selected';

View File

@ -1,6 +1,7 @@
import { GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/flash';
@ -43,6 +44,7 @@ describe('UpdateUsername component', () => {
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
Vue.config.errorHandler = null;
});
const findElements = () => {
@ -58,6 +60,13 @@ describe('UpdateUsername component', () => {
};
};
const clickModalWithErrorResponse = () => {
Vue.config.errorHandler = jest.fn(); // silence thrown error
const { modal } = findElements();
modal.vm.$emit('primary');
return waitForPromises();
};
it('has a disabled button if the username was not changed', async () => {
const { openModalBtn } = findElements();
@ -98,14 +107,15 @@ describe('UpdateUsername component', () => {
axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
await wrapper.vm.onConfirm();
await nextTick();
const { modal } = findElements();
modal.vm.$emit('primary');
await waitForPromises();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
});
it('sets the username after a successful update', async () => {
const { input, openModalBtn } = findElements();
const { input, openModalBtn, modal } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input.attributes('disabled')).toBe('disabled');
@ -115,8 +125,8 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
await wrapper.vm.onConfirm();
await nextTick();
modal.vm.$emit('primary');
await waitForPromises();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(true);
@ -134,7 +144,8 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
await clickModalWithErrorResponse();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(false);
@ -145,7 +156,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'Invalid username',
@ -157,7 +168,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating your username, please try again.',

View File

@ -71,8 +71,10 @@ describe('Branch rule', () => {
});
it('renders a detail button with the correct href', () => {
const encodedBranchName = encodeURIComponent(branchRulePropsMock.name);
expect(findDetailsButton().attributes('href')).toBe(
`${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`,
`${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`,
);
});
});

View File

@ -74,7 +74,7 @@ export const branchRuleProvideMock = {
};
export const branchRulePropsMock = {
name: 'main',
name: 'branch-with-$speci@l-#-chars',
isDefault: true,
matchingBranchesCount: 1,
branchProtection: {

View File

@ -1,203 +1,114 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Participants from '~/sidebar/components/participants/participants.vue';
const PARTICIPANT = {
id: 1,
state: 'active',
username: 'marcene',
name: 'Allie Will',
web_url: 'foo.com',
avatar_url: 'gravatar.com/avatar/xxx',
};
const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
describe('Participants', () => {
describe('Participants component', () => {
let wrapper;
const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]');
const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
const participant = {
id: 1,
state: 'active',
username: 'marcene',
name: 'Allie Will',
web_url: 'foo.com',
avatar_url: 'gravatar.com/avatar/xxx',
};
const mountComponent = (propsData) =>
shallowMount(Participants, {
propsData,
});
const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }];
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findMoreParticipantsButton = () => wrapper.findComponent(GlButton);
const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon');
const findParticipantsAuthor = () => wrapper.findAll('.participants-author');
const mountComponent = (propsData) => shallowMount(Participants, { propsData });
describe('collapsed sidebar state', () => {
it('shows loading spinner when loading', () => {
wrapper = mountComponent({
loading: true,
});
wrapper = mountComponent({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not show loading spinner not loading', () => {
wrapper = mountComponent({
loading: false,
});
it('does not show loading spinner when not loading', () => {
wrapper = mountComponent({ loading: false });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(findLoadingIcon().exists()).toBe(false);
});
it('shows participant count when given', () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
});
wrapper = mountComponent({ participants });
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
it('shows full participant count when there are hidden participants', () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 1,
});
wrapper = mountComponent({ participants, numberOfLessParticipants: 1 });
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
});
describe('expanded sidebar state', () => {
it('shows loading spinner when loading', () => {
wrapper = mountComponent({
loading: true,
});
wrapper = mountComponent({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(true);
});
it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
const numberOfLessParticipants = 2;
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants,
});
wrapper = mountComponent({ participants, numberOfLessParticipants });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
await nextTick();
expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
});
it('when only showing all participants, each has an avatar', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
await findMoreParticipantsButton().vm.$emit('click');
await nextTick();
expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
expect(findParticipantsAuthor()).toHaveLength(participants.length);
});
it('does not have more participants link when they can all be shown', () => {
const numberOfLessParticipants = 100;
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants,
});
wrapper = mountComponent({ participants, numberOfLessParticipants });
expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
expect(getMoreParticipantsButton().exists()).toBe(false);
expect(participants.length).toBeLessThan(numberOfLessParticipants);
expect(findMoreParticipantsButton().exists()).toBe(false);
});
it('when too many participants, has more participants link to show more', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
it('when too many participants, has more participants link to show more', () => {
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
await nextTick();
expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
expect(findMoreParticipantsButton().text()).toBe('+ 1 more');
});
it('when too many participants and already showing them, has more participants link to show less', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
await findMoreParticipantsButton().vm.$emit('click');
await nextTick();
expect(getMoreParticipantsButton().text()).toBe('- show less');
expect(findMoreParticipantsButton().text()).toBe('- show less');
});
it('clicking more participants link emits event', () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
it('clicking on participants icon emits `toggleSidebar` event', () => {
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
findCollapsedIcon().trigger('click');
getMoreParticipantsButton().vm.$emit('click');
expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
});
it('clicking on participants icon emits `toggleSidebar` event', async () => {
wrapper = mountComponent({
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
const spy = jest.spyOn(wrapper.vm, '$emit');
wrapper.find('.sidebar-collapsed-icon').trigger('click');
await nextTick();
expect(spy).toHaveBeenCalledWith('toggleSidebar');
spy.mockRestore();
expect(wrapper.emitted('toggleSidebar')).toEqual([[]]);
});
});
describe('when not showing participants label', () => {
beforeEach(() => {
wrapper = mountComponent({
participants: PARTICIPANT_LIST,
showParticipantLabel: false,
});
wrapper = mountComponent({ participants, showParticipantLabel: false });
});
it('does not show sidebar collapsed icon', () => {
expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false);
expect(findCollapsedIcon().exists()).toBe(false);
});
it('does not show participants label title', () => {

View File

@ -0,0 +1,68 @@
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
describe('SidebarPortal', () => {
let targetWrapper;
const Target = {
components: { SidebarPortalTarget },
props: ['show'],
template: '<sidebar-portal-target v-if="show" />',
};
const Source = {
components: { SidebarPortal },
template: '<sidebar-portal><br data-testid="test"></sidebar-portal>',
};
const mountSource = () => {
mount(Source);
};
const mountTarget = ({ show = true } = {}) => {
targetWrapper = mount(Target, {
propsData: { show },
attachTo: document.body,
});
};
const findTestContent = () => targetWrapper.find('[data-testid="test"]');
it('renders content into the target', async () => {
mountTarget();
await nextTick();
mountSource();
await nextTick();
expect(findTestContent().exists()).toBe(true);
});
it('waits for target to be available before rendering', async () => {
mountSource();
await nextTick();
mountTarget();
await nextTick();
expect(findTestContent().exists()).toBe(true);
});
it('supports conditional rendering of target', async () => {
mountTarget({ show: false });
await nextTick();
mountSource();
await nextTick();
expect(findTestContent().exists()).toBe(false);
await targetWrapper.setProps({ show: true });
expect(findTestContent().exists()).toBe(true);
await targetWrapper.setProps({ show: false });
expect(findTestContent().exists()).toBe(false);
});
});

View File

@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
import { sidebarData } from '../mock_data';
describe('SuperSidebar component', () => {
@ -9,6 +10,7 @@ describe('SuperSidebar component', () => {
const findUserBar = () => wrapper.findComponent(UserBar);
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
@ -31,5 +33,9 @@ describe('SuperSidebar component', () => {
it('renders HelpCenter with sidebarData', () => {
expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
});
it('renders SidebarPortalTarget', () => {
expect(findSidebarPortalTarget().exists()).toBe(true);
});
});
});

View File

@ -9,8 +9,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
let(:user) { create(:user) }
let(:client_id) { '123' }
let(:enable_public_keys_storage_config) { false }
let(:enable_public_keys_storage_setting) { false }
let(:enable_public_keys_storage) { false }
before do
stub_application_setting(jira_connect_application_key: client_id)
@ -22,9 +21,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
before do
allow(view).to receive(:current_user).and_return(nil)
allow(Gitlab.config.gitlab).to receive(:url).and_return('http://test.host')
allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
.and_return(enable_public_keys_storage_config)
stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage_setting)
stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage)
end
it 'includes Jira Connect app attributes' do
@ -108,16 +105,8 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
expect(subject[:public_key_storage_enabled]).to eq(false)
end
context 'when public_key_storage is enabled via config' do
let(:enable_public_keys_storage_config) { true }
it 'assignes public_key_storage_enabled to true' do
expect(subject[:public_key_storage_enabled]).to eq(true)
end
end
context 'when public_key_storage is enabled via setting' do
let(:enable_public_keys_storage_setting) { true }
context 'when public_key_storage is enabled' do
let(:enable_public_keys_storage) { true }
it 'assignes public_key_storage_enabled to true' do
expect(subject[:public_key_storage_enabled]).to eq(true)

View File

@ -10,6 +10,60 @@ RSpec.describe SortingHelper do
allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: { label_name: option }))
end
describe '#issuable_sort_options' do
let(:viewing_issues) { false }
let(:viewing_merge_requests) { false }
let(:params) { {} }
subject(:options) { helper.issuable_sort_options(viewing_issues, viewing_merge_requests) }
before do
allow(helper).to receive(:params).and_return(params)
end
shared_examples 'with merged date option' do
it 'adds merged date option' do
expect(options).to include(
a_hash_including(
value: 'merged_at',
text: 'Merged date'
)
)
end
end
shared_examples 'without merged date option' do
it 'does not set merged date option' do
expect(options).not_to include(
a_hash_including(
value: 'merged_at',
text: 'Merged date'
)
)
end
end
it_behaves_like 'without merged date option'
context 'when viewing_merge_requests is true' do
let(:viewing_merge_requests) { true }
it_behaves_like 'without merged date option'
context 'when state param is all' do
let(:params) { { state: 'all' } }
it_behaves_like 'with merged date option'
end
context 'when state param is merged' do
let(:params) { { state: 'merged' } }
it_behaves_like 'with merged date option'
end
end
end
describe '#admin_users_sort_options' do
it 'returns correct link attributes in array' do
options = admin_users_sort_options(filter: 'filter', search_query: 'search')

View File

@ -16,19 +16,21 @@ RSpec.shared_examples 'validate schema data' do |tables_and_views|
end
end
RSpec.describe Gitlab::Database::GitlabSchema do
RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
shared_examples 'maps table name to table schema' do
using RSpec::Parameterized::TableSyntax
where(:name, :classification) do
'ci_builds' | :gitlab_ci
'my_schema.ci_builds' | :gitlab_ci
'information_schema.columns' | :gitlab_internal
'audit_events_part_5fc467ac26' | :gitlab_main
'_test_gitlab_main_table' | :gitlab_main
'_test_gitlab_ci_table' | :gitlab_ci
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_internal
'ci_builds' | :gitlab_ci
'my_schema.ci_builds' | :gitlab_ci
'my_schema.ci_runner_machine_builds_100' | :gitlab_ci
'my_schema._test_gitlab_main_table' | :gitlab_main
'information_schema.columns' | :gitlab_internal
'audit_events_part_5fc467ac26' | :gitlab_main
'_test_gitlab_main_table' | :gitlab_main
'_test_gitlab_ci_table' | :gitlab_ci
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_internal
end
with_them do

View File

@ -238,23 +238,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR)
end
# Postgres 11 does not support foreign keys to partitioned tables
if ApplicationRecord.database.version.to_f >= 12
context 'when the model is the target of a foreign key' do
before do
connection.execute(<<~SQL)
context 'when the model is the target of a foreign key' do
before do
connection.execute(<<~SQL)
create unique index idx_for_fk ON #{partitioned_table_name}(created_at);
create table _test_gitlab_main_referencing_table (
id bigserial primary key not null,
referencing_created_at timestamptz references #{partitioned_table_name}(created_at)
);
SQL
end
SQL
end
it 'does not detach partitions with a referenced foreign key' do
expect { subject }.not_to change { find_partitions(my_model.table_name).size }
end
it 'does not detach partitions with a referenced foreign key' do
expect { subject }.not_to change { find_partitions(my_model.table_name).size }
end
end
end

View File

@ -203,10 +203,8 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
end
context 'when supporting foreign keys to inherited tables in postgres 12' do
context 'when supporting foreign keys to inherited tables' do
before do
skip('not supported before postgres 12') if ApplicationRecord.database.version.to_f < 12
ApplicationRecord.connection.execute(<<~SQL)
create table #{schema_table_name('parent')} (
id bigserial primary key not null

View File

@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
is_expected.to have_request_urgency(:medium)
end
end

View File

@ -5,11 +5,10 @@ require 'spec_helper'
RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do
describe 'GET /-/jira_connect/public_keys/:uuid' do
let(:uuid) { non_existing_record_id }
let(:public_key_storage_enabled_config) { true }
let(:public_key_storage_enabled) { true }
before do
allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
.and_return(public_key_storage_enabled_config)
stub_application_setting(jira_connect_public_key_storage_enabled: public_key_storage_enabled)
end
it 'renders 404' do
@ -30,26 +29,14 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
expect(response.body).to eq(public_key.key)
end
context 'when public key storage config disabled' do
let(:public_key_storage_enabled_config) { false }
context 'when public key storage setting disabled' do
let(:public_key_storage_enabled) { false }
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when public key storage setting is enabled' do
before do
stub_application_setting(jira_connect_public_key_storage_enabled: true)
end
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
end

View File

@ -178,6 +178,8 @@ RSpec.configure do |config|
config.include RenderedHelpers
config.include RSpec::Benchmark::Matchers, type: :benchmark
config.include DetailedErrorHelpers
config.include RequestUrgencyMatcher, type: :controller
config.include RequestUrgencyMatcher, type: :request
config.include_context 'when rendered has no HTML escapes', type: :view

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
module RequestUrgencyMatcher
RSpec::Matchers.define :have_request_urgency do |request_urgency|
match do |_actual|
if controller_instance = request.env["action_controller.instance"]
controller_instance.urgency.name == request_urgency
elsif endpoint = request.env['api.endpoint']
urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
urgency.name == request_urgency
else
raise 'neither a controller nor a request spec'
end
end
failure_message do |_actual|
if controller_instance = request.env["action_controller.instance"]
"request urgency #{controller_instance.urgency.name} is set, \
but expected to be #{request_urgency}".squish
elsif endpoint = request.env['api.endpoint']
urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
"request urgency #{urgency.name} is set, \
but expected to be #{request_urgency}".squish
end
end
end
end