Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0a5ea888dc
commit
52dbfea964
53 changed files with 511 additions and 179 deletions
|
@ -13,15 +13,10 @@
|
||||||
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/322903
|
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/322903
|
||||||
Graphql/Descriptions:
|
Graphql/Descriptions:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/graphql/types/container_expiration_policy_cadence_enum.rb'
|
|
||||||
- 'app/graphql/types/container_expiration_policy_keep_enum.rb'
|
|
||||||
- 'app/graphql/types/container_expiration_policy_older_than_enum.rb'
|
|
||||||
- 'app/graphql/types/packages/package_type_enum.rb'
|
|
||||||
- 'app/graphql/types/snippets/blob_action_enum.rb'
|
- 'app/graphql/types/snippets/blob_action_enum.rb'
|
||||||
- 'app/graphql/types/snippets/type_enum.rb'
|
- 'app/graphql/types/snippets/type_enum.rb'
|
||||||
- 'app/graphql/types/snippets/visibility_scopes_enum.rb'
|
- 'app/graphql/types/snippets/visibility_scopes_enum.rb'
|
||||||
- 'ee/app/graphql/ee/types/list_limit_metric_enum.rb'
|
- 'ee/app/graphql/ee/types/list_limit_metric_enum.rb'
|
||||||
- 'ee/app/graphql/types/alert_management/payload_alert_field_name_enum.rb'
|
|
||||||
- 'ee/app/graphql/types/epic_state_enum.rb'
|
- 'ee/app/graphql/types/epic_state_enum.rb'
|
||||||
- 'ee/app/graphql/types/health_status_enum.rb'
|
- 'ee/app/graphql/types/health_status_enum.rb'
|
||||||
- 'ee/app/graphql/types/iteration_state_enum.rb'
|
- 'ee/app/graphql/types/iteration_state_enum.rb'
|
||||||
|
|
|
@ -232,7 +232,7 @@ export function insertMarkdownText({
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
} else if (tag.indexOf(textPlaceholder) > -1) {
|
} else if (tag.indexOf(textPlaceholder) > -1) {
|
||||||
textToInsert = tag.replace(textPlaceholder, selected.replace(/\\n/g, '\n'));
|
textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
|
||||||
} else {
|
} else {
|
||||||
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
|
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,13 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
sections: {
|
||||||
|
required: true,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
maxValue: Object.keys(ACTION_LABELS).length,
|
maxValue: Object.keys(ACTION_LABELS).length,
|
||||||
sections: Object.keys(ACTION_SECTIONS),
|
actionSections: Object.keys(ACTION_SECTIONS),
|
||||||
computed: {
|
computed: {
|
||||||
progressValue() {
|
progressValue() {
|
||||||
return Object.values(this.actions).filter((a) => a.completed).length;
|
return Object.values(this.actions).filter((a) => a.completed).length;
|
||||||
|
@ -38,6 +42,9 @@ export default {
|
||||||
);
|
);
|
||||||
return actions;
|
return actions;
|
||||||
},
|
},
|
||||||
|
svgFor(section) {
|
||||||
|
return this.sections[section].svg;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -59,8 +66,12 @@ export default {
|
||||||
<gl-progress-bar :value="progressValue" :max="$options.maxValue" />
|
<gl-progress-bar :value="progressValue" :max="$options.maxValue" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-cols-1 row-cols-md-3 gl-mt-5">
|
<div class="row row-cols-1 row-cols-md-3 gl-mt-5">
|
||||||
<div v-for="section in $options.sections" :key="section" class="col gl-mb-6">
|
<div v-for="section in $options.actionSections" :key="section" class="col gl-mb-6">
|
||||||
<learn-gitlab-section-card :section="section" :actions="actionsFor(section)" />
|
<learn-gitlab-section-card
|
||||||
|
:section="section"
|
||||||
|
:svg="svgFor(section)"
|
||||||
|
:actions="actionsFor(section)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlCard } from '@gitlab/ui';
|
import { GlCard } from '@gitlab/ui';
|
||||||
import { imagePath } from '~/lib/utils/common_utils';
|
|
||||||
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
|
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
|
||||||
|
|
||||||
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
|
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
|
||||||
|
@ -16,6 +15,10 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
svg: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -28,17 +31,12 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
svg(section) {
|
|
||||||
return imagePath(`learn_gitlab/section_${section}.svg`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<gl-card class="gl-pt-0 learn-gitlab-section-card">
|
<gl-card class="gl-pt-0 learn-gitlab-section-card">
|
||||||
<div class="learn-gitlab-section-card-header">
|
<div class="learn-gitlab-section-card-header">
|
||||||
<img :src="svg(section)" />
|
<img :src="svg" />
|
||||||
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
|
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
|
||||||
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
|
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ function initLearnGitlab() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
|
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
|
||||||
|
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
|
||||||
|
|
||||||
const { learnGitlabA } = gon.experiments;
|
const { learnGitlabA } = gon.experiments;
|
||||||
|
|
||||||
|
@ -20,7 +21,9 @@ function initLearnGitlab() {
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
|
return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, {
|
||||||
|
props: { actions, sections },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,14 +36,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayPipelineActions() {
|
|
||||||
return (
|
|
||||||
this.pipeline.flags.retryable ||
|
|
||||||
this.pipeline.flags.cancelable ||
|
|
||||||
this.pipeline.details.manual_actions.length ||
|
|
||||||
this.pipeline.details.has_downloadable_artifacts
|
|
||||||
);
|
|
||||||
},
|
|
||||||
actions() {
|
actions() {
|
||||||
if (!this.pipeline || !this.pipeline.details) {
|
if (!this.pipeline || !this.pipeline.details) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -54,9 +46,6 @@ export default {
|
||||||
isCancelling() {
|
isCancelling() {
|
||||||
return this.cancelingPipeline === this.pipeline.id;
|
return this.cancelingPipeline === this.pipeline.id;
|
||||||
},
|
},
|
||||||
showArtifacts() {
|
|
||||||
return this.pipeline.details.has_downloadable_artifacts;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
pipeline() {
|
pipeline() {
|
||||||
|
@ -79,7 +68,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="displayPipelineActions" class="gl-text-right">
|
<div class="gl-text-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
|
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
|
||||||
|
|
||||||
|
@ -113,7 +102,7 @@ export default {
|
||||||
@click="handleCancelClick"
|
@click="handleCancelClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<pipeline-multi-actions v-if="showArtifacts" :pipeline-id="pipeline.id" />
|
<pipeline-multi-actions :pipeline-id="pipeline.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Types
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
::ContainerExpirationPolicy.cadence_options.each do |option, description|
|
::ContainerExpirationPolicy.cadence_options.each do |option, description|
|
||||||
value OPTIONS_MAPPING[option], description, value: option.to_s
|
value OPTIONS_MAPPING[option], description: description, value: option.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ module Types
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
::ContainerExpirationPolicy.keep_n_options.each do |option, description|
|
::ContainerExpirationPolicy.keep_n_options.each do |option, description|
|
||||||
value OPTIONS_MAPPING[option], description, value: option
|
value OPTIONS_MAPPING[option], description: description, value: option
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Types
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
::ContainerExpirationPolicy.older_than_options.each do |option, description|
|
::ContainerExpirationPolicy.older_than_options.each do |option, description|
|
||||||
value OPTIONS_MAPPING[option], description, value: option.to_s
|
value OPTIONS_MAPPING[option], description: description, value: option.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Types
|
||||||
|
|
||||||
::Packages::Package.package_types.keys.each do |package_type|
|
::Packages::Package.package_types.keys.each do |package_type|
|
||||||
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
|
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
|
||||||
value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s
|
value package_type.to_s.upcase, description: "Packages from the #{type_name} package manager", value: package_type.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,6 +36,20 @@ module LearnGitlabHelper
|
||||||
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
|
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def onboarding_sections_data
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
svg: image_path("learn_gitlab/section_workspace.svg")
|
||||||
|
},
|
||||||
|
plan: {
|
||||||
|
svg: image_path("learn_gitlab/section_plan.svg")
|
||||||
|
},
|
||||||
|
deploy: {
|
||||||
|
svg: image_path("learn_gitlab/section_deploy.svg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def action_urls
|
def action_urls
|
||||||
|
|
|
@ -1076,14 +1076,6 @@ module Ci
|
||||||
complete? && builds.latest.with_exposed_artifacts.exists?
|
complete? && builds.latest.with_exposed_artifacts.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_downloadable_artifacts?
|
|
||||||
if downloadable_artifacts.loaded?
|
|
||||||
downloadable_artifacts.any?
|
|
||||||
else
|
|
||||||
downloadable_artifacts.exists?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def branch_updated?
|
def branch_updated?
|
||||||
strong_memoize(:branch_updated) do
|
strong_memoize(:branch_updated) do
|
||||||
push_details.branch_updated?
|
push_details.branch_updated?
|
||||||
|
|
|
@ -8,7 +8,6 @@ class PipelineDetailsEntity < Ci::PipelineEntity
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :details do
|
expose :details do
|
||||||
expose :has_downloadable_artifacts?, as: :has_downloadable_artifacts
|
|
||||||
expose :artifacts, unless: proc { options[:disable_artifacts] } do |pipeline, options|
|
expose :artifacts, unless: proc { options[:disable_artifacts] } do |pipeline, options|
|
||||||
rel = pipeline.downloadable_artifacts
|
rel = pipeline.downloadable_artifacts
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,18 @@
|
||||||
class IssueRebalancingService
|
class IssueRebalancingService
|
||||||
MAX_ISSUE_COUNT = 10_000
|
MAX_ISSUE_COUNT = 10_000
|
||||||
BATCH_SIZE = 100
|
BATCH_SIZE = 100
|
||||||
|
SMALLEST_BATCH_SIZE = 5
|
||||||
|
RETRIES_LIMIT = 3
|
||||||
TooManyIssues = Class.new(StandardError)
|
TooManyIssues = Class.new(StandardError)
|
||||||
|
|
||||||
|
TIMING_CONFIGURATION = [
|
||||||
|
[0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms
|
||||||
|
[0.5.seconds, 0.05.seconds],
|
||||||
|
[1.second, 0.5.seconds],
|
||||||
|
[1.second, 0.5.seconds],
|
||||||
|
[5.seconds, 1.second]
|
||||||
|
].freeze
|
||||||
|
|
||||||
def initialize(issue)
|
def initialize(issue)
|
||||||
@issue = issue
|
@issue = issue
|
||||||
@base = Issue.relative_positioning_query_base(issue)
|
@base = Issue.relative_positioning_query_base(issue)
|
||||||
|
@ -23,14 +33,23 @@ class IssueRebalancingService
|
||||||
assign_positions(start, indexed_ids)
|
assign_positions(start, indexed_ids)
|
||||||
.sort_by(&:first)
|
.sort_by(&:first)
|
||||||
.each_slice(BATCH_SIZE) do |pairs_with_position|
|
.each_slice(BATCH_SIZE) do |pairs_with_position|
|
||||||
update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
|
if Feature.enabled?(:issue_rebalancing_with_retry)
|
||||||
|
update_positions_with_retry(pairs_with_position, 'rebalance issue positions in batches ordered by id')
|
||||||
|
else
|
||||||
|
update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Issue.transaction do
|
Issue.transaction do
|
||||||
indexed_ids.each_slice(BATCH_SIZE) do |pairs|
|
indexed_ids.each_slice(BATCH_SIZE) do |pairs|
|
||||||
pairs_with_position = assign_positions(start, pairs)
|
pairs_with_position = assign_positions(start, pairs)
|
||||||
update_positions(pairs_with_position, 'rebalance issue positions')
|
|
||||||
|
if Feature.enabled?(:issue_rebalancing_with_retry)
|
||||||
|
update_positions_with_retry(pairs_with_position, 'rebalance issue positions')
|
||||||
|
else
|
||||||
|
update_positions(pairs_with_position, 'rebalance issue positions')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -52,12 +71,37 @@ class IssueRebalancingService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_positions_with_retry(pairs_with_position, query_name)
|
||||||
|
retries = 0
|
||||||
|
batch_size = pairs_with_position.size
|
||||||
|
|
||||||
|
until pairs_with_position.empty?
|
||||||
|
begin
|
||||||
|
update_positions(pairs_with_position.first(batch_size), query_name)
|
||||||
|
pairs_with_position = pairs_with_position.drop(batch_size)
|
||||||
|
retries = 0
|
||||||
|
rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => ex
|
||||||
|
raise ex if batch_size < SMALLEST_BATCH_SIZE
|
||||||
|
|
||||||
|
if (retries += 1) == RETRIES_LIMIT
|
||||||
|
# shrink the batch size in half when RETRIES limit is reached and update still fails perhaps because batch size is still too big
|
||||||
|
batch_size = (batch_size / 2).to_i
|
||||||
|
retries = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_positions(pairs_with_position, query_name)
|
def update_positions(pairs_with_position, query_name)
|
||||||
values = pairs_with_position.map do |id, index|
|
values = pairs_with_position.map do |id, index|
|
||||||
"(#{id}, #{index})"
|
"(#{id}, #{index})"
|
||||||
end.join(', ')
|
end.join(', ')
|
||||||
|
|
||||||
run_update_query(values, query_name)
|
Gitlab::Database::WithLockRetries.new(timing_configuration: TIMING_CONFIGURATION, klass: self.class).run do
|
||||||
|
run_update_query(values, query_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_update_query(values, query_name)
|
def run_update_query(values, query_name)
|
||||||
|
|
|
@ -1,28 +1,3 @@
|
||||||
- if project_nav_tab? :snippets
|
|
||||||
= nav_link(controller: :snippets) do
|
|
||||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('snippet')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Snippets')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_snippets_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Snippets')
|
|
||||||
|
|
||||||
= nav_link(controller: :project_members) do
|
|
||||||
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('users')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Members')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_project_members_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Members')
|
|
||||||
|
|
||||||
- if project_nav_tab? :settings
|
- if project_nav_tab? :settings
|
||||||
= nav_link(path: sidebar_settings_paths) do
|
= nav_link(path: sidebar_settings_paths) do
|
||||||
= link_to edit_project_path(@project) do
|
= link_to edit_project_path(@project) do
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
- page_title _("Learn GitLab")
|
- page_title _("Learn GitLab")
|
||||||
- add_page_specific_style 'page_bundles/learn_gitlab'
|
- add_page_specific_style 'page_bundles/learn_gitlab'
|
||||||
|
|
||||||
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } }
|
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } }
|
||||||
|
|
6
changelogs/unreleased/fix-smtp-pool-errors.yml
Normal file
6
changelogs/unreleased/fix-smtp-pool-errors.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Fix SMTP errors when delivering service desk thank you emails with SMTP pool
|
||||||
|
enabled
|
||||||
|
merge_request: 60843
|
||||||
|
author:
|
||||||
|
type: fixed
|
5
changelogs/unreleased/mo-display-artifacts-dropdown.yml
Normal file
5
changelogs/unreleased/mo-display-artifacts-dropdown.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Stop exposing has_downloadable_artifacts in pipelines.json
|
||||||
|
merge_request: 60950
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fixed dollar signs in suggestions getting replaced incorrectly
|
||||||
|
merge_request: 61041
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: Increase load time of project select dropdowns
|
title: Decrease load time of project select dropdowns
|
||||||
merge_request: 61117
|
merge_request: 61117
|
||||||
author:
|
author:
|
||||||
type: performance
|
type: performance
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: issue_rebalancing_with_retry
|
||||||
|
introduced_by_url:
|
||||||
|
rollout_issue_url:
|
||||||
|
milestone: '13.11'
|
||||||
|
type: development
|
||||||
|
group: group::project management
|
||||||
|
default_enabled: false
|
|
@ -27,7 +27,7 @@ have a high degree of confidence in being able to perform them accurately.
|
||||||
|
|
||||||
## Not all data is automatically replicated
|
## Not all data is automatically replicated
|
||||||
|
|
||||||
If you are using any GitLab features that Geo [doesn't support](../index.md#limitations),
|
If you are using any GitLab features that Geo [doesn't support](../replication/datatypes.md#limitations-on-replicationverification),
|
||||||
you must make separate provisions to ensure that the **secondary** node has an
|
you must make separate provisions to ensure that the **secondary** node has an
|
||||||
up-to-date copy of any data associated with that feature. This may extend the
|
up-to-date copy of any data associated with that feature. This may extend the
|
||||||
required scheduled maintenance period significantly.
|
required scheduled maintenance period significantly.
|
||||||
|
@ -40,8 +40,7 @@ final transfer inside the maintenance window) will then transfer only the
|
||||||
|
|
||||||
Repository-centric strategies for using `rsync` effectively can be found in the
|
Repository-centric strategies for using `rsync` effectively can be found in the
|
||||||
[moving repositories](../../operations/moving_repositories.md) documentation; these strategies can
|
[moving repositories](../../operations/moving_repositories.md) documentation; these strategies can
|
||||||
be adapted for use with any other file-based data, such as GitLab Pages (to
|
be adapted for use with any other file-based data, such as [GitLab Pages](../../pages/index.md#change-storage-path).
|
||||||
be found in `/var/opt/gitlab/gitlab-rails/shared/pages` if using Omnibus).
|
|
||||||
|
|
||||||
## Preflight checks
|
## Preflight checks
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,25 @@ When something is marked to be updated in the tracking database instance, asynch
|
||||||
|
|
||||||
This new architecture allows GitLab to be resilient to connectivity issues between the nodes. It doesn't matter how long the **secondary** node is disconnected from the **primary** node as it will be able to replay all the events in the correct order and become synchronized with the **primary** node again.
|
This new architecture allows GitLab to be resilient to connectivity issues between the nodes. It doesn't matter how long the **secondary** node is disconnected from the **primary** node as it will be able to replay all the events in the correct order and become synchronized with the **primary** node again.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
|
||||||
|
|
||||||
|
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
|
||||||
|
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/-/issues/208465).
|
||||||
|
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2978) for details.
|
||||||
|
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
|
||||||
|
- [Selective synchronization](replication/configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
|
||||||
|
- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
|
||||||
|
- GitLab Runners cannot register with a **secondary** node. Support for this is [planned for the future](https://gitlab.com/gitlab-org/gitlab/-/issues/3294).
|
||||||
|
- Geo **secondary** nodes can not be configured to [use high-availability configurations of PostgreSQL](https://gitlab.com/groups/gitlab-org/-/epics/2536).
|
||||||
|
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accomodate compliance / export control use cases.
|
||||||
|
|
||||||
|
### Limitations on replication/verification
|
||||||
|
|
||||||
|
There is a complete list of all GitLab [data types](replication/datatypes.md) and [existing support for replication and verification](replication/datatypes.md#limitations-on-replicationverification).
|
||||||
|
|
||||||
## Setup instructions
|
## Setup instructions
|
||||||
|
|
||||||
For setup instructions, see [Setting up Geo](setup/index.md).
|
For setup instructions, see [Setting up Geo](setup/index.md).
|
||||||
|
@ -275,25 +294,6 @@ For more information on removing a Geo node, see [Removing **secondary** Geo nod
|
||||||
|
|
||||||
To find out how to disable Geo, see [Disabling Geo](replication/disable_geo.md).
|
To find out how to disable Geo, see [Disabling Geo](replication/disable_geo.md).
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
WARNING:
|
|
||||||
This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
|
|
||||||
|
|
||||||
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
|
|
||||||
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/-/issues/208465).
|
|
||||||
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2978) for details.
|
|
||||||
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
|
|
||||||
- [Selective synchronization](replication/configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
|
|
||||||
- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
|
|
||||||
- GitLab Runners cannot register with a **secondary** node. Support for this is [planned for the future](https://gitlab.com/gitlab-org/gitlab/-/issues/3294).
|
|
||||||
- Geo **secondary** nodes can not be configured to [use high-availability configurations of PostgreSQL](https://gitlab.com/groups/gitlab-org/-/epics/2536).
|
|
||||||
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accomodate compliance / export control use cases.
|
|
||||||
|
|
||||||
### Limitations on replication/verification
|
|
||||||
|
|
||||||
There is a complete list of all GitLab [data types](replication/datatypes.md) and [existing support for replication and verification](replication/datatypes.md#limitations-on-replicationverification).
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
For answers to common questions, see the [Geo FAQ](replication/faq.md).
|
For answers to common questions, see the [Geo FAQ](replication/faq.md).
|
||||||
|
|
|
@ -853,6 +853,12 @@ To resolve this issue:
|
||||||
the **primary** node using IPv4 in the `/etc/hosts` file. Alternatively, you should
|
the **primary** node using IPv4 in the `/etc/hosts` file. Alternatively, you should
|
||||||
[enable IPv6 on the **primary** node](https://docs.gitlab.com/omnibus/settings/nginx.html#setting-the-nginx-listen-address-or-addresses).
|
[enable IPv6 on the **primary** node](https://docs.gitlab.com/omnibus/settings/nginx.html#setting-the-nginx-listen-address-or-addresses).
|
||||||
|
|
||||||
|
### GitLab Pages return 404 errors after promoting
|
||||||
|
|
||||||
|
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
|
||||||
|
Find advice to resolve those errors in the
|
||||||
|
[Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node).
|
||||||
|
|
||||||
## Fixing client errors
|
## Fixing client errors
|
||||||
|
|
||||||
### Authorization errors from LFS HTTP(s) client requests
|
### Authorization errors from LFS HTTP(s) client requests
|
||||||
|
|
|
@ -1167,6 +1167,17 @@ date > /var/opt/gitlab/gitlab-rails/shared/pages/.update
|
||||||
|
|
||||||
If you've customized the Pages storage path, adjust the command above to use your custom path.
|
If you've customized the Pages storage path, adjust the command above to use your custom path.
|
||||||
|
|
||||||
|
### 404 error after promoting a Geo secondary to a primary node
|
||||||
|
|
||||||
|
These are due to the Pages files not being among the
|
||||||
|
[supported data types](../geo/replication/datatypes.md#limitations-on-replicationverification).
|
||||||
|
|
||||||
|
It is possible to copy the subfolders and files in the [Pages path](#change-storage-path)
|
||||||
|
to the new primary node to resolve this.
|
||||||
|
For example, you can adapt the `rsync` strategy from the
|
||||||
|
[moving repositories documenation](../operations/moving_repositories.md).
|
||||||
|
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
|
||||||
|
|
||||||
### Failed to connect to the internal GitLab API
|
### Failed to connect to the internal GitLab API
|
||||||
|
|
||||||
If you have enabled [API-based configuration](#gitlab-api-based-configuration) and see the following error:
|
If you have enabled [API-based configuration](#gitlab-api-based-configuration) and see the following error:
|
||||||
|
|
|
@ -24,7 +24,9 @@ Fields that are deprecated are marked with **{warning-solid}**.
|
||||||
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
|
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
|
||||||
in [Removed Items](../removed_items.md).
|
in [Removed Items](../removed_items.md).
|
||||||
|
|
||||||
<!-- vale gitlab.Spelling = NO -->
|
<!-- vale off -->
|
||||||
|
<!-- Docs linting disabled after this line. -->
|
||||||
|
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
|
||||||
|
|
||||||
## `Query` type
|
## `Query` type
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
||||||
--->
|
--->
|
||||||
|
|
||||||
<!-- vale gitlab.Spelling = NO -->
|
|
||||||
|
|
||||||
# Metrics Dictionary
|
# Metrics Dictionary
|
||||||
|
|
||||||
This file is autogenerated, please do not edit directly.
|
This file is autogenerated, please do not edit directly.
|
||||||
|
@ -30,6 +28,10 @@ The Metrics Dictionary is based on the following metrics definition YAML files:
|
||||||
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
||||||
was released.
|
was released.
|
||||||
|
|
||||||
|
<!-- vale off -->
|
||||||
|
<!-- Docs linting disabled after this line. -->
|
||||||
|
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
|
||||||
|
|
||||||
## Metrics Definitions
|
## Metrics Definitions
|
||||||
|
|
||||||
### `active_user_count`
|
### `active_user_count`
|
||||||
|
|
|
@ -302,6 +302,9 @@ A 404 can also be related to incorrect permissions. If [Pages Access Control](pa
|
||||||
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
|
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
|
||||||
To fix this, verify that the user is a member of the project.
|
To fix this, verify that the user is a member of the project.
|
||||||
|
|
||||||
|
For Geo instances, 404 errors on Pages occur after promoting a secondary to a primary.
|
||||||
|
Find more details in the [Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node)
|
||||||
|
|
||||||
### Cannot play media content on Safari
|
### Cannot play media content on Safari
|
||||||
|
|
||||||
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)
|
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)
|
||||||
|
|
|
@ -38,7 +38,7 @@ module Gitlab
|
||||||
|
|
||||||
if from_address
|
if from_address
|
||||||
add_email_participant
|
add_email_participant
|
||||||
send_thank_you_email!
|
send_thank_you_email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -92,8 +92,8 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_thank_you_email!
|
def send_thank_you_email
|
||||||
Notify.service_desk_thank_you_email(@issue.id).deliver_later!
|
Notify.service_desk_thank_you_email(@issue.id).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_including_template
|
def message_including_template
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
|
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
|
||||||
in [Removed Items](../removed_items.md).
|
in [Removed Items](../removed_items.md).
|
||||||
|
|
||||||
<!-- vale gitlab.Spelling = NO -->
|
<!-- vale off -->
|
||||||
|
<!-- Docs linting disabled after this line. -->
|
||||||
|
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
|
||||||
\
|
\
|
||||||
|
|
||||||
:plain
|
:plain
|
||||||
|
|
|
@ -18,8 +18,6 @@ module Gitlab
|
||||||
|
|
||||||
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
||||||
--->
|
--->
|
||||||
|
|
||||||
<!-- vale gitlab.Spelling = NO -->
|
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
||||||
was released.
|
was released.
|
||||||
|
|
||||||
|
<!-- vale off -->
|
||||||
|
<!-- Docs linting disabled after this line. -->
|
||||||
|
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
|
||||||
|
|
||||||
## Metrics Definitions
|
## Metrics Definitions
|
||||||
|
|
||||||
\
|
\
|
||||||
|
|
41
lib/sidebars/projects/menus/members_menu.rb
Normal file
41
lib/sidebars/projects/menus/members_menu.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sidebars
|
||||||
|
module Projects
|
||||||
|
module Menus
|
||||||
|
class MembersMenu < ::Sidebars::Menu
|
||||||
|
override :link
|
||||||
|
def link
|
||||||
|
project_project_members_path(context.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
override :extra_container_html_options
|
||||||
|
def extra_container_html_options
|
||||||
|
{
|
||||||
|
id: 'js-onboarding-members-link'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
override :title
|
||||||
|
def title
|
||||||
|
_('Members')
|
||||||
|
end
|
||||||
|
|
||||||
|
override :sprite_icon
|
||||||
|
def sprite_icon
|
||||||
|
'users'
|
||||||
|
end
|
||||||
|
|
||||||
|
override :render?
|
||||||
|
def render?
|
||||||
|
can?(context.current_user, :read_project_member, context.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
override :active_routes
|
||||||
|
def active_routes
|
||||||
|
{ controller: :project_members }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
41
lib/sidebars/projects/menus/snippets_menu.rb
Normal file
41
lib/sidebars/projects/menus/snippets_menu.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sidebars
|
||||||
|
module Projects
|
||||||
|
module Menus
|
||||||
|
class SnippetsMenu < ::Sidebars::Menu
|
||||||
|
override :link
|
||||||
|
def link
|
||||||
|
project_snippets_path(context.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
override :extra_container_html_options
|
||||||
|
def extra_container_html_options
|
||||||
|
{
|
||||||
|
class: 'shortcuts-snippets'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
override :title
|
||||||
|
def title
|
||||||
|
_('Snippets')
|
||||||
|
end
|
||||||
|
|
||||||
|
override :sprite_icon
|
||||||
|
def sprite_icon
|
||||||
|
'snippet'
|
||||||
|
end
|
||||||
|
|
||||||
|
override :render?
|
||||||
|
def render?
|
||||||
|
can?(context.current_user, :read_snippet, context.project)
|
||||||
|
end
|
||||||
|
|
||||||
|
override :active_routes
|
||||||
|
def active_routes
|
||||||
|
{ controller: :snippets }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,6 +22,8 @@ module Sidebars
|
||||||
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context))
|
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context))
|
||||||
add_menu(confluence_or_wiki_menu)
|
add_menu(confluence_or_wiki_menu)
|
||||||
add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
|
add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
|
||||||
|
add_menu(Sidebars::Projects::Menus::SnippetsMenu.new(context))
|
||||||
|
add_menu(Sidebars::Projects::Menus::MembersMenu.new(context))
|
||||||
end
|
end
|
||||||
|
|
||||||
override :render_raw_menus_partial
|
override :render_raw_menus_partial
|
||||||
|
|
|
@ -13,11 +13,6 @@ module QA
|
||||||
include SubMenus::Settings
|
include SubMenus::Settings
|
||||||
include SubMenus::Packages
|
include SubMenus::Packages
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
|
||||||
element :snippets_link
|
|
||||||
element :members_link
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_merge_requests
|
def click_merge_requests
|
||||||
within_sidebar do
|
within_sidebar do
|
||||||
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
|
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
|
||||||
|
@ -38,13 +33,13 @@ module QA
|
||||||
|
|
||||||
def click_snippets
|
def click_snippets
|
||||||
within_sidebar do
|
within_sidebar do
|
||||||
click_element(:snippets_link)
|
click_element(:sidebar_menu_link, menu_item: 'Snippets')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_members
|
def click_members
|
||||||
within_sidebar do
|
within_sidebar do
|
||||||
click_element(:members_link)
|
click_element(:sidebar_menu_link, menu_item: 'Members')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -88,12 +88,18 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
|
||||||
def build_filter_text(pipeline, initial_text)
|
def build_filter_text(pipeline, initial_text)
|
||||||
filter_source = {}
|
filter_source = {}
|
||||||
input_text = initial_text
|
input_text = initial_text
|
||||||
|
result = nil
|
||||||
|
|
||||||
pipeline.filters.each do |filter_klass|
|
pipeline.filters.each do |filter_klass|
|
||||||
filter_source[filter_klass] = input_text
|
# store inputs for current filter_klass
|
||||||
|
filter_source[filter_klass] = { input_text: input_text, input_result: result }
|
||||||
|
|
||||||
output = filter_klass.call(input_text, context)
|
filter = filter_klass.new(input_text, context, result)
|
||||||
|
output = filter.call
|
||||||
|
|
||||||
|
# save these for the next filter_klass
|
||||||
input_text = output
|
input_text = output
|
||||||
|
result = filter.result
|
||||||
end
|
end
|
||||||
|
|
||||||
filter_source
|
filter_source
|
||||||
|
@ -111,7 +117,12 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
|
||||||
pipeline.filters.each do |filter_klass|
|
pipeline.filters.each do |filter_klass|
|
||||||
label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20)
|
label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20)
|
||||||
|
|
||||||
x.report(label) { filter_klass.call(filter_source[filter_klass], context) }
|
x.report(label) do
|
||||||
|
filter = filter_klass.new(filter_source[filter_klass][:input_text],
|
||||||
|
context,
|
||||||
|
filter_source[filter_klass][:input_result])
|
||||||
|
filter.call
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
x.compare!
|
x.compare!
|
||||||
|
|
|
@ -51,6 +51,25 @@ describe('init markdown', () => {
|
||||||
expect(textArea.value).toEqual(`${initialValue}- `);
|
expect(textArea.value).toEqual(`${initialValue}- `);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('inserts dollar signs correctly', () => {
|
||||||
|
const initialValue = '';
|
||||||
|
|
||||||
|
textArea.value = initialValue;
|
||||||
|
textArea.selectionStart = 0;
|
||||||
|
textArea.selectionEnd = 0;
|
||||||
|
|
||||||
|
insertMarkdownText({
|
||||||
|
textArea,
|
||||||
|
text: textArea.value,
|
||||||
|
tag: '```suggestion:-0+0\n{text}\n```',
|
||||||
|
blockTag: true,
|
||||||
|
selected: '# Does not parse the `$` currently.',
|
||||||
|
wrap: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(textArea.value).toContain('# Does not parse the `$` currently.');
|
||||||
|
});
|
||||||
|
|
||||||
it('inserts the tag on a new line if the current one is not empty', () => {
|
it('inserts the tag on a new line if the current one is not empty', () => {
|
||||||
const initialValue = 'some text';
|
const initialValue = 'some text';
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
|
||||||
class="learn-gitlab-section-card-header"
|
class="learn-gitlab-section-card-header"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/learn_gitlab/section_workspace.svg"
|
src="workspace.svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
|
@ -246,7 +246,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
|
||||||
class="learn-gitlab-section-card-header"
|
class="learn-gitlab-section-card-header"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/learn_gitlab/section_plan.svg"
|
src="plan.svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
|
@ -324,7 +324,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
|
||||||
class="learn-gitlab-section-card-header"
|
class="learn-gitlab-section-card-header"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/learn_gitlab/section_deploy.svg"
|
src="deploy.svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
|
|
|
@ -11,7 +11,7 @@ exports[`Learn GitLab Section Card renders correctly 1`] = `
|
||||||
class="learn-gitlab-section-card-header"
|
class="learn-gitlab-section-card-header"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/learn_gitlab/section_workspace.svg"
|
src="workspace.svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { GlProgressBar } from '@gitlab/ui';
|
import { GlProgressBar } from '@gitlab/ui';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
|
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
|
||||||
import { testActions } from './mock_data';
|
import { testActions, testSections } from './mock_data';
|
||||||
|
|
||||||
describe('Learn GitLab Design A', () => {
|
describe('Learn GitLab Design A', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
wrapper = mount(LearnGitlabA, { propsData: { actions: testActions } });
|
wrapper = mount(LearnGitlabA, { propsData: { actions: testActions, sections: testSections } });
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/lea
|
||||||
import { testActions } from './mock_data';
|
import { testActions } from './mock_data';
|
||||||
|
|
||||||
const defaultSection = 'workspace';
|
const defaultSection = 'workspace';
|
||||||
|
const testImage = 'workspace.svg';
|
||||||
|
|
||||||
describe('Learn GitLab Section Card', () => {
|
describe('Learn GitLab Section Card', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -14,7 +15,7 @@ describe('Learn GitLab Section Card', () => {
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
wrapper = shallowMount(LearnGitlabSectionCard, {
|
wrapper = shallowMount(LearnGitlabSectionCard, {
|
||||||
propsData: { section: defaultSection, actions: testActions },
|
propsData: { section: defaultSection, actions: testActions, svg: testImage },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -45,3 +45,15 @@ export const testActions = {
|
||||||
svg: 'http://example.com/images/illustration.svg',
|
svg: 'http://example.com/images/illustration.svg',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const testSections = {
|
||||||
|
workspace: {
|
||||||
|
svg: 'workspace.svg',
|
||||||
|
},
|
||||||
|
deploy: {
|
||||||
|
svg: 'deploy.svg',
|
||||||
|
},
|
||||||
|
plan: {
|
||||||
|
svg: 'plan.svg',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -96,6 +96,17 @@ RSpec.describe LearnGitlabHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.onboarding_sections_data' do
|
||||||
|
subject(:sections) { helper.onboarding_sections_data }
|
||||||
|
|
||||||
|
it 'has the right keys' do
|
||||||
|
expect(sections.keys).to contain_exactly(:deploy, :plan, :workspace)
|
||||||
|
end
|
||||||
|
it 'has the svg' do
|
||||||
|
expect(sections.values.map { |section| section.keys }).to eq([[:svg]] * 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.learn_gitlab_experiment_tracking_category' do
|
describe '.learn_gitlab_experiment_tracking_category' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
|
|
@ -90,11 +90,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
|
||||||
context 'when quick actions are present' do
|
context 'when quick actions are present' do
|
||||||
let(:label) { create(:label, project: project, title: 'label1') }
|
let(:label) { create(:label, project: project, title: 'label1') }
|
||||||
let(:milestone) { create(:milestone, project: project) }
|
let(:milestone) { create(:milestone, project: project) }
|
||||||
let!(:user) { create(:user, username: 'user1') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'applies quick action commands present on templates' do
|
it 'applies quick action commands present on templates' do
|
||||||
file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")
|
file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")
|
||||||
|
|
27
spec/lib/sidebars/projects/menus/members_menu_spec.rb
Normal file
27
spec/lib/sidebars/projects/menus/members_menu_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Sidebars::Projects::Menus::MembersMenu do
|
||||||
|
let(:project) { build(:project) }
|
||||||
|
let(:user) { project.owner }
|
||||||
|
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||||
|
|
||||||
|
subject { described_class.new(context) }
|
||||||
|
|
||||||
|
describe '#render?' do
|
||||||
|
context 'when user cannot access members' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.render?).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can access members' do
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.render?).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
Normal file
27
spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Sidebars::Projects::Menus::SnippetsMenu do
|
||||||
|
let(:project) { build(:project) }
|
||||||
|
let(:user) { project.owner }
|
||||||
|
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||||
|
|
||||||
|
subject { described_class.new(context) }
|
||||||
|
|
||||||
|
describe '#render?' do
|
||||||
|
context 'when user cannot access snippets' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.render?).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can access snippets' do
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.render?).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4491,18 +4491,4 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
.not_to exceed_query_limit(control_count)
|
.not_to exceed_query_limit(control_count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#has_downloadable_artifacts?' do
|
|
||||||
it 'returns false when when pipeline does not have downloadable artifacts' do
|
|
||||||
pipeline = create(:ci_pipeline, :success)
|
|
||||||
|
|
||||||
expect(pipeline.has_downloadable_artifacts?). to eq(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false when when pipeline does not have downloadable artifacts' do
|
|
||||||
pipeline = create(:ci_pipeline, :with_codequality_reports)
|
|
||||||
|
|
||||||
expect(pipeline.has_downloadable_artifacts?). to eq(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe PipelineDetailsEntity do
|
||||||
expect(subject[:details])
|
expect(subject[:details])
|
||||||
.to include :duration, :finished_at
|
.to include :duration, :finished_at
|
||||||
expect(subject[:details])
|
expect(subject[:details])
|
||||||
.to include :stages, :artifacts, :has_downloadable_artifacts, :manual_actions, :scheduled_actions
|
.to include :stages, :artifacts, :manual_actions, :scheduled_actions
|
||||||
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
|
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -186,35 +186,5 @@ RSpec.describe PipelineDetailsEntity do
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'public artifacts'
|
it_behaves_like 'public artifacts'
|
||||||
|
|
||||||
context 'when pipeline has downloadable artifacts' do
|
|
||||||
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: disable_artifacts).as_json }
|
|
||||||
|
|
||||||
let_it_be(:pipeline) { create(:ci_pipeline, :with_codequality_reports) }
|
|
||||||
|
|
||||||
context 'when disable_artifacts is true' do
|
|
||||||
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: true).as_json }
|
|
||||||
|
|
||||||
it 'excludes artifacts data' do
|
|
||||||
expect(entity[:details]).not_to include(:artifacts)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true for has_downloadable_artifacts' do
|
|
||||||
expect(entity[:details][:has_downloadable_artifacts]).to eq(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when disable_artifacts is false' do
|
|
||||||
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: false).as_json }
|
|
||||||
|
|
||||||
it 'includes artifacts data' do
|
|
||||||
expect(entity[:details]).to include(:artifacts)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true for has_downloadable_artifacts' do
|
|
||||||
expect(entity[:details][:has_downloadable_artifacts]).to eq(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,31 +3,35 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe IssueRebalancingService do
|
RSpec.describe IssueRebalancingService do
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be(:project, reload: true) { create(:project) }
|
||||||
let_it_be(:user) { project.creator }
|
let_it_be(:user) { project.creator }
|
||||||
let_it_be(:start) { RelativePositioning::START_POSITION }
|
let_it_be(:start) { RelativePositioning::START_POSITION }
|
||||||
let_it_be(:max_pos) { RelativePositioning::MAX_POSITION }
|
let_it_be(:max_pos) { RelativePositioning::MAX_POSITION }
|
||||||
let_it_be(:min_pos) { RelativePositioning::MIN_POSITION }
|
let_it_be(:min_pos) { RelativePositioning::MIN_POSITION }
|
||||||
let_it_be(:clump_size) { 300 }
|
let_it_be(:clump_size) { 300 }
|
||||||
|
|
||||||
let_it_be(:unclumped) do
|
let_it_be(:unclumped, reload: true) do
|
||||||
(0..clump_size).to_a.map do |i|
|
(1..clump_size).to_a.map do |i|
|
||||||
create(:issue, project: project, author: user, relative_position: start + (1024 * i))
|
create(:issue, project: project, author: user, relative_position: start + (1024 * i))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
let_it_be(:end_clump) do
|
let_it_be(:end_clump, reload: true) do
|
||||||
(0..clump_size).to_a.map do |i|
|
(1..clump_size).to_a.map do |i|
|
||||||
create(:issue, project: project, author: user, relative_position: max_pos - i)
|
create(:issue, project: project, author: user, relative_position: max_pos - i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
let_it_be(:start_clump) do
|
let_it_be(:start_clump, reload: true) do
|
||||||
(0..clump_size).to_a.map do |i|
|
(1..clump_size).to_a.map do |i|
|
||||||
create(:issue, project: project, author: user, relative_position: min_pos + i)
|
create(:issue, project: project, author: user, relative_position: min_pos + i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_rebalancing_with_retry: false)
|
||||||
|
end
|
||||||
|
|
||||||
def issues_in_position_order
|
def issues_in_position_order
|
||||||
project.reload.issues.reorder(relative_position: :asc).to_a
|
project.reload.issues.reorder(relative_position: :asc).to_a
|
||||||
end
|
end
|
||||||
|
@ -101,19 +105,70 @@ RSpec.describe IssueRebalancingService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'rebalancing is retried on statement timeout exceptions' do
|
||||||
|
subject { described_class.new(project.issues.first) }
|
||||||
|
|
||||||
|
it 'retries update statement' do
|
||||||
|
call_count = 0
|
||||||
|
allow(subject).to receive(:run_update_query) do
|
||||||
|
call_count += 1
|
||||||
|
if call_count < 13
|
||||||
|
raise(ActiveRecord::QueryCanceled)
|
||||||
|
else
|
||||||
|
call_count = 0 if call_count == 13 + 16 # 16 = 17 sub-batches - 1 call that succeeded as part of 5th batch
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# call math:
|
||||||
|
# batches start at 100 and are split in half after every 3 retries if ActiveRecord::StatementTimeout exception is raised.
|
||||||
|
# We raise ActiveRecord::StatementTimeout exception for 13 calls:
|
||||||
|
# 1. 100 => 3 calls
|
||||||
|
# 2. 100/2=50 => 3 calls + 3 above = 6 calls, raise ActiveRecord::StatementTimeout
|
||||||
|
# 3. 50/2=25 => 3 calls + 6 above = 9 calls, raise ActiveRecord::StatementTimeout
|
||||||
|
# 4. 25/2=12 => 3 calls + 9 above = 12 calls, raise ActiveRecord::StatementTimeout
|
||||||
|
# 5. 12/2=6 => 1 call + 12 above = 13 calls, run successfully
|
||||||
|
#
|
||||||
|
# so out of 100 elements we created batches of 6 items => 100/6 = 17 sub-batches of 6 or less elements
|
||||||
|
#
|
||||||
|
# project.issues.count: 900 issues, so 9 batches of 100 => 9 * (13+16) = 261
|
||||||
|
expect(subject).to receive(:update_positions).exactly(261).times.and_call_original
|
||||||
|
|
||||||
|
subject.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when issue_rebalancing_optimization feature flag is on' do
|
context 'when issue_rebalancing_optimization feature flag is on' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(issue_rebalancing_optimization: true)
|
stub_feature_flags(issue_rebalancing_optimization: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'IssueRebalancingService shared examples'
|
it_behaves_like 'IssueRebalancingService shared examples'
|
||||||
|
|
||||||
|
context 'when issue_rebalancing_with_retry feature flag is on' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_rebalancing_with_retry: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'IssueRebalancingService shared examples'
|
||||||
|
it_behaves_like 'rebalancing is retried on statement timeout exceptions'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when issue_rebalancing_optimization feature flag is on' do
|
context 'when issue_rebalancing_optimization feature flag is off' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(issue_rebalancing_optimization: false)
|
stub_feature_flags(issue_rebalancing_optimization: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'IssueRebalancingService shared examples'
|
it_behaves_like 'IssueRebalancingService shared examples'
|
||||||
|
|
||||||
|
context 'when issue_rebalancing_with_retry feature flag is on' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_rebalancing_with_retry: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'IssueRebalancingService shared examples'
|
||||||
|
it_behaves_like 'rebalancing is retried on statement timeout exceptions'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -863,6 +863,46 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Snippets' do
|
||||||
|
before do
|
||||||
|
render
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can access snippets' do
|
||||||
|
it 'shows Snippets link' do
|
||||||
|
expect(rendered).to have_link('Snippets', href: project_snippets_path(project))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access snippets' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'does not show Snippets link' do
|
||||||
|
expect(rendered).not_to have_link('Snippets')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Members' do
|
||||||
|
before do
|
||||||
|
render
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can access members' do
|
||||||
|
it 'show Members link' do
|
||||||
|
expect(rendered).to have_link('Members', href: project_project_members_path(project))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access members' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'show Members link' do
|
||||||
|
expect(rendered).not_to have_link('Members')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'operations settings tab' do
|
describe 'operations settings tab' do
|
||||||
describe 'archive projects' do
|
describe 'archive projects' do
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -30,5 +30,11 @@ module Mail
|
||||||
def deliver!(mail)
|
def deliver!(mail)
|
||||||
@pool.with { |conn| conn.deliver!(mail) }
|
@pool.with { |conn| conn.deliver!(mail) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This makes it compatible with Mail's `#deliver!` method
|
||||||
|
# https://github.com/mikel/mail/blob/22a7afc23f253319965bf9228a0a430eec94e06d/lib/mail/message.rb#L271
|
||||||
|
def settings
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,5 +64,27 @@ describe Mail::SMTPPool do
|
||||||
|
|
||||||
expect(MockSMTP.deliveries.size).to eq(1)
|
expect(MockSMTP.deliveries.size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when called from Mail:Message' do
|
||||||
|
before do
|
||||||
|
mail.delivery_method(described_class, { pool: described_class.create_pool })
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#deliver' do
|
||||||
|
it 'delivers mail' do
|
||||||
|
mail.deliver
|
||||||
|
|
||||||
|
expect(MockSMTP.deliveries.size).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#deliver!' do
|
||||||
|
it 'delivers mail' do
|
||||||
|
mail.deliver!
|
||||||
|
|
||||||
|
expect(MockSMTP.deliveries.size).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue