Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
19ab203bec
commit
9c042f0dad
|
@ -136,8 +136,8 @@ export default {
|
|||
return __('Branch');
|
||||
}
|
||||
},
|
||||
commitTitleText() {
|
||||
return this.pipeline?.commit?.title || __("Can't find HEAD commit for this branch");
|
||||
commitTitle() {
|
||||
return this.pipeline?.commit?.title;
|
||||
},
|
||||
hasAuthor() {
|
||||
return (
|
||||
|
@ -159,22 +159,27 @@ export default {
|
|||
<div class="pipeline-tags" data-testid="pipeline-url-table-cell">
|
||||
<template v-if="rearrangePipelinesTable">
|
||||
<div class="commit-title gl-mb-2" data-testid="commit-title-container">
|
||||
<span class="gl-display-flex">
|
||||
<tooltip-on-truncate :title="commitTitleText" class="flex-truncate-child gl-flex-grow-1">
|
||||
<span v-if="commitTitle" class="gl-display-flex">
|
||||
<tooltip-on-truncate :title="commitTitle" class="flex-truncate-child gl-flex-grow-1">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
class="commit-row-message gl-text-blue-600!"
|
||||
:href="commitUrl"
|
||||
class="commit-row-message gl-text-gray-900"
|
||||
data-testid="commit-title"
|
||||
data-qa-selector="pipeline_url_link"
|
||||
>{{ commitTitleText }}</gl-link
|
||||
>{{ commitTitle }}</gl-link
|
||||
>
|
||||
</tooltip-on-truncate>
|
||||
</span>
|
||||
<span v-else>{{ __("Can't find HEAD commit for this branch") }}</span>
|
||||
</div>
|
||||
<div class="gl-mb-2">
|
||||
<span class="gl-font-weight-bold gl-text-gray-500" data-testid="pipeline-identifier">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
class="gl-text-decoration-underline gl-text-blue-600!"
|
||||
data-testid="pipeline-url-link"
|
||||
data-qa-selector="pipeline_url_link"
|
||||
>
|
||||
#{{ pipeline[pipelineKey] }}
|
||||
</span>
|
||||
</gl-link>
|
||||
<!--Commit row-->
|
||||
<div class="icon-container gl-display-inline-block">
|
||||
<gl-icon
|
||||
|
|
|
@ -10,11 +10,11 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
|
|||
import { __ } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import blobInfoQuery from '../queries/blob_info.query.graphql';
|
||||
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
|
||||
import BlobButtonGroup from './blob_button_group.vue';
|
||||
import BlobEdit from './blob_edit.vue';
|
||||
import ForkSuggestion from './fork_suggestion.vue';
|
||||
import { loadViewer } from './blob_viewers';
|
||||
|
||||
|
@ -24,12 +24,12 @@ export default {
|
|||
},
|
||||
components: {
|
||||
BlobHeader,
|
||||
BlobEdit,
|
||||
BlobButtonGroup,
|
||||
BlobContent,
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
ForkSuggestion,
|
||||
WebIdeLink,
|
||||
},
|
||||
mixins: [getRefMixin, glFeatureFlagMixin()],
|
||||
inject: {
|
||||
|
@ -213,12 +213,15 @@ export default {
|
|||
@viewer-changed="switchViewer"
|
||||
>
|
||||
<template #actions>
|
||||
<blob-edit
|
||||
<web-ide-link
|
||||
v-if="!blobInfo.archived"
|
||||
:show-edit-button="!isBinaryFileType"
|
||||
:edit-path="blobInfo.editBlobPath"
|
||||
:web-ide-path="blobInfo.ideEditPath"
|
||||
class="gl-mr-3"
|
||||
:edit-url="blobInfo.editBlobPath"
|
||||
:web-ide-url="blobInfo.ideEditPath"
|
||||
:needs-to-fork="showForkSuggestion"
|
||||
is-blob
|
||||
disable-fork-modal
|
||||
@edit="editBlob"
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
<script>
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WebIdeLink,
|
||||
},
|
||||
props: {
|
||||
showEditButton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
editPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
webIdePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
needsToFork: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEdit(target) {
|
||||
this.$emit('edit', target);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-ide-link
|
||||
:show-edit-button="showEditButton"
|
||||
class="gl-mr-3"
|
||||
:edit-url="editPath"
|
||||
:web-ide-url="webIdePath"
|
||||
:needs-to-fork="needsToFork"
|
||||
:is-blob="true"
|
||||
disable-fork-modal
|
||||
@edit="onEdit"
|
||||
/>
|
||||
</template>
|
|
@ -111,6 +111,15 @@ class ApplicationController < ActionController::Base
|
|||
render plain: e.message, status: :too_many_requests
|
||||
end
|
||||
|
||||
content_security_policy do |p|
|
||||
next if p.directives.blank?
|
||||
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?
|
||||
|
||||
default_connect_src = p.directives['connect-src'] || p.directives['default-src']
|
||||
connect_src_values = Array.wrap(default_connect_src) | [Gitlab::CurrentSettings.snowplow_collector_hostname]
|
||||
p.connect_src(*connect_src_values)
|
||||
end
|
||||
|
||||
def redirect_back_or_default(default: root_path, options: {})
|
||||
redirect_back(fallback_location: default, **options)
|
||||
end
|
||||
|
|
|
@ -5,8 +5,19 @@ module Types
|
|||
class ServiceTypeEnum < BaseEnum
|
||||
graphql_name 'ServiceType'
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def type_description(type)
|
||||
"#{type} type"
|
||||
end
|
||||
end
|
||||
|
||||
# This prepend must stay here because the dynamic block below depends on it.
|
||||
prepend_mod # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
::Integration.available_integration_types(include_dev: false).each do |type|
|
||||
value type.underscore.upcase, value: type, description: "#{type} type"
|
||||
value type.underscore.upcase, value: type, description: type_description(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ class Integration < ApplicationRecord
|
|||
include EachBatch
|
||||
include IgnorableColumns
|
||||
|
||||
ignore_column :template, remove_with: '14.10', remove_after: '2022-03-22'
|
||||
ignore_column :template, remove_with: '15.0', remove_after: '2022-04-22'
|
||||
|
||||
INTEGRATION_NAMES = %w[
|
||||
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
= render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) }
|
||||
- if user_application_theme == 'gl-dark'
|
||||
%meta{ name: 'color-scheme', content: 'dark light' }
|
||||
= stylesheet_link_tag_defer "application_dark"
|
||||
= yield :page_specific_styles
|
||||
= stylesheet_link_tag_defer "application_utilities_dark"
|
||||
|
|
|
@ -714,7 +714,14 @@ module.exports = {
|
|||
},
|
||||
host: DEV_SERVER_HOST || 'localhost',
|
||||
port: DEV_SERVER_PORT || 3808,
|
||||
// Setting up hot module reloading
|
||||
// HMR works by setting up a websocket server and injecting
|
||||
// a client script which connects to that server.
|
||||
// The server will push messages to the client to reload parts
|
||||
// of the JavaScript or reload the page if necessary
|
||||
webSocketServer: DEV_SERVER_LIVERELOAD ? 'ws' : false,
|
||||
hot: DEV_SERVER_LIVERELOAD,
|
||||
liveReload: DEV_SERVER_LIVERELOAD,
|
||||
// The following settings are mainly needed for HMR support in gitpod.
|
||||
// Per default only local hosts are allowed, but here we could
|
||||
// allow different hosts (e.g. ['.gitpod'], all of gitpod),
|
||||
|
|
|
@ -18018,6 +18018,7 @@ State of a Sentry error.
|
|||
| <a id="servicetypeexternal_wiki_service"></a>`EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. |
|
||||
| <a id="servicetypeflowdock_service"></a>`FLOWDOCK_SERVICE` | FlowdockService type. |
|
||||
| <a id="servicetypegithub_service"></a>`GITHUB_SERVICE` | GithubService type. |
|
||||
| <a id="servicetypegitlab_slack_application_service"></a>`GITLAB_SLACK_APPLICATION_SERVICE` | GitlabSlackApplicationService type (Gitlab.com only). |
|
||||
| <a id="servicetypehangouts_chat_service"></a>`HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. |
|
||||
| <a id="servicetypeirker_service"></a>`IRKER_SERVICE` | IrkerService type. |
|
||||
| <a id="servicetypejenkins_service"></a>`JENKINS_SERVICE` | JenkinsService type. |
|
||||
|
|
|
@ -559,6 +559,8 @@ Decreasing the user cap does not approve pending members.
|
|||
When the number of billable users reaches the user cap, any new member is put in a pending state
|
||||
and must be approved.
|
||||
|
||||
Pending members do not count as billable. Members count as billable only after they have been approved and are no longer in a pending state.
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must be assigned the Owner role) for the group.
|
||||
|
|
|
@ -20,7 +20,7 @@ The default behavior is:
|
|||
explicitly, the inventory object is stored in the `default` namespace.
|
||||
- The `name` is generated from the numeric project ID of the manifest project and the numeric agent ID.
|
||||
|
||||
This way the Agent constructs the name and local where the inventory object is
|
||||
This way, the Agent constructs the name and location where the inventory object is
|
||||
stored in the cluster.
|
||||
|
||||
The Agent cannot locate the existing inventory object if you:
|
||||
|
@ -31,7 +31,7 @@ The Agent cannot locate the existing inventory object if you:
|
|||
## Inventory object template
|
||||
|
||||
The inventory object template is a `ConfigMap` object that allows you to configure the namespace and the name of the inventory
|
||||
object. Store this template with manifest files in a single group.
|
||||
object. Store this template with manifest files as a single logical group.
|
||||
|
||||
Example inventory object template:
|
||||
|
||||
|
|
|
@ -37,8 +37,8 @@ These shortcuts are available in most areas of GitLab:
|
|||
| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
|
||||
| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. |
|
||||
| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
|
||||
| <kbd>p</kbd> + <kbd>b</kbd> | Show or hide the Performance Bar. |
|
||||
| <kbd>g</kbd> + <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
|
||||
| <kbd>p</kbd> then <kbd>b</kbd> | Show or hide the Performance Bar. |
|
||||
| <kbd>g</kbd> then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
|
||||
| <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). |
|
||||
|
||||
Additionally, the following shortcuts are available when editing text in text
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Subscriptions Content Security Policy' do
|
||||
include ContentSecurityPolicyHelpers
|
||||
|
||||
let(:installation) { create(:jira_connect_installation) }
|
||||
let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test') }
|
||||
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
|
||||
|
@ -11,10 +13,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
|
|||
|
||||
context 'when there is no global config' do
|
||||
before do
|
||||
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy)
|
||||
.and_return(ActionDispatch::ContentSecurityPolicy.new)
|
||||
end
|
||||
setup_csp_for_controller(JiraConnect::SubscriptionsController)
|
||||
end
|
||||
|
||||
it 'does not add CSP directives' do
|
||||
|
@ -31,9 +30,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
|
|||
p.style_src :self, 'https://some-cdn.test'
|
||||
end
|
||||
|
||||
expect_next_instance_of(JiraConnect::SubscriptionsController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy).and_return(csp)
|
||||
end
|
||||
setup_existing_csp_for_controller(JiraConnect::SubscriptionsController, csp)
|
||||
end
|
||||
|
||||
it 'appends to CSP directives' do
|
||||
|
|
|
@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
it 'sees branch pipelines and detached merge request pipelines in correct order' do
|
||||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.ci-created', count: 2)
|
||||
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}")
|
||||
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -101,16 +101,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.ci-pending', count: 4)
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[0])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[0])
|
||||
.to have_content("##{detached_merge_request_pipeline_2.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[1])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[1])
|
||||
.to have_content("##{detached_merge_request_pipeline.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[2])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[2])
|
||||
.to have_content("##{push_pipeline_2.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[3])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[3])
|
||||
.to have_content("##{push_pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
@ -201,7 +201,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
it 'sees a branch pipeline in pipeline tab' do
|
||||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.ci-created', count: 1)
|
||||
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{push_pipeline.id}")
|
||||
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -252,7 +252,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
it 'sees branch pipelines and detached merge request pipelines in correct order' do
|
||||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.ci-pending', count: 2)
|
||||
expect(first('[data-testid="pipeline-identifier"]')).to have_content("##{detached_merge_request_pipeline.id}")
|
||||
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -295,16 +295,16 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.ci-pending', count: 4)
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[0])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[0])
|
||||
.to have_content("##{detached_merge_request_pipeline_2.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[1])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[1])
|
||||
.to have_content("##{detached_merge_request_pipeline.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[2])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[2])
|
||||
.to have_content("##{push_pipeline_2.id}")
|
||||
|
||||
expect(all('[data-testid="pipeline-identifier"]')[3])
|
||||
expect(all('[data-testid="pipeline-url-link"]')[3])
|
||||
.to have_content("##{push_pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -134,7 +134,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
|
|||
create_merge_request_pipeline
|
||||
act_on_security_warning(action: 'Run pipeline')
|
||||
|
||||
check_pipeline(expected_project: parent_project, link_selector: 'pipeline-url-link')
|
||||
check_pipeline(expected_project: parent_project)
|
||||
check_head_pipeline(expected_project: parent_project)
|
||||
end
|
||||
|
||||
|
@ -179,13 +179,13 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
|
|||
click_button('Run pipeline')
|
||||
end
|
||||
|
||||
def check_pipeline(expected_project:, link_selector: 'commit-title')
|
||||
def check_pipeline(expected_project:)
|
||||
page.within('.ci-table') do
|
||||
expect(page).to have_selector('.commit', count: 2)
|
||||
|
||||
page.within(first('.commit')) do
|
||||
page.within('.pipeline-tags') do
|
||||
expect(page.find("[data-testid=#{link_selector}]")[:href]).to include(expected_project.full_path)
|
||||
expect(page.find('[data-testid="pipeline-url-link"]')[:href]).to include(expected_project.full_path)
|
||||
expect(page).to have_content('detached')
|
||||
end
|
||||
page.within('.pipeline-triggerer') do
|
||||
|
|
|
@ -711,7 +711,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
end
|
||||
|
||||
expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline'
|
||||
expect(page.find('[data-testid="pipeline-identifier"]')).to have_content "##{pipeline.iid}"
|
||||
expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Tracings Content Security Policy' do
|
||||
include ContentSecurityPolicyHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
@ -18,10 +20,7 @@ RSpec.describe 'Tracings Content Security Policy' do
|
|||
|
||||
context 'when there is no global config' do
|
||||
before do
|
||||
expect_next_instance_of(Projects::TracingsController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy)
|
||||
.and_return(ActionDispatch::ContentSecurityPolicy.new)
|
||||
end
|
||||
setup_csp_for_controller(Projects::TracingsController)
|
||||
end
|
||||
|
||||
it 'does not add CSP directives' do
|
||||
|
@ -37,9 +36,7 @@ RSpec.describe 'Tracings Content Security Policy' do
|
|||
p.frame_src 'https://global-policy.com'
|
||||
end
|
||||
|
||||
expect_next_instance_of(Projects::TracingsController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy).and_return(csp)
|
||||
end
|
||||
setup_existing_csp_for_controller(Projects::TracingsController, csp)
|
||||
end
|
||||
|
||||
context 'when external_url is set' do
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Static Site Editor' do
|
||||
include ContentSecurityPolicyHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :public, :repository) }
|
||||
|
||||
|
@ -79,10 +81,7 @@ RSpec.describe 'Static Site Editor' do
|
|||
|
||||
context 'when no global CSP config exists' do
|
||||
before do
|
||||
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy)
|
||||
.and_return(ActionDispatch::ContentSecurityPolicy.new)
|
||||
end
|
||||
setup_csp_for_controller(Projects::StaticSiteEditorController)
|
||||
end
|
||||
|
||||
it 'does not add CSP directives' do
|
||||
|
@ -101,9 +100,7 @@ RSpec.describe 'Static Site Editor' do
|
|||
p.frame_src :self, cdn_url
|
||||
end
|
||||
|
||||
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy).and_return(csp)
|
||||
end
|
||||
setup_existing_csp_for_controller(Projects::StaticSiteEditorController, csp)
|
||||
end
|
||||
|
||||
it 'appends youtube to the CSP frame-src policy' do
|
||||
|
|
|
@ -676,7 +676,7 @@ export const mockPipeline = (projectPath) => {
|
|||
short_id: 'fd6df5b3',
|
||||
created_at: '2021-10-19T21:17:12.000+00:00',
|
||||
parent_ids: ['7147906b84306e83cb3fec6582a25390b75713c6'],
|
||||
title: 'Commit Title',
|
||||
title: 'Commit',
|
||||
message: 'Commit',
|
||||
author_name: 'Administrator',
|
||||
author_email: 'admin@example.com',
|
||||
|
@ -1141,176 +1141,3 @@ export const mockPipelineBranch = () => {
|
|||
viewType: 'root',
|
||||
};
|
||||
};
|
||||
|
||||
export const mockPipelineNoCommit = () => {
|
||||
return {
|
||||
pipeline: {
|
||||
id: 268,
|
||||
iid: 34,
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'root',
|
||||
name: 'Administrator',
|
||||
state: 'active',
|
||||
avatar_url:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
web_url: 'http://gdk.test:3000/root',
|
||||
show_status: false,
|
||||
path: '/root',
|
||||
},
|
||||
active: false,
|
||||
source: 'push',
|
||||
created_at: '2022-01-14T17:40:27.866Z',
|
||||
updated_at: '2022-01-14T18:02:35.850Z',
|
||||
path: '/root/mr-widgets/-/pipelines/268',
|
||||
flags: {
|
||||
stuck: false,
|
||||
auto_devops: false,
|
||||
merge_request: false,
|
||||
yaml_errors: false,
|
||||
retryable: true,
|
||||
cancelable: false,
|
||||
failure_reason: false,
|
||||
detached_merge_request_pipeline: false,
|
||||
merge_request_pipeline: false,
|
||||
merge_train_pipeline: false,
|
||||
latest: true,
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_warning',
|
||||
text: 'passed',
|
||||
label: 'passed with warnings',
|
||||
group: 'success-with-warnings',
|
||||
tooltip: 'passed',
|
||||
has_details: true,
|
||||
details_path: '/root/mr-widgets/-/pipelines/268',
|
||||
illustration: null,
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
name: 'validate',
|
||||
title: 'validate: passed with warnings',
|
||||
status: {
|
||||
icon: 'status_warning',
|
||||
text: 'passed',
|
||||
label: 'passed with warnings',
|
||||
group: 'success-with-warnings',
|
||||
tooltip: 'passed',
|
||||
has_details: true,
|
||||
details_path: '/root/mr-widgets/-/pipelines/268#validate',
|
||||
illustration: null,
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
|
||||
},
|
||||
path: '/root/mr-widgets/-/pipelines/268#validate',
|
||||
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=validate',
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
title: 'test: passed',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
tooltip: 'passed',
|
||||
has_details: true,
|
||||
details_path: '/root/mr-widgets/-/pipelines/268#test',
|
||||
illustration: null,
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
|
||||
},
|
||||
path: '/root/mr-widgets/-/pipelines/268#test',
|
||||
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=test',
|
||||
},
|
||||
{
|
||||
name: 'build',
|
||||
title: 'build: passed',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
tooltip: 'passed',
|
||||
has_details: true,
|
||||
details_path: '/root/mr-widgets/-/pipelines/268#build',
|
||||
illustration: null,
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
|
||||
},
|
||||
path: '/root/mr-widgets/-/pipelines/268#build',
|
||||
dropdown_path: '/root/mr-widgets/-/pipelines/268/stage.json?stage=build',
|
||||
},
|
||||
],
|
||||
duration: 75,
|
||||
finished_at: '2022-01-14T18:02:35.842Z',
|
||||
name: 'Pipeline',
|
||||
manual_actions: [],
|
||||
scheduled_actions: [],
|
||||
},
|
||||
ref: {
|
||||
name: 'update-ci',
|
||||
path: '/root/mr-widgets/-/commits/update-ci',
|
||||
tag: false,
|
||||
branch: true,
|
||||
merge_request: false,
|
||||
},
|
||||
retry_path: '/root/mr-widgets/-/pipelines/268/retry',
|
||||
delete_path: '/root/mr-widgets/-/pipelines/268',
|
||||
failed_builds: [
|
||||
{
|
||||
id: 1260,
|
||||
name: 'fmt',
|
||||
started: '2022-01-14T17:40:36.435Z',
|
||||
complete: true,
|
||||
archived: false,
|
||||
build_path: '/root/mr-widgets/-/jobs/1260',
|
||||
retry_path: '/root/mr-widgets/-/jobs/1260/retry',
|
||||
playable: false,
|
||||
scheduled: false,
|
||||
created_at: '2022-01-14T17:40:27.879Z',
|
||||
updated_at: '2022-01-14T17:40:42.129Z',
|
||||
status: {
|
||||
icon: 'status_warning',
|
||||
text: 'failed',
|
||||
label: 'failed (allowed to fail)',
|
||||
group: 'failed-with-warnings',
|
||||
tooltip: 'failed - (script failure) (allowed to fail)',
|
||||
has_details: true,
|
||||
details_path: '/root/mr-widgets/-/jobs/1260',
|
||||
illustration: {
|
||||
image:
|
||||
'/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
|
||||
size: 'svg-430',
|
||||
title: 'This job does not have a trace.',
|
||||
},
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
path: '/root/mr-widgets/-/jobs/1260/retry',
|
||||
method: 'post',
|
||||
button_title: 'Retry this job',
|
||||
},
|
||||
},
|
||||
recoverable: false,
|
||||
},
|
||||
],
|
||||
project: {
|
||||
id: 23,
|
||||
name: 'mr-widgets',
|
||||
full_path: '/root/mr-widgets',
|
||||
full_name: 'Administrator / mr-widgets',
|
||||
},
|
||||
triggered_by: null,
|
||||
triggered: [],
|
||||
},
|
||||
pipelineScheduleUrl: 'foo',
|
||||
pipelineKey: 'id',
|
||||
viewType: 'root',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
|
||||
import {
|
||||
mockPipeline,
|
||||
mockPipelineBranch,
|
||||
mockPipelineTag,
|
||||
mockPipelineNoCommit,
|
||||
} from './mock_data';
|
||||
import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
|
||||
|
||||
const projectPath = 'test/test';
|
||||
|
||||
|
@ -31,7 +26,7 @@ describe('Pipeline Url Component', () => {
|
|||
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
|
||||
|
||||
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
|
||||
const findCommitTitle = () => wrapper.findByTestId('commit-title');
|
||||
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
|
||||
|
||||
const defaultProps = mockPipeline(projectPath);
|
||||
|
||||
|
@ -237,33 +232,5 @@ describe('Pipeline Url Component', () => {
|
|||
expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
|
||||
},
|
||||
);
|
||||
|
||||
describe('with commit', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, true);
|
||||
});
|
||||
|
||||
it('displays commit title with link to pipeline', () => {
|
||||
expect(findCommitTitle().attributes('href')).toBe(defaultProps.pipeline.path);
|
||||
});
|
||||
|
||||
it('displays commit title text', () => {
|
||||
expect(findCommitTitle().text()).toBe(defaultProps.pipeline.commit.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without commit', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(mockPipelineNoCommit(), true);
|
||||
});
|
||||
|
||||
it('displays cant find head commit text', () => {
|
||||
expect(findCommitTitle().text()).toBe("Can't find HEAD commit for this branch");
|
||||
});
|
||||
|
||||
it('displays link to pipeline', () => {
|
||||
expect(findCommitTitle().attributes('href')).toBe(mockPipelineNoCommit().pipeline.path);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
|
|||
import BlobHeader from '~/blob/components/blob_header.vue';
|
||||
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
|
||||
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
|
||||
import BlobEdit from '~/repository/components/blob_edit.vue';
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
|
||||
import { loadViewer } from '~/repository/components/blob_viewers';
|
||||
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
|
||||
|
@ -99,7 +99,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
|
|||
describe('Blob content viewer component', () => {
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
|
||||
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
|
||||
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
|
||||
const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
|
||||
const findBlobContent = () => wrapper.findComponent(BlobContent);
|
||||
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
|
||||
|
@ -255,32 +255,32 @@ describe('Blob content viewer component', () => {
|
|||
describe('BlobHeader action slot', () => {
|
||||
const { ideEditPath, editBlobPath } = simpleViewerMock;
|
||||
|
||||
it('renders BlobHeaderEdit buttons in simple viewer', async () => {
|
||||
it('renders WebIdeLink button in simple viewer', async () => {
|
||||
await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
|
||||
|
||||
expect(findBlobEdit().props()).toMatchObject({
|
||||
editPath: editBlobPath,
|
||||
webIdePath: ideEditPath,
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
editUrl: editBlobPath,
|
||||
webIdeUrl: ideEditPath,
|
||||
showEditButton: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders BlobHeaderEdit button in rich viewer', async () => {
|
||||
it('renders WebIdeLink button in rich viewer', async () => {
|
||||
await createComponent({ blob: richViewerMock }, mount);
|
||||
|
||||
expect(findBlobEdit().props()).toMatchObject({
|
||||
editPath: editBlobPath,
|
||||
webIdePath: ideEditPath,
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
editUrl: editBlobPath,
|
||||
webIdeUrl: ideEditPath,
|
||||
showEditButton: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders BlobHeaderEdit button for binary files', async () => {
|
||||
it('renders WebIdeLink button for binary files', async () => {
|
||||
await createComponent({ blob: richViewerMock, isBinary: true }, mount);
|
||||
|
||||
expect(findBlobEdit().props()).toMatchObject({
|
||||
editPath: editBlobPath,
|
||||
webIdePath: ideEditPath,
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
editUrl: editBlobPath,
|
||||
webIdeUrl: ideEditPath,
|
||||
showEditButton: false,
|
||||
});
|
||||
});
|
||||
|
@ -318,7 +318,7 @@ describe('Blob content viewer component', () => {
|
|||
|
||||
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
|
||||
expect(findBlobHeader().props('isBinary')).toBe(true);
|
||||
expect(findBlobEdit().props('showEditButton')).toBe(false);
|
||||
expect(findWebIdeLink().props('showEditButton')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -401,12 +401,12 @@ describe('Blob content viewer component', () => {
|
|||
beforeEach(() => createComponent({}, mount));
|
||||
|
||||
it('simple edit redirects to the simple editor', () => {
|
||||
findBlobEdit().vm.$emit('edit', 'simple');
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
|
||||
});
|
||||
|
||||
it('IDE edit redirects to the IDE editor', () => {
|
||||
findBlobEdit().vm.$emit('edit', 'ide');
|
||||
findWebIdeLink().vm.$emit('edit', 'ide');
|
||||
expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
|
||||
});
|
||||
|
||||
|
@ -435,7 +435,7 @@ describe('Blob content viewer component', () => {
|
|||
mount,
|
||||
);
|
||||
|
||||
findBlobEdit().vm.$emit('edit', 'simple');
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import BlobEdit from '~/repository/components/blob_edit.vue';
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
editPath: 'some_file.js/edit',
|
||||
webIdePath: 'some_file.js/ide/edit',
|
||||
showEditButton: true,
|
||||
needsToFork: false,
|
||||
};
|
||||
|
||||
describe('BlobEdit component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(BlobEdit, {
|
||||
propsData: {
|
||||
...DEFAULT_PROPS,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findEditButton = () => wrapper.find('[data-testid="edit"]');
|
||||
const findWebIdeLink = () => wrapper.find(WebIdeLink);
|
||||
|
||||
it('renders component', () => {
|
||||
createComponent();
|
||||
|
||||
const { editPath, webIdePath } = DEFAULT_PROPS;
|
||||
|
||||
expect(wrapper.props()).toMatchObject({
|
||||
editPath,
|
||||
webIdePath,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders WebIdeLink component', () => {
|
||||
createComponent();
|
||||
|
||||
const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
|
||||
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
editUrl,
|
||||
webIdeUrl,
|
||||
isBlob: true,
|
||||
showEditButton: true,
|
||||
needsToFork,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without Edit button', () => {
|
||||
const showEditButton = false;
|
||||
|
||||
it('renders WebIdeLink component without an edit button', () => {
|
||||
createComponent({ showEditButton });
|
||||
|
||||
expect(findWebIdeLink().props()).toMatchObject({ showEditButton });
|
||||
});
|
||||
|
||||
it('does not render an Edit button', () => {
|
||||
createComponent({ showEditButton });
|
||||
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -566,6 +566,12 @@ RSpec.describe Integration do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.integration_name_to_type' do
|
||||
it 'transforms the name to a type' do
|
||||
expect(described_class.integration_name_to_type('asana')).to eq('AsanaService')
|
||||
end
|
||||
end
|
||||
|
||||
describe "{property}_changed?" do
|
||||
let(:integration) do
|
||||
Integrations::Bamboo.create!(
|
||||
|
@ -774,35 +780,33 @@ RSpec.describe Integration do
|
|||
end
|
||||
|
||||
describe '.available_integration_names' do
|
||||
it 'calls the right methods' do
|
||||
expect(described_class).to receive(:integration_names).and_call_original
|
||||
expect(described_class).to receive(:dev_integration_names).and_call_original
|
||||
expect(described_class).to receive(:project_specific_integration_names).and_call_original
|
||||
subject { described_class.available_integration_names }
|
||||
|
||||
described_class.available_integration_names
|
||||
before do
|
||||
allow(described_class).to receive(:integration_names).and_return(%w(foo))
|
||||
allow(described_class).to receive(:project_specific_integration_names).and_return(['bar'])
|
||||
allow(described_class).to receive(:dev_integration_names).and_return(['baz'])
|
||||
end
|
||||
|
||||
it 'does not call project_specific_integration_names with include_project_specific false' do
|
||||
expect(described_class).to receive(:integration_names).and_call_original
|
||||
expect(described_class).to receive(:dev_integration_names).and_call_original
|
||||
expect(described_class).not_to receive(:project_specific_integration_names)
|
||||
it { is_expected.to include('foo', 'bar', 'baz') }
|
||||
|
||||
described_class.available_integration_names(include_project_specific: false)
|
||||
context 'when `include_project_specific` is false' do
|
||||
subject { described_class.available_integration_names(include_project_specific: false) }
|
||||
|
||||
it { is_expected.to include('foo', 'baz') }
|
||||
it { is_expected.not_to include('bar') }
|
||||
end
|
||||
|
||||
it 'does not call dev_integration_names with include_dev false' do
|
||||
expect(described_class).to receive(:integration_names).and_call_original
|
||||
expect(described_class).not_to receive(:dev_integration_names)
|
||||
expect(described_class).to receive(:project_specific_integration_names).and_call_original
|
||||
context 'when `include_dev` is false' do
|
||||
subject { described_class.available_integration_names(include_dev: false) }
|
||||
|
||||
described_class.available_integration_names(include_dev: false)
|
||||
it { is_expected.to include('foo', 'bar') }
|
||||
it { is_expected.not_to include('baz') }
|
||||
end
|
||||
|
||||
it { expect(described_class.available_integration_names).to include('jenkins') }
|
||||
end
|
||||
|
||||
describe '.project_specific_integration_names' do
|
||||
it do
|
||||
specify do
|
||||
expect(described_class.project_specific_integration_names)
|
||||
.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES)
|
||||
end
|
||||
|
|
|
@ -10,6 +10,14 @@ RSpec.describe API::Integrations do
|
|||
create(:project, creator_id: user.id, namespace: user.namespace)
|
||||
end
|
||||
|
||||
# The API supports all integrations except the GitLab Slack Application
|
||||
# integration; this integration must be installed via the UI.
|
||||
def self.integration_names
|
||||
names = Integration.available_integration_names
|
||||
names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee?
|
||||
names
|
||||
end
|
||||
|
||||
%w[integrations services].each do |endpoint|
|
||||
describe "GET /projects/:id/#{endpoint}" do
|
||||
it 'returns authentication error when unauthenticated' do
|
||||
|
@ -43,7 +51,7 @@ RSpec.describe API::Integrations do
|
|||
end
|
||||
end
|
||||
|
||||
Integration.available_integration_names.each do |integration|
|
||||
integration_names.each do |integration|
|
||||
describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
|
||||
include_context integration
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# The AnonymousController doesn't support setting the CSP
|
||||
# This is why an arbitrary test request was chosen instead
|
||||
# of testing in application_controller_spec.
|
||||
RSpec.describe 'Content Security Policy' do
|
||||
let(:snowplow_host) { 'snowplow.example.com' }
|
||||
|
||||
shared_examples 'snowplow is not in the CSP' do
|
||||
it 'does not add the snowplow collector hostname to the CSP' do
|
||||
get explore_root_url
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Content-Security-Policy']).not_to include(snowplow_host)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #explore' do
|
||||
context 'snowplow is enabled' do
|
||||
before do
|
||||
stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: snowplow_host)
|
||||
end
|
||||
|
||||
it 'adds the snowplow collector hostname to the CSP' do
|
||||
get explore_root_url
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Content-Security-Policy']).to include(snowplow_host)
|
||||
end
|
||||
end
|
||||
|
||||
context 'snowplow is enabled but host is not configured' do
|
||||
before do
|
||||
stub_application_setting(snowplow_enabled: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'snowplow is not in the CSP'
|
||||
end
|
||||
|
||||
context 'snowplow is disabled' do
|
||||
before do
|
||||
stub_application_setting(snowplow_enabled: false, snowplow_collector_hostname: snowplow_host)
|
||||
end
|
||||
|
||||
it_behaves_like 'snowplow is not in the CSP'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ContentSecurityPolicyHelpers
|
||||
# Expecting 2 calls to current_content_security_policy by default, once for
|
||||
# the call that's being tested and once for the call in ApplicationController
|
||||
def setup_csp_for_controller(controller_class, times = 2)
|
||||
expect_next_instance_of(controller_class) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy)
|
||||
.and_return(ActionDispatch::ContentSecurityPolicy.new).exactly(times).times
|
||||
end
|
||||
end
|
||||
|
||||
# Expecting 2 calls to current_content_security_policy by default, once for
|
||||
# the call that's being tested and once for the call in ApplicationController
|
||||
def setup_existing_csp_for_controller(controller_class, csp, times = 2)
|
||||
expect_next_instance_of(controller_class) do |controller|
|
||||
expect(controller).to receive(:current_content_security_policy).and_return(csp).exactly(times).times
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue