Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-04-05 21:08:46 +00:00
parent bc3187f6d9
commit 6cdb39a9ef
42 changed files with 339 additions and 273 deletions

View File

@ -150,6 +150,7 @@ export default {
v-model="searchText" v-model="searchText"
role="searchbox" role="searchbox"
class="gl-z-index-1" class="gl-z-index-1"
data-qa-selector="search_term_field"
autocomplete="off" autocomplete="off"
:placeholder="$options.i18n.searchGitlab" :placeholder="$options.i18n.searchGitlab"
:aria-activedescendant="currentFocusedId" :aria-activedescendant="currentFocusedId"

View File

@ -431,7 +431,7 @@ export default {
clearFlash() { clearFlash() {
if (this.flashContainer) { if (this.flashContainer) {
this.flashContainer.style.display = 'none'; this.flashContainer.close();
this.flashContainer = null; this.flashContainer = null;
} }
}, },

View File

@ -57,6 +57,7 @@ export default {
:value="sortDirection" :value="sortDirection"
:storage-key="storageKey" :storage-key="storageKey"
:persist="persistSortOrder" :persist="persistSortOrder"
as-string
@input="setDiscussionSortDirection({ direction: $event })" @input="setDiscussionSortDirection({ direction: $event })"
/> />
<gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile"> <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile">

View File

@ -99,7 +99,6 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="package_registry_list_sorting" storage-key="package_registry_list_sorting"
:value="sorting" :value="sorting"
as-json
@input="updateSorting" @input="updateSorting"
> >
<url-sync> <url-sync>

View File

@ -23,6 +23,7 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for
return new Vue({ return new Vue({
el, el,
name: 'SignupRestrictions',
provide: { provide: {
...parsedDataset, ...parsedDataset,
}, },

View File

@ -273,6 +273,7 @@ export default {
<local-storage-sync <local-storage-sync
:storage-key="$options.viewTypeKey" :storage-key="$options.viewTypeKey"
:value="currentViewType" :value="currentViewType"
as-string
@input="updateViewType" @input="updateViewType"
> >
<graph-view-selector <graph-view-selector

View File

@ -144,7 +144,6 @@ export default {
<local-storage-sync <local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects" v-model="autoDevopsEnabledAlertDismissedProjects"
:storage-key="$options.autoDevopsEnabledAlertStorageKey" :storage-key="$options.autoDevopsEnabledAlertStorageKey"
as-json
/> />
<user-callout-dismisser <user-callout-dismisser

View File

@ -113,13 +113,14 @@ export default {
'gl-display-grid gl-align-items-center': showVerticalList, 'gl-display-grid gl-align-items-center': showVerticalList,
'gl-mb-3': index !== users.length - 1 && showVerticalList, 'gl-mb-3': index !== users.length - 1 && showVerticalList,
}" }"
class="assignee-attention-grid" class="assignee-grid"
> >
<assignee-avatar-link <assignee-avatar-link
:user="user" :user="user"
:issuable-type="issuableType" :issuable-type="issuableType"
:tooltip-has-name="!showVerticalList" :tooltip-has-name="!showVerticalList"
class="gl-grid-column-2 gl-grid-row-1 gl-word-break-word" class="gl-word-break-word"
data-css-area="user"
> >
<div <div
v-if="showVerticalList" v-if="showVerticalList"
@ -134,7 +135,8 @@ export default {
v-if="showVerticalList" v-if="showVerticalList"
:user="user" :user="user"
type="assignee" type="assignee"
class="gl-grid-column-1 gl-grid-row-1 gl-mr-2" class="gl-mr-2"
data-css-area="attention"
@toggle-attention-requested="toggleAttentionRequested" @toggle-attention-requested="toggleAttentionRequested"
/> />
</div> </div>

View File

@ -94,15 +94,19 @@ export default {
<div <div
v-for="(user, index) in users" v-for="(user, index) in users"
:key="user.id" :key="user.id"
:class="{ 'gl-mb-3': index !== users.length - 1 }" :class="{
class="gl-display-grid gl-align-items-center reviewer-attention-grid" 'gl-mb-3': index !== users.length - 1,
'attention-requests': glFeatures.mrAttentionRequests,
}"
class="gl-display-grid gl-align-items-center reviewer-grid"
data-testid="reviewer" data-testid="reviewer"
> >
<reviewer-avatar-link <reviewer-avatar-link
:user="user" :user="user"
:root-path="rootPath" :root-path="rootPath"
:issuable-type="issuableType" :issuable-type="issuableType"
class="gl-grid-column-2 gl-grid-row-1 gl-word-break-word gl-mr-2" class="gl-word-break-word gl-mr-2"
data-css-area="user"
> >
<div class="gl-ml-3 gl-line-height-normal gl-display-grid"> <div class="gl-ml-3 gl-line-height-normal gl-display-grid">
<span>{{ user.name }}</span> <span>{{ user.name }}</span>
@ -113,7 +117,8 @@ export default {
v-if="glFeatures.mrAttentionRequests" v-if="glFeatures.mrAttentionRequests"
:user="user" :user="user"
type="reviewer" type="reviewer"
class="gl-grid-column-1 gl-grid-row-1 gl-mr-2" class="gl-mr-2"
data-css-area="attention"
@toggle-attention-requested="toggleAttentionRequested" @toggle-attention-requested="toggleAttentionRequested"
/> />
<gl-icon <gl-icon

View File

@ -112,7 +112,6 @@ export default {
v-model="mergeRequestMeta" v-model="mergeRequestMeta"
:storage-key="$options.storageKey" :storage-key="$options.storageKey"
:clear="clearStorage" :clear="clearStorage"
as-json
/> />
<edit-meta-controls <edit-meta-controls
ref="editMetaControls" ref="editMetaControls"

View File

@ -37,7 +37,7 @@ export default {
<template> <template>
<div v-show="showAlert"> <div v-show="showAlert">
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json /> <local-storage-sync v-model="isDismissed" :storage-key="storageKey" />
<gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert"> <gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
<slot></slot> <slot></slot>
</gl-alert> </gl-alert>

View File

@ -21,12 +21,17 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
methods: {
targetFn() {
return this.$refs.popoverTrigger?.$el;
},
},
}; };
</script> </script>
<template> <template>
<span> <span>
<gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" /> <gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
<gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options"> <gl-popover :target="targetFn" v-bind="options">
<template v-if="options.title" #title> <template v-if="options.title" #title>
<span v-safe-html="options.title"></span> <span v-safe-html="options.title"></span>
</template> </template>

View File

@ -1,6 +1,18 @@
<script> <script>
import { isEqual } from 'lodash'; import { isEqual, isString } from 'lodash';
/**
* This component will save and restore a value to and from localStorage.
* The value will be saved only when the value changes; the initial value won't be saved.
*
* By default, the value will be saved using JSON.stringify(), and retrieved back using JSON.parse().
*
* If you would like to save the raw string instead, you may set the 'asString' prop to true, though be aware that this is a
* legacy prop to maintain backwards compatibility.
*
* For new components saving data for the first time, it's recommended to not use 'asString' even if you're saving a string; it will still be
* saved and restored properly using JSON.stringify()/JSON.parse().
*/
export default { export default {
props: { props: {
storageKey: { storageKey: {
@ -12,7 +24,7 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
asJson: { asString: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
@ -30,6 +42,8 @@ export default {
}, },
watch: { watch: {
value(newVal) { value(newVal) {
if (!this.persist) return;
this.saveValue(this.serialize(newVal)); this.saveValue(this.serialize(newVal));
}, },
clear(newVal) { clear(newVal) {
@ -67,15 +81,22 @@ export default {
} }
}, },
saveValue(val) { saveValue(val) {
if (!this.persist) return;
localStorage.setItem(this.storageKey, val); localStorage.setItem(this.storageKey, val);
}, },
serialize(val) { serialize(val) {
return this.asJson ? JSON.stringify(val) : val; if (!isString(val) && this.asString) {
// eslint-disable-next-line no-console
console.warn(
`[gitlab] LocalStorageSync is saving`,
val,
`to the key "${this.storageKey}", but it is not a string and the 'asString' prop is true. This will save and restore the stringified value rather than the original value. If this is not intended, please remove or set the 'asString' prop to false.`,
);
}
return this.asString ? val : JSON.stringify(val);
}, },
deserialize(val) { deserialize(val) {
return this.asJson ? JSON.parse(val) : val; return this.asString ? val : JSON.parse(val);
}, },
}, },
render() { render() {

View File

@ -43,7 +43,7 @@ export default {
</script> </script>
<template> <template>
<local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected"> <local-storage-sync :storage-key="storageKey" :value="selected" as-string @input="setSelected">
<gl-dropdown :text="dropdownText" lazy> <gl-dropdown :text="dropdownText" lazy>
<gl-dropdown-item <gl-dropdown-item
v-for="option in parsedOptions" v-for="option in parsedOptions"

View File

@ -314,6 +314,7 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="gl-web-ide-button-selected" storage-key="gl-web-ide-button-selected"
:value="selection" :value="selection"
as-string
@input="select" @input="select"
/> />
<gl-modal <gl-modal

View File

@ -227,22 +227,28 @@
margin-right: -$gl-spacing-scale-2; margin-right: -$gl-spacing-scale-2;
} }
.reviewer-attention-grid, .assignee-grid {
.assignee-attention-grid { grid-template-areas: ' attention user';
grid-template-columns: min-content 1fr min-content; grid-template-columns: min-content 1fr;
} }
/* TODO: These are non-standardized classes, and should be moved into gitlab-ui .reviewer-grid {
Please see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1780 grid-template-areas: ' user approval rerequest';
*/ grid-template-columns: 1fr min-content min-content;
.gl-grid-column-1 {
grid-column: 1; &.attention-requests {
grid-template-areas: ' attention user approval';
grid-template-columns: min-content 1fr min-content;
}
} }
.gl-grid-row-1 { .assignee-grid,
grid-row: 1; .reviewer-grid {
} [data-css-area='attention'] {
grid-area: attention;
}
.gl-grid-column-2 { [data-css-area='user'] {
grid-column: 2; grid-area: user;
}
} }

View File

@ -22,6 +22,8 @@ class JiraConnect::ApplicationController < ApplicationController
def verify_qsh_claim! def verify_qsh_claim!
payload, _ = decode_auth_token! payload, _ = decode_auth_token!
return if request.format.json? && payload['qsh'] == 'context-qsh'
# Make sure `qsh` claim matches the current request # Make sure `qsh` claim matches the current request
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url) render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
rescue StandardError rescue StandardError

View File

@ -21,7 +21,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
end end
before_action :allow_rendering_in_iframe, only: :index before_action :allow_rendering_in_iframe, only: :index
before_action :verify_qsh_claim!, only: :index before_action :verify_qsh_claim!
before_action :authenticate_user!, only: :create before_action :authenticate_user!, only: :create
def index def index

View File

@ -16,8 +16,6 @@ class ContainerRepository < ApplicationRecord
ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + %w[pre_import_done default]).freeze ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + %w[pre_import_done default]).freeze
SKIPPABLE_MIGRATION_STATES = (ABORTABLE_MIGRATION_STATES + %w[import_aborted]).freeze SKIPPABLE_MIGRATION_STATES = (ABORTABLE_MIGRATION_STATES + %w[import_aborted]).freeze
IRRECONCILABLE_MIGRATIONS_STATUSES = %w[import_in_progress pre_import_in_progress pre_import_canceled import_canceled].freeze
MIGRATION_PHASE_1_STARTED_AT = Date.new(2021, 11, 4).freeze MIGRATION_PHASE_1_STARTED_AT = Date.new(2021, 11, 4).freeze
TooManyImportsError = Class.new(StandardError) TooManyImportsError = Class.new(StandardError)
@ -113,7 +111,7 @@ class ContainerRepository < ApplicationRecord
end end
event :start_pre_import do event :start_pre_import do
transition default: :pre_importing transition %i[default pre_importing importing import_aborted] => :pre_importing
end end
event :finish_pre_import do event :finish_pre_import do
@ -153,7 +151,10 @@ class ContainerRepository < ApplicationRecord
container_repository.migration_pre_import_done_at = nil container_repository.migration_pre_import_done_at = nil
end end
after_transition any => :pre_importing do |container_repository| after_transition any => :pre_importing do |container_repository, transition|
forced = transition.args.first.try(:[], :forced)
next if forced
container_repository.try_import do container_repository.try_import do
container_repository.migration_pre_import container_repository.migration_pre_import
end end
@ -168,7 +169,10 @@ class ContainerRepository < ApplicationRecord
container_repository.migration_import_done_at = nil container_repository.migration_import_done_at = nil
end end
after_transition any => :importing do |container_repository| after_transition any => :importing do |container_repository, transition|
forced = transition.args.first.try(:[], :forced)
next if forced
container_repository.try_import do container_repository.try_import do
container_repository.migration_import container_repository.migration_import
end end
@ -259,10 +263,10 @@ class ContainerRepository < ApplicationRecord
super super
end end
def start_pre_import def start_pre_import(*args)
return false unless ContainerRegistry::Migration.enabled? return false unless ContainerRegistry::Migration.enabled?
super super(*args)
end end
def retry_pre_import def retry_pre_import
@ -295,8 +299,18 @@ class ContainerRepository < ApplicationRecord
case status case status
when 'native' when 'native'
finish_import_as(:native_import) finish_import_as(:native_import)
when *IRRECONCILABLE_MIGRATIONS_STATUSES when 'pre_import_in_progress'
nil return if pre_importing?
start_pre_import(forced: true)
when 'import_in_progress'
return if importing?
start_import(forced: true)
when 'import_canceled', 'pre_import_canceled'
return if import_skipped?
skip_import(reason: :migration_canceled)
when 'import_complete' when 'import_complete'
finish_import finish_import
when 'import_failed' when 'import_failed'

View File

@ -26,7 +26,7 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment has_one :last_deployment, -> { Feature.enabled?(:env_last_deployment_by_finished_at) ? success.ordered : success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment' has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true

View File

@ -16,6 +16,6 @@
domain_denylist_raw: @application_setting.domain_denylist_raw, domain_denylist_raw: @application_setting.domain_denylist_raw,
email_restrictions_enabled: @application_setting[:email_restrictions_enabled].to_s, email_restrictions_enabled: @application_setting[:email_restrictions_enabled].to_s,
supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax', supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax',
email_restrictions: @application_setting.email_restrictions, email_restrictions: @application_setting.email_restrictions.to_s,
after_sign_up_text: @application_setting[:after_sign_up_text], after_sign_up_text: @application_setting[:after_sign_up_text].to_s,
pending_user_count: pending_user_count } } pending_user_count: pending_user_count } }

View File

@ -6,7 +6,10 @@
= form_tag search_path, method: :get do |_f| = form_tag search_path, method: :get do |_f|
.gl-search-box-by-type .gl-search-box-by-type
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon') = sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', autocomplete: 'off' } %input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'),
class: 'form-control gl-form-input gl-search-box-by-type-input',
autocomplete: 'off',
data: { qa_selector: 'search_box' } }
= hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group] = hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group]
= hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project] = hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project]

View File

@ -4,16 +4,12 @@ module BulkImports
class EntityWorker # rubocop:disable Scalability/IdempotentWorker class EntityWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker include ApplicationWorker
data_consistency :always
feature_category :importers
sidekiq_options retry: false, dead: false
worker_has_external_dependencies!
idempotent! idempotent!
deduplicate :until_executed, including_scheduled: true deduplicate :until_executing
data_consistency :always
feature_category :importers
sidekiq_options retry: false, dead: false
worker_has_external_dependencies!
def perform(entity_id, current_stage = nil) def perform(entity_id, current_stage = nil)
return if stage_running?(entity_id, current_stage) return if stage_running?(entity_id, current_stage)

View File

@ -20,8 +20,17 @@ module Namespaces
Namespaces::StatisticsRefresherService.new.execute(namespace) Namespaces::StatisticsRefresherService.new.execute(namespace)
namespace.aggregation_schedule.destroy namespace.aggregation_schedule.destroy
notify_storage_usage(namespace)
rescue ::Namespaces::StatisticsRefresherService::RefresherError, ActiveRecord::RecordNotFound => ex rescue ::Namespaces::StatisticsRefresherService::RefresherError, ActiveRecord::RecordNotFound => ex
Gitlab::ErrorTracking.track_exception(ex, namespace_id: namespace_id, namespace: namespace&.full_path) Gitlab::ErrorTracking.track_exception(ex, namespace_id: namespace_id, namespace: namespace&.full_path)
end end
private
def notify_storage_usage(namespace)
end
end end
end end
Namespaces::RootStatisticsWorker.prepend_mod_with('Namespaces::RootStatisticsWorker')

View File

@ -0,0 +1,8 @@
---
name: env_last_deployment_by_finished_at
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83558
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357299
milestone: '14.10'
type: development
group: group::release
default_enabled: false

View File

@ -1881,8 +1881,9 @@ Input type: `DastSiteProfileCreateInput`
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="mutationdastsiteprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationdastsiteprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastsiteprofilecreatedastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Site Profile object. |
| <a id="mutationdastsiteprofilecreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationdastsiteprofilecreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdastsiteprofilecreateid"></a>`id` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile. | | <a id="mutationdastsiteprofilecreateid"></a>`id` **{warning-solid}** | [`DastSiteProfileID`](#dastsiteprofileid) | **Deprecated:** use `dastSiteProfile.id` field. Deprecated in 14.10. |
### `Mutation.dastSiteProfileDelete` ### `Mutation.dastSiteProfileDelete`
@ -1927,8 +1928,9 @@ Input type: `DastSiteProfileUpdateInput`
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="mutationdastsiteprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationdastsiteprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastsiteprofileupdatedastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Site profile object. |
| <a id="mutationdastsiteprofileupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationdastsiteprofileupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdastsiteprofileupdateid"></a>`id` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile. | | <a id="mutationdastsiteprofileupdateid"></a>`id` **{warning-solid}** | [`DastSiteProfileID`](#dastsiteprofileid) | **Deprecated:** use `dastSiteProfile.id` field. Deprecated in 14.10. |
### `Mutation.dastSiteTokenCreate` ### `Mutation.dastSiteTokenCreate`

View File

@ -17,7 +17,11 @@ You can then connect to this session by using the [pry-shell](https://github.com
You can watch [this video](https://www.youtube.com/watch?v=Lzs_PL_BySo), for more information about You can watch [this video](https://www.youtube.com/watch?v=Lzs_PL_BySo), for more information about
how to use the `pry-shell`. how to use the `pry-shell`.
## `byebug` vs `binding.pry` WARNING:
`binding.pry` can occasionally experience autoloading issues and fail during name resolution.
If needed, `binding.irb` can be used instead with a more limited feature set.
## `byebug` vs `binding.pry` vs `binding.irb`
`byebug` has a very similar interface as `gdb`, but `byebug` does not `byebug` has a very similar interface as `gdb`, but `byebug` does not
use the powerful Pry REPL. use the powerful Pry REPL.
@ -41,6 +45,12 @@ this document, so for the full documentation head over to the [Pry wiki](https:/
Below are a few features definitely worth checking out, also run Below are a few features definitely worth checking out, also run
`help` in a pry session to see what else you can do. `help` in a pry session to see what else you can do.
## `binding.irb`
As of Ruby 2.7, IRB ships with a simple interactive debugger.
Check out [the docs](https://ruby-doc.org/stdlib-2.7.0/libdoc/irb/rdoc/Binding.html) for more.
### State navigation ### State navigation
With the [state navigation](https://github.com/pry/pry/wiki/State-navigation) With the [state navigation](https://github.com/pry/pry/wiki/State-navigation)

View File

@ -381,6 +381,12 @@ Learn more on overriding security jobs:
All the security scanning tools define their stage, so this error can occur with all of them. All the security scanning tools define their stage, so this error can occur with all of them.
## Self managed installation options
For self managed installations, you can choose to run most of the GitLab security scanners even when [not connected to the internet](offline_deployments/index.md).
Self managed installations can also run the security scanners on a GitLab Runner [running inside OpenShift](../../install/openshift_and_gitlab/index.md).
## Security report validation ## Security report validation
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321918) in GitLab 13.11. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321918) in GitLab 13.11.

View File

@ -33855,7 +33855,7 @@ msgstr ""
msgid "SecurityReports|Take survey" msgid "SecurityReports|Take survey"
msgstr "" msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}" msgid "SecurityReports|The Vulnerability Report shows results of successful scans on your project's default branch, manually added vulnerability records, and vulnerabilities found from scanning operational environments. %{linkStart}Learn more.%{linkEnd}"
msgstr "" msgstr ""
msgid "SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}." msgid "SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."

View File

@ -130,6 +130,10 @@ module QA
has_css?(".active", text: 'Standard') has_css?(".active", text: 'Standard')
end end
def has_arkose_labs_token?
has_css?('[name="arkose_labs_token"][value]', visible: false)
end
def switch_to_sign_in_tab def switch_to_sign_in_tab
click_element :sign_in_tab click_element :sign_in_tab
end end
@ -174,6 +178,17 @@ module QA
fill_element :login_field, user.username fill_element :login_field, user.username
fill_element :password_field, user.password fill_element :password_field, user.password
if Runtime::Env.running_on_dot_com?
# Arkose only appears in staging.gitlab.com, gitlab.com, etc...
# Wait until the ArkoseLabs challenge has initialized
Support::WaitForRequests.wait_for_requests
Support::Waiter.wait_until(max_duration: 5, reload_page: false, raise_on_failure: false) do
has_arkose_labs_token?
end
end
click_element :sign_in_button click_element :sign_in_button
Support::WaitForRequests.wait_for_requests Support::WaitForRequests.wait_for_requests

View File

@ -45,6 +45,14 @@ module QA
element :search_term_field element :search_term_field
end end
view 'app/views/layouts/_header_search.html.haml' do
element :search_box
end
view 'app/assets/javascripts/header_search/components/app.vue' do
element :search_term_field
end
def go_to_groups def go_to_groups
within_groups_menu do within_groups_menu do
click_element(:menu_item_link, title: 'Your groups') click_element(:menu_item_link, title: 'Your groups')
@ -146,6 +154,7 @@ module QA
end end
def search_for(term) def search_for(term)
click_element(:search_box)
fill_element :search_term_field, "#{term}\n" fill_element :search_term_field, "#{term}\n"
end end

View File

@ -75,6 +75,18 @@ RSpec.describe JiraConnect::SubscriptionsController do
expect(json_response).to include('login_path' => nil) expect(json_response).to include('login_path' => nil)
end end
end end
context 'with context qsh' do
# The JSON endpoint will be requested by frontend using a JWT that Atlassian provides via Javascript.
# This JWT will likely use a context-qsh because Atlassian don't know for which endpoint it will be used.
# Read more about context JWT here: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt-for-connect-apps/
let(:qsh) { 'context-qsh' }
specify do
expect(response).to have_gitlab_http_status(:ok)
end
end
end end
end end
end end
@ -102,7 +114,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
end end
context 'with valid JWT' do context 'with valid JWT' do
let(:claims) { { iss: installation.client_key, sub: 1234 } } let(:claims) { { iss: installation.client_key, sub: 1234, qsh: 'context-qsh' } }
let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) } let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } } let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } }
let(:jira_group_name) { 'site-admins' } let(:jira_group_name) { 'site-admins' }
@ -158,7 +170,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
.stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups") .stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups")
.to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' }) .to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' })
delete :destroy, params: { jwt: jwt, id: subscription.id } delete :destroy, params: { jwt: jwt, id: subscription.id, format: :json }
end end
context 'without JWT' do context 'without JWT' do
@ -170,7 +182,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
end end
context 'with valid JWT' do context 'with valid JWT' do
let(:claims) { { iss: installation.client_key, sub: 1234 } } let(:claims) { { iss: installation.client_key, sub: 1234, qsh: 'context-qsh' } }
let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) } let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
it 'deletes the subscription' do it 'deletes the subscription' do

View File

@ -38,8 +38,8 @@ describe('Sort Discussion component', () => {
createComponent(); createComponent();
}); });
it('has local storage sync', () => { it('has local storage sync with the correct props', () => {
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('calls setDiscussionSortDirection when update is emitted', () => { it('calls setDiscussionSortDirection when update is emitted', () => {

View File

@ -73,7 +73,6 @@ describe('Package Search', () => {
mountComponent(); mountComponent();
expect(findLocalStorageSync().props()).toMatchObject({ expect(findLocalStorageSync().props()).toMatchObject({
asJson: true,
storageKey: 'package_registry_list_sorting', storageKey: 'package_registry_list_sorting',
value: { value: {
orderBy: LIST_KEY_CREATED_AT, orderBy: LIST_KEY_CREATED_AT,

View File

@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils'; import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import * as sentryUtils from '~/pipelines/utils'; import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data'; import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => {
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]'); wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector); const getViewSelector = () => wrapper.find(GraphViewSelector);
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert); const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({ const createComponent = ({
apolloProvider, apolloProvider,
@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => {
localStorage.clear(); localStorage.clear();
}); });
it('sets the asString prop on the LocalStorageSync component', () => {
expect(getLocalStorageSync().props('asString')).toBe(true);
});
it('reads the view type from localStorage when available', () => { it('reads the view type from localStorage when available', () => {
const viewSelectorNeedsSegment = wrapper const viewSelectorNeedsSegment = wrapper
.find(GlButtonGroup) .find(GlButtonGroup)

View File

@ -1,31 +1,29 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const STORAGE_KEY = 'key';
describe('Local Storage Sync', () => { describe('Local Storage Sync', () => {
let wrapper; let wrapper;
const createComponent = ({ props = {}, slots = {} } = {}) => { const createComponent = ({ value, asString = false, slots = {} } = {}) => {
wrapper = shallowMount(LocalStorageSync, { wrapper = shallowMount(LocalStorageSync, {
propsData: props, propsData: { storageKey: STORAGE_KEY, value, asString },
slots, slots,
}); });
}; };
const setStorageValue = (value) => localStorage.setItem(STORAGE_KEY, value);
const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
afterEach(() => { afterEach(() => {
if (wrapper) { wrapper.destroy();
wrapper.destroy();
}
wrapper = null;
localStorage.clear(); localStorage.clear();
}); });
it('is a renderless component', () => { it('is a renderless component', () => {
const html = '<div class="test-slot"></div>'; const html = '<div class="test-slot"></div>';
createComponent({ createComponent({
props: {
storageKey: 'key',
},
slots: { slots: {
default: html, default: html,
}, },
@ -35,233 +33,136 @@ describe('Local Storage Sync', () => {
}); });
describe('localStorage empty', () => { describe('localStorage empty', () => {
const storageKey = 'issue_list_order';
it('does not emit input event', () => { it('does not emit input event', () => {
createComponent({ createComponent({ value: 'ascending' });
props: {
storageKey,
value: 'ascending',
},
});
expect(wrapper.emitted('input')).toBeFalsy(); expect(wrapper.emitted('input')).toBeUndefined();
}); });
it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })( it('does not save initial value if it did not change', () => {
'saves updated value to localStorage', createComponent({ value: 'ascending' });
async (newValue) => {
createComponent({
props: {
storageKey,
value: 'initial',
},
});
wrapper.setProps({ value: newValue }); expect(getStorageValue()).toBeNull();
await nextTick();
expect(localStorage.getItem(storageKey)).toBe(String(newValue));
},
);
it('does not save default value', () => {
const value = 'ascending';
createComponent({
props: {
storageKey,
value,
},
});
expect(localStorage.getItem(storageKey)).toBe(null);
}); });
}); });
describe('localStorage has saved value', () => { describe('localStorage has saved value', () => {
const storageKey = 'issue_list_order_by';
const savedValue = 'last_updated'; const savedValue = 'last_updated';
beforeEach(() => { beforeEach(() => {
localStorage.setItem(storageKey, savedValue); setStorageValue(savedValue);
createComponent({ asString: true });
}); });
it('emits input event with saved value', () => { it('emits input event with saved value', () => {
createComponent({
props: {
storageKey,
value: 'ascending',
},
});
expect(wrapper.emitted('input')[0][0]).toBe(savedValue); expect(wrapper.emitted('input')[0][0]).toBe(savedValue);
}); });
it('does not overwrite localStorage with prop value', () => { it('does not overwrite localStorage with initial prop value', () => {
createComponent({ expect(getStorageValue()).toBe(savedValue);
props: {
storageKey,
value: 'created',
},
});
expect(localStorage.getItem(storageKey)).toBe(savedValue);
}); });
it('updating the value updates localStorage', async () => { it('updating the value updates localStorage', async () => {
createComponent({
props: {
storageKey,
value: 'created',
},
});
const newValue = 'last_updated'; const newValue = 'last_updated';
wrapper.setProps({ await wrapper.setProps({ value: newValue });
value: newValue,
});
await nextTick(); expect(getStorageValue()).toBe(newValue);
expect(localStorage.getItem(storageKey)).toBe(newValue);
});
it('persists the value by default', async () => {
const persistedValue = 'persisted';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({ value: persistedValue });
await nextTick();
expect(localStorage.getItem(storageKey)).toBe(persistedValue);
});
it('does not save a value if persist is set to false', async () => {
const notPersistedValue = 'notPersisted';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({ persist: false, value: notPersistedValue });
await nextTick();
expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue);
}); });
}); });
describe('with "asJson" prop set to "true"', () => { describe('persist prop', () => {
const storageKey = 'testStorageKey'; it('persists the value by default', async () => {
const persistedValue = 'persisted';
createComponent({ asString: true });
// Sanity check to make sure we start with nothing saved.
expect(getStorageValue()).toBeNull();
describe.each` await wrapper.setProps({ value: persistedValue });
value | serializedValue
${null} | ${'null'}
${''} | ${'""'}
${true} | ${'true'}
${false} | ${'false'}
${42} | ${'42'}
${'42'} | ${'"42"'}
${'{ foo: '} | ${'"{ foo: "'}
${['test']} | ${'["test"]'}
${{ foo: 'bar' }} | ${'{"foo":"bar"}'}
`('given $value', ({ value, serializedValue }) => {
describe('is a new value', () => {
beforeEach(async () => {
createComponent({
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
wrapper.setProps({ value }); expect(getStorageValue()).toBe(persistedValue);
await nextTick();
});
it('serializes the value correctly to localStorage', () => {
expect(localStorage.getItem(storageKey)).toBe(serializedValue);
});
});
describe('is already stored', () => {
beforeEach(() => {
localStorage.setItem(storageKey, serializedValue);
createComponent({
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
});
it('emits an input event with the deserialized value', () => {
expect(wrapper.emitted('input')).toEqual([[value]]);
});
});
}); });
describe('with bad JSON in storage', () => { it('does not save a value if persist is set to false', async () => {
const badJSON = '{ badJSON'; const value = 'saved';
const notPersistedValue = 'notPersisted';
createComponent({ asString: true });
// Save some value so we can test that it's not overwritten.
await wrapper.setProps({ value });
beforeEach(() => { expect(getStorageValue()).toBe(value);
jest.spyOn(console, 'warn').mockImplementation();
localStorage.setItem(storageKey, badJSON);
createComponent({ await wrapper.setProps({ persist: false, value: notPersistedValue });
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
});
it('should console warn', () => { expect(getStorageValue()).toBe(value);
// eslint-disable-next-line no-console });
expect(console.warn).toHaveBeenCalledWith( });
`[gitlab] Failed to deserialize value from localStorage (key=${storageKey})`,
badJSON,
);
});
it('should not emit an input event', () => { describe('saving and restoring', () => {
expect(wrapper.emitted('input')).toBeUndefined(); it.each`
}); value | asString
${'foo'} | ${true}
${'foo'} | ${false}
${'{ a: 1 }'} | ${true}
${'{ a: 1 }'} | ${false}
${3} | ${false}
${['foo', 'bar']} | ${false}
${{ foo: 'bar' }} | ${false}
${null} | ${false}
${' '} | ${false}
${true} | ${false}
${false} | ${false}
${42} | ${false}
${'42'} | ${false}
${'{ foo: '} | ${false}
`('saves and restores the same value', async ({ value, asString }) => {
// Create an initial component to save the value.
createComponent({ asString });
await wrapper.setProps({ value });
wrapper.destroy();
// Create a second component to restore the value. Restore is only done once, when the
// component is first mounted.
createComponent({ asString });
expect(wrapper.emitted('input')[0][0]).toEqual(value);
});
it('shows a warning when trying to save a non-string value when asString prop is true', async () => {
const spy = jest.spyOn(console, 'warn').mockImplementation();
createComponent({ asString: true });
await wrapper.setProps({ value: [] });
expect(spy).toHaveBeenCalled();
});
});
describe('with bad JSON in storage', () => {
const badJSON = '{ badJSON';
let spy;
beforeEach(() => {
spy = jest.spyOn(console, 'warn').mockImplementation();
setStorageValue(badJSON);
createComponent();
});
it('should console warn', () => {
expect(spy).toHaveBeenCalled();
});
it('should not emit an input event', () => {
expect(wrapper.emitted('input')).toBeUndefined();
}); });
}); });
it('clears localStorage when clear property is true', async () => { it('clears localStorage when clear property is true', async () => {
const storageKey = 'key';
const value = 'initial'; const value = 'initial';
createComponent({ asString: true });
await wrapper.setProps({ value });
createComponent({ expect(getStorageValue()).toBe(value);
props: {
storageKey,
},
});
wrapper.setProps({
value,
});
await nextTick(); await wrapper.setProps({ clear: true });
expect(localStorage.getItem(storageKey)).toBe(value); expect(getStorageValue()).toBeNull();
wrapper.setProps({
clear: true,
});
await nextTick();
expect(localStorage.getItem(storageKey)).toBe(null);
}); });
}); });

View File

@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => {
}); });
describe('local storage sync', () => { describe('local storage sync', () => {
it('uses the local storage sync component', () => { it('uses the local storage sync component with the correct props', () => {
createComponent(); createComponent();
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('passes the right props', () => { it('passes the right props', () => {

View File

@ -261,7 +261,10 @@ describe('Web IDE link component', () => {
}); });
it('should update local storage when selection changes', async () => { it('should update local storage when selection changes', async () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key); expect(findLocalStorageSync().props()).toMatchObject({
asString: true,
value: ACTION_WEB_IDE.key,
});
findActionsButton().vm.$emit('select', ACTION_GITPOD.key); findActionsButton().vm.$emit('select', ACTION_GITPOD.key);

View File

@ -224,7 +224,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
end end
end end
it_behaves_like 'transitioning from allowed states', %w[default] it_behaves_like 'transitioning from allowed states', %w[default pre_importing importing import_aborted]
it_behaves_like 'transitioning to pre_importing' it_behaves_like 'transitioning to pre_importing'
end end

View File

@ -698,10 +698,29 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'when there is a deployment record with success status' do context 'when there is a deployment record with success status' do
let!(:deployment) { create(:deployment, :success, environment: environment) } let!(:deployment) { create(:deployment, :success, environment: environment) }
let!(:old_deployment) { create(:deployment, :success, environment: environment, finished_at: 2.days.ago) }
it 'returns the latest successful deployment' do it 'returns the latest successful deployment' do
is_expected.to eq(deployment) is_expected.to eq(deployment)
end end
context 'env_last_deployment_by_finished_at feature flag' do
it 'when enabled it returns the deployment with the latest finished_at' do
stub_feature_flags(env_last_deployment_by_finished_at: true)
expect(old_deployment.finished_at < deployment.finished_at).to be_truthy
is_expected.to eq(deployment)
end
it 'when disabled it returns the deployment with the highest id' do
stub_feature_flags(env_last_deployment_by_finished_at: false)
expect(old_deployment.finished_at < deployment.finished_at).to be_truthy
is_expected.to eq(old_deployment)
end
end
end end
end end
end end

View File

@ -118,11 +118,14 @@ RSpec.shared_examples 'not hitting graphql network errors with the container reg
end end
RSpec.shared_examples 'reconciling migration_state' do RSpec.shared_examples 'reconciling migration_state' do
shared_examples 'no action' do shared_examples 'enforcing states coherence to' do |expected_migration_state|
it 'does nothing' do it 'leaves the repository in the expected migration_state' do
expect { subject }.not_to change { repository.reload.migration_state } expect(repository.gitlab_api_client).not_to receive(:pre_import_repository)
expect(repository.gitlab_api_client).not_to receive(:import_repository)
expect(subject).to eq(nil) subject
expect(repository.reload.migration_state).to eq(expected_migration_state)
end end
end end
@ -155,7 +158,7 @@ RSpec.shared_examples 'reconciling migration_state' do
context 'import_in_progress response' do context 'import_in_progress response' do
let(:status) { 'import_in_progress' } let(:status) { 'import_in_progress' }
it_behaves_like 'no action' it_behaves_like 'enforcing states coherence to', 'importing'
end end
context 'import_complete response' do context 'import_complete response' do
@ -175,7 +178,7 @@ RSpec.shared_examples 'reconciling migration_state' do
context 'pre_import_in_progress response' do context 'pre_import_in_progress response' do
let(:status) { 'pre_import_in_progress' } let(:status) { 'pre_import_in_progress' }
it_behaves_like 'no action' it_behaves_like 'enforcing states coherence to', 'pre_importing'
end end
context 'pre_import_complete response' do context 'pre_import_complete response' do
@ -194,4 +197,12 @@ RSpec.shared_examples 'reconciling migration_state' do
it_behaves_like 'retrying the pre_import' it_behaves_like 'retrying the pre_import'
end end
%w[pre_import_canceled import_canceled].each do |canceled_status|
context "#{canceled_status} response" do
let(:status) { canceled_status }
it_behaves_like 'enforcing states coherence to', 'import_skipped'
end
end
end end

View File

@ -10,7 +10,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
context 'with a namespace' do context 'with a namespace' do
it 'executes refresher service' do it 'executes refresher service' do
expect_any_instance_of(Namespaces::StatisticsRefresherService) expect_any_instance_of(Namespaces::StatisticsRefresherService)
.to receive(:execute) .to receive(:execute).and_call_original
worker.perform(group.id) worker.perform(group.id)
end end