Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bc3187f6d9
commit
6cdb39a9ef
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 } }
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
|
@ -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`
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}."
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue