Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-12 21:10:33 +00:00
parent bf1990164b
commit 8cdf31a1f9
32 changed files with 592 additions and 187 deletions

View File

@ -1,53 +1,106 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { ContentEditor } from '../services/content_editor';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor';
import ContentEditorError from './content_editor_error.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
export default {
components: {
GlAlert,
GlLoadingIcon,
ContentEditorError,
ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
FormattingBubbleMenu,
},
provide() {
return {
tiptapEditor: this.contentEditor.tiptapEditor,
};
EditorStateObserver,
},
props: {
contentEditor: {
type: ContentEditor,
renderMarkdown: {
type: Function,
required: true,
},
uploadsPath: {
type: String,
required: true,
},
extensions: {
type: Array,
required: false,
default: () => [],
},
serializerConfig: {
type: Object,
required: false,
default: () => {},
},
},
data() {
return {
error: '',
isLoadingContent: false,
focused: false,
};
},
mounted() {
this.contentEditor.tiptapEditor.on('error', (error) => {
this.error = error;
created() {
const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this;
// This is a non-reactive attribute intentionally since this is a complex object.
this.contentEditor = createContentEditor({
renderMarkdown,
uploadsPath,
extensions,
serializerConfig,
});
this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
this.$emit('initialized', this.contentEditor);
},
beforeDestroy() {
this.contentEditor.dispose();
this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
},
methods: {
displayLoadingIndicator() {
this.isLoadingContent = true;
},
hideLoadingIndicator() {
this.isLoadingContent = false;
},
focus() {
this.focused = true;
},
blur() {
this.focused = false;
},
notifyChange() {
this.$emit('change', {
empty: this.contentEditor.empty,
});
},
},
};
</script>
<template>
<div>
<gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<div
data-testid="content-editor"
class="md-area"
:class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
<formatting-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
<content-editor-provider :content-editor="contentEditor">
<div>
<editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
<content-editor-error />
<div data-testid="content-editor" class="md-area" :class="{ 'is-focused': focused }">
<top-toolbar ref="toolbar" class="gl-mb-4" />
<formatting-bubble-menu />
<div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
<gl-loading-icon size="sm" />
</div>
<tiptap-editor-content v-else class="md" :editor="contentEditor.tiptapEditor" />
</div>
</div>
</div>
</content-editor-provider>
</template>

View File

@ -0,0 +1,24 @@
<script>
export default {
provide() {
// We can't use this.contentEditor due to bug in vue-apollo when
// provide is called in beforeCreate
// See https://github.com/vuejs/vue-apollo/pull/1153 for details
const { contentEditor } = this.$options.propsData;
return {
contentEditor,
tiptapEditor: contentEditor.tiptapEditor,
};
},
props: {
contentEditor: {
type: Object,
required: true,
},
},
render() {
return this.$slots.default;
},
};
</script>

View File

@ -19,6 +19,10 @@ export class ContentEditor {
return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0);
}
dispose() {
this.tiptapEditor.destroy();
}
once(type, handler) {
this._eventHub.$once(type, handler);
}

View File

@ -72,7 +72,9 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
editor.emit('error', {
error: __('An error occurred while uploading the image. Please try again.'),
});
}
};
@ -100,7 +102,9 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
editor.emit('error', __('An error occurred while uploading the file. Please try again.'));
editor.emit('error', {
error: __('An error occurred while uploading the file. Please try again.'),
});
}
};

View File

@ -6,7 +6,6 @@ import {
GlButton,
GlSprintf,
GlAlert,
GlLoadingIcon,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
@ -114,7 +113,6 @@ export default {
GlButton,
GlModal,
MarkdownField,
GlLoadingIcon,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
@ -136,11 +134,12 @@ export default {
commitMessage: '',
isDirty: false,
contentEditorRenderFailed: false,
contentEditorEmpty: false,
};
},
computed: {
noContent() {
if (this.isContentEditorActive) return this.contentEditor?.empty;
if (this.isContentEditorActive) return this.contentEditorEmpty;
return !this.content.trim();
},
csrfToken() {
@ -205,7 +204,7 @@ export default {
window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
getContentHTML(content) {
renderMarkdown(content) {
return axios
.post(this.pageInfo.markdownPreviewPath, { text: content })
.then(({ data }) => data.body);
@ -232,6 +231,32 @@ export default {
this.isDirty = true;
},
async loadInitialContent(contentEditor) {
this.contentEditor = contentEditor;
try {
await this.contentEditor.setSerializedContent(this.content);
this.trackContentEditorLoaded();
} catch (e) {
this.contentEditorRenderFailed = true;
}
},
async retryInitContentEditor() {
try {
this.contentEditorRenderFailed = false;
await this.contentEditor.setSerializedContent(this.content);
} catch (e) {
this.contentEditorRenderFailed = true;
}
},
handleContentEditorChange({ empty }) {
this.contentEditorEmpty = empty;
// TODO: Implement a precise mechanism to detect changes in the Content
this.isDirty = true;
},
onPageUnload(event) {
if (!this.isDirty) return undefined;
@ -252,36 +277,8 @@ export default {
this.commitMessage = newCommitMessage;
},
async initContentEditor() {
this.isContentEditorLoading = true;
initContentEditor() {
this.useContentEditor = true;
const { createContentEditor } = await import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_content_editor'
);
this.contentEditor =
this.contentEditor ||
createContentEditor({
renderMarkdown: (markdown) => this.getContentHTML(markdown),
uploadsPath: this.pageInfo.uploadsPath,
tiptapOptions: {
onUpdate: () => this.handleContentChange(),
},
});
try {
await this.contentEditor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
this.trackContentEditorLoaded();
} catch (e) {
this.contentEditorRenderFailed = true;
}
},
retryInitContentEditor() {
this.contentEditorRenderFailed = false;
this.initContentEditor();
},
switchToOldEditor() {
@ -475,12 +472,12 @@ export default {
>
</gl-sprintf>
</gl-alert>
<gl-loading-icon
v-if="isContentEditorLoading"
size="sm"
class="bordered-box gl-w-full gl-py-6"
<content-editor
:render-markdown="renderMarkdown"
:uploads-path="pageInfo.uploadsPath"
@initialized="loadInitialContent"
@change="handleContentEditorChange"
/>
<content-editor v-else :content-editor="contentEditor" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
</div>

View File

@ -83,6 +83,7 @@
li.md-header-toolbar {
margin-left: auto;
display: none;
padding-bottom: $gl-padding-8;
&.active {
display: block;
@ -92,8 +93,8 @@
display: flex;
justify-content: center;
width: 100%;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
flex-wrap: wrap;
margin-top: $gl-padding-8;
}
}
}

View File

@ -14,7 +14,6 @@
a,
button {
padding: $gl-padding-8;
padding-bottom: $gl-padding-8 + 1;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;

View File

@ -54,7 +54,7 @@
.common-note-form {
.md-area {
padding: $gl-padding-top $gl-padding;
padding: $gl-padding-8 $gl-padding;
border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,

View File

@ -3,11 +3,8 @@
module Packages
module Pypi
class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
def execute!
results = packages.with_normalized_pypi_name(@params[:package_name])
raise ActiveRecord::RecordNotFound if results.empty?
results
def execute
packages.with_normalized_pypi_name(@params[:package_name])
end
private

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddPypiPackageRequestsForwardingToApplicationSettings < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
add_column(:application_settings, :pypi_package_requests_forwarding, :boolean, default: true, null: false)
end
end
def down
with_lock_retries do
remove_column(:application_settings, :pypi_package_requests_forwarding)
end
end
end

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class FinalizeCiJobArtifactsBigintConversion < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
TABLE_NAME = 'ci_job_artifacts'
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
table_name: TABLE_NAME,
column_name: 'id',
job_arguments: [%w[id job_id], %w[id_convert_to_bigint job_id_convert_to_bigint]]
)
swap
end
def down
swap
end
private
def swap
add_concurrent_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: 'index_ci_job_artifact_on_id_convert_to_bigint'
# This is to replace the existing "index_ci_job_artifacts_for_terraform_reports" btree (project_id, id) where (file_type = 18)
add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: 'index_ci_job_artifacts_for_terraform_reports_bigint', where: "file_type = 18"
# This is to replace the existing "index_ci_job_artifacts_id_for_terraform_reports" btree (id) where (file_type = 18)
add_concurrent_index TABLE_NAME, [:id_convert_to_bigint], name: 'index_ci_job_artifacts_id_for_terraform_reports_bigint', where: "file_type = 18"
# Add a FK on `project_pages_metadata(artifacts_archive_id)` to `id_convert_to_bigint`, the old FK (fk_69366a119e)
# will be removed when ci_job_artifacts_pkey constraint is droppped.
fk_artifacts_archive_id = concurrent_foreign_key_name(:project_pages_metadata, :artifacts_archive_id)
fk_artifacts_archive_id_tmp = "#{fk_artifacts_archive_id}_tmp"
add_concurrent_foreign_key :project_pages_metadata, TABLE_NAME,
column: :artifacts_archive_id, target_column: :id_convert_to_bigint,
name: fk_artifacts_archive_id_tmp,
on_delete: :nullify,
reverse_lock_order: true
with_lock_retries(raise_on_exhaustion: true) do
# We'll need ACCESS EXCLUSIVE lock on the related tables,
# lets make sure it can be acquired from the start
execute "LOCK TABLE #{TABLE_NAME}, project_pages_metadata IN ACCESS EXCLUSIVE MODE"
# Swap column names
temp_name = 'id_tmp'
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:id)} TO #{quote_column_name(temp_name)}"
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:id_convert_to_bigint)} TO #{quote_column_name(:id)}"
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:id_convert_to_bigint)}"
# We need to update the trigger function in order to make PostgreSQL to
# regenerate the execution plan for it. This is to avoid type mismatch errors like
# "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name([:id, :job_id], [:id_convert_to_bigint, :job_id_convert_to_bigint])
execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
# Swap defaults
execute "ALTER SEQUENCE ci_job_artifacts_id_seq OWNED BY #{TABLE_NAME}.id"
change_column_default TABLE_NAME, :id, -> { "nextval('ci_job_artifacts_id_seq'::regclass)" }
change_column_default TABLE_NAME, :id_convert_to_bigint, 0
# Swap PK constraint
execute "ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT ci_job_artifacts_pkey CASCADE" # this will drop ci_job_artifacts_pkey primary key
rename_index TABLE_NAME, 'index_ci_job_artifact_on_id_convert_to_bigint', 'ci_job_artifacts_pkey'
execute "ALTER TABLE #{TABLE_NAME} ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY USING INDEX ci_job_artifacts_pkey"
# Rename the rest of the indexes (we already hold an exclusive lock, so no need to use DROP INDEX CONCURRENTLY here
execute 'DROP INDEX index_ci_job_artifacts_for_terraform_reports'
rename_index TABLE_NAME, 'index_ci_job_artifacts_for_terraform_reports_bigint', 'index_ci_job_artifacts_for_terraform_reports'
execute 'DROP INDEX index_ci_job_artifacts_id_for_terraform_reports'
rename_index TABLE_NAME, 'index_ci_job_artifacts_id_for_terraform_reports_bigint', 'index_ci_job_artifacts_id_for_terraform_reports'
# Change the name of the temporary FK for project_pages_metadata(artifacts_archive_id) -> id
rename_constraint(:project_pages_metadata, fk_artifacts_archive_id_tmp, fk_artifacts_archive_id)
end
end
end

View File

@ -0,0 +1 @@
33162af4ef99c32d3c5b38479e407d4911a8d3dce53407dbee6e5745c8e945ae

View File

@ -0,0 +1 @@
1bdbcc6ef5ccf7a2bfb1f9571885e218e230a81b632a2d993302bd87432963f3

View File

@ -9624,6 +9624,7 @@ CREATE TABLE application_settings (
usage_ping_features_enabled boolean DEFAULT false NOT NULL,
encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea,
pypi_package_requests_forwarding boolean DEFAULT true NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
@ -10785,7 +10786,7 @@ CREATE SEQUENCE ci_instance_variables_id_seq
ALTER SEQUENCE ci_instance_variables_id_seq OWNED BY ci_instance_variables.id;
CREATE TABLE ci_job_artifacts (
id integer NOT NULL,
id_convert_to_bigint integer DEFAULT 0 NOT NULL,
project_id integer NOT NULL,
job_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
file_type integer NOT NULL,
@ -10798,7 +10799,7 @@ CREATE TABLE ci_job_artifacts (
file_sha256 bytea,
file_format smallint,
file_location smallint,
id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
id bigint NOT NULL,
job_id bigint NOT NULL,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
);

View File

@ -79,6 +79,7 @@ Example response:
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"asset_proxy_allowlist": ["example.com", "*.example.com", "your-instance.com"],
"npm_package_requests_forwarding": true,
"pypi_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300,
"raw_blob_request_limit": 300,
@ -180,6 +181,7 @@ Example response:
"allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false,
"npm_package_requests_forwarding": true,
"pypi_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300,
"raw_blob_request_limit": 300,
@ -347,6 +349,7 @@ listed in the descriptions of the relevant settings.
| `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. |
| `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
| `npm_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
| `pypi_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or IP addresses to which local requests are allowed when local requests for hooks and services are disabled.
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |

View File

@ -262,10 +262,20 @@ To disable it:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand the **Package Registry** section.
1. Uncheck **Enable forwarding of npm package requests to npmjs.org**.
1. Click **Save changes**.
1. Clear the checkbox **Forward npm package requests to the npm Registry if the packages are not found in the GitLab Package Registry**.
1. Select **Save changes**.
![npm package requests forwarding](img/admin_package_registry_npm_package_requests_forward.png)
### PyPI Forwarding **(PREMIUM SELF)**
GitLab administrators can disable the forwarding of PyPI requests to [pypi.org](https://pypi.org/).
To disable it:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand the **Package Registry** section.
1. Clear the checkbox **Forward PyPI package requests to the PyPI Registry if the packages are not found in the GitLab Package Registry**.
1. Select **Save changes**.
### Package file size limits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -328,6 +328,11 @@ more than once, a `400 Bad Request` error occurs.
## Install a PyPI package
In [GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/233413),
when a PyPI package is not found in the Package Registry, the request is forwarded to [pypi.org](https://pypi.org/).
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
### Install from the project level
To install the latest version of a package, use the following command:

View File

@ -5,11 +5,17 @@ module API
module Packages
module DependencyProxyHelpers
REGISTRY_BASE_URLS = {
npm: 'https://registry.npmjs.org/'
npm: 'https://registry.npmjs.org/',
pypi: 'https://pypi.org/simple/'
}.freeze
APPLICATION_SETTING_NAMES = {
npm: 'npm_package_requests_forwarding',
pypi: 'pypi_package_requests_forwarding'
}.freeze
def redirect_registry_request(forward_to_registry, package_type, options)
if forward_to_registry && redirect_registry_request_available?
if forward_to_registry && redirect_registry_request_available?(package_type)
::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward")
redirect(registry_url(package_type, options))
else
@ -25,11 +31,20 @@ module API
case package_type
when :npm
"#{base_url}#{options[:package_name]}"
when :pypi
"#{base_url}#{options[:package_name]}/"
end
end
def redirect_registry_request_available?
::Gitlab::CurrentSettings.current_application_settings.npm_package_requests_forwarding
def redirect_registry_request_available?(package_type)
application_setting_name = APPLICATION_SETTING_NAMES[package_type]
raise ArgumentError, "Can't find application setting for package_type #{package_type}" unless application_setting_name
::Gitlab::CurrentSettings
.current_application_settings
.attributes
.fetch(application_setting_name, false)
end
end
end

View File

@ -10,6 +10,7 @@ module API
helpers ::API::Helpers::PackagesManagerClientsHelpers
helpers ::API::Helpers::RelatedResourcesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
helpers ::API::Helpers::Packages::DependencyProxyHelpers
include ::API::Helpers::Packages::BasicAuthHelpers::Constants
feature_category :package_registry
@ -82,15 +83,20 @@ module API
track_package_event('list_package', :pypi)
packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute!
presenter = ::Packages::Pypi::PackagePresenter.new(packages, group)
packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute
empty_packages = packages.empty?
# Adjusts grape output format
# to be HTML
content_type "text/html; charset=utf-8"
env['api.format'] = :binary
redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
not_found!('Package') if empty_packages
presenter = ::Packages::Pypi::PackagePresenter.new(packages, group)
body presenter.body
# Adjusts grape output format
# to be HTML
content_type "text/html; charset=utf-8"
env['api.format'] = :binary
body presenter.body
end
end
end
end
@ -142,15 +148,20 @@ module API
track_package_event('list_package', :pypi, project: authorized_user_project, namespace: authorized_user_project.namespace)
packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute!
presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute
empty_packages = packages.empty?
# Adjusts grape output format
# to be HTML
content_type "text/html; charset=utf-8"
env['api.format'] = :binary
redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
not_found!('Package') if empty_packages
presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
body presenter.body
# Adjusts grape output format
# to be HTML
content_type "text/html; charset=utf-8"
env['api.format'] = :binary
body presenter.body
end
end
desc 'The PyPi Package upload endpoint' do

View File

@ -4166,7 +4166,7 @@ msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCoun
msgstr[0] ""
msgstr[1] ""
msgid "ApprovalRule|%{scanner} +%{additionalScanners} more"
msgid "ApprovalRule|%{firstLabel} +%{numberOfAdditionalLabels} more"
msgstr ""
msgid "ApprovalRule|Add approvers"
@ -4175,9 +4175,15 @@ msgstr ""
msgid "ApprovalRule|All scanners"
msgstr ""
msgid "ApprovalRule|All severity levels"
msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected security scanners."
msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected severity levels."
msgstr ""
msgid "ApprovalRule|Approval rules"
msgstr ""
@ -4205,6 +4211,9 @@ msgstr ""
msgid "ApprovalRule|Please select at least one security scanner"
msgstr ""
msgid "ApprovalRule|Please select at least one severity level"
msgstr ""
msgid "ApprovalRule|Rule name"
msgstr ""
@ -4217,6 +4226,12 @@ msgstr ""
msgid "ApprovalRule|Select scanners"
msgstr ""
msgid "ApprovalRule|Select severity levels"
msgstr ""
msgid "ApprovalRule|Severity levels"
msgstr ""
msgid "ApprovalRule|Target branch"
msgstr ""
@ -4922,6 +4937,9 @@ msgstr ""
msgid "Automatic deployment rollbacks"
msgstr ""
msgid "Automatic event tracking provides a traceable history for audits."
msgstr ""
msgid "Automatically close associated incident when a recovery alert notification resolves an alert"
msgstr ""
@ -14578,6 +14596,9 @@ msgstr ""
msgid "Format: %{dateFormat}"
msgstr ""
msgid "Forward %{package_type} package requests to the %{registry_type} Registry if the packages are not found in the GitLab Package Registry"
msgstr ""
msgid "Found errors in your %{gitlab_ci_yml}:"
msgstr ""
@ -26774,9 +26795,6 @@ msgstr ""
msgid "Promotions|Add Group Webhooks and GitLab Enterprise Edition."
msgstr ""
msgid "Promotions|Audit Events is a way to keep track of important events that happened in GitLab."
msgstr ""
msgid "Promotions|Better Protected Branches"
msgstr ""
@ -26828,6 +26846,9 @@ msgstr ""
msgid "Promotions|Improve search with Advanced Search and GitLab Enterprise Edition."
msgstr ""
msgid "Promotions|Keep track of events in your project"
msgstr ""
msgid "Promotions|Learn more"
msgstr ""
@ -26867,9 +26888,6 @@ msgstr ""
msgid "Promotions|Track activity with Contribution Analytics."
msgstr ""
msgid "Promotions|Track your project with Audit Events."
msgstr ""
msgid "Promotions|Try it for free"
msgstr ""

View File

@ -14,14 +14,14 @@ RSpec.describe Packages::Pypi::PackagesFinder do
let(:package_name) { package2.name }
describe 'execute!' do
subject { described_class.new(user, scope, package_name: package_name).execute! }
describe 'execute' do
subject { described_class.new(user, scope, package_name: package_name).execute }
shared_examples 'when no package is found' do
context 'non-existing package' do
let(:package_name) { 'none' }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
it { expect(subject).to be_empty }
end
end
@ -29,7 +29,7 @@ RSpec.describe Packages::Pypi::PackagesFinder do
context 'non-existing package' do
let(:package_name) { package2.name.upcase.tr('-', '.') }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
it { expect(subject).to be_empty }
end
end
@ -45,7 +45,7 @@ RSpec.describe Packages::Pypi::PackagesFinder do
context 'within a group' do
let(:scope) { group }
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
it { expect(subject).to be_empty }
context 'user with access to only one project' do
before do

View File

@ -1,93 +1,175 @@
import { GlAlert } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
import {
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
} from '~/content_editor/constants';
import { emitEditorEvent } from '../test_utils';
jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
let editor;
let contentEditor;
let renderMarkdown;
const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const findEditorContent = () => wrapper.findComponent(EditorContent);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
const createWrapper = async (contentEditor) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
contentEditor,
renderMarkdown,
uploadsPath,
...propsData,
},
stubs: {
EditorStateObserver,
ContentEditorProvider,
},
listeners: {
initialized(editor) {
contentEditor = editor;
},
},
});
};
beforeEach(() => {
editor = createContentEditor({ renderMarkdown: () => true });
});
afterEach(() => {
wrapper.destroy();
});
it('renders editor content component and attaches editor instance', () => {
createWrapper(editor);
it('triggers initialized event and provides contentEditor instance as event data', () => {
createWrapper();
const editorContent = wrapper.findComponent(EditorContent);
expect(contentEditor).not.toBeFalsy();
});
expect(editorContent.props().editor).toBe(editor.tiptapEditor);
it('renders EditorContent component and provides tiptapEditor instance', () => {
createWrapper();
const editorContent = findEditorContent();
expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor);
expect(editorContent.classes()).toContain('md');
});
it('renders ContentEditorProvider component', () => {
createWrapper();
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
it('renders top toolbar component', () => {
createWrapper(editor);
createWrapper();
expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
});
it.each`
isFocused | classes
${true} | ${['md-area', 'is-focused']}
${false} | ${['md-area']}
`(
'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
({ isFocused, classes }) => {
editor.tiptapEditor.isFocused = isFocused;
createWrapper(editor);
it('adds is-focused class when focus event is emitted', async () => {
createWrapper();
expect(findEditorElement().classes()).toStrictEqual(classes);
},
);
it('adds isFocused class when tiptapEditor is focused', () => {
editor.tiptapEditor.isFocused = true;
createWrapper(editor);
await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
expect(findEditorElement().classes()).toContain('is-focused');
});
describe('displaying error', () => {
const error = 'Content Editor error';
it('removes is-focused class when blur event is emitted', async () => {
createWrapper();
await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' });
expect(findEditorElement().classes()).not.toContain('is-focused');
});
it('emits change event when document is updated', async () => {
createWrapper();
await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' });
expect(wrapper.emitted('change')).toEqual([
[
{
empty: contentEditor.empty,
},
],
]);
});
it('renders content_editor_error component', () => {
createWrapper();
expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true);
});
describe('when loading content', () => {
beforeEach(async () => {
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
await nextTick();
});
it('displays loading indicator', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('hides EditorContent component', () => {
expect(findEditorContent().exists()).toBe(false);
});
});
describe('when loading content succeeds', () => {
beforeEach(async () => {
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
await nextTick();
contentEditor.emit(LOADING_SUCCESS_EVENT);
await nextTick();
});
it('hides loading indicator', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('displays EditorContent component', () => {
expect(findEditorContent().exists()).toBe(true);
});
});
describe('when loading content fails', () => {
const error = 'error';
beforeEach(async () => {
createWrapper(editor);
editor.tiptapEditor.emit('error', error);
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
await nextTick();
contentEditor.emit(LOADING_ERROR_EVENT, error);
await nextTick();
});
it('displays error notifications from the tiptap editor', () => {
expect(findErrorAlert().text()).toBe(error);
it('hides loading indicator', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('allows dismissing an error alert', async () => {
findErrorAlert().vm.$emit('dismiss');
await nextTick();
expect(findErrorAlert().exists()).toBe(false);
it('displays EditorContent component', () => {
expect(findEditorContent().exists()).toBe(true);
});
});
});

View File

@ -10,7 +10,7 @@ import httpStatus from '~/lib/utils/http_status';
import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/extensions/image', () => {
describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
let eq;
let doc;
@ -144,8 +144,8 @@ describe('content_editor/extensions/image', () => {
it('emits an error event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
tiptapEditor.on('error', (message) => {
expect(message).toBe('An error occurred while uploading the image. Please try again.');
tiptapEditor.on('error', ({ error }) => {
expect(error).toBe('An error occurred while uploading the image. Please try again.');
done();
});
});
@ -224,8 +224,8 @@ describe('content_editor/extensions/image', () => {
it('emits an error event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
tiptapEditor.on('error', (message) => {
expect(message).toBe('An error occurred while uploading the file. Please try again.');
tiptapEditor.on('error', ({ error }) => {
expect(error).toBe('An error occurred while uploading the file. Please try again.');
done();
});
});

View File

@ -13,10 +13,22 @@ describe('content_editor/services/content_editor', () => {
beforeEach(() => {
const tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'destroy');
serializer = { deserialize: jest.fn() };
contentEditor = new ContentEditor({ tiptapEditor, serializer });
});
describe('.dispose', () => {
it('destroys the tiptapEditor', () => {
expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled();
contentEditor.dispose();
expect(contentEditor.tiptapEditor.destroy).toHaveBeenCalled();
});
});
describe('when setSerializedContent succeeds', () => {
beforeEach(() => {
serializer.deserialize.mockResolvedValueOnce('');

View File

@ -352,11 +352,6 @@ describe('WikiForm', () => {
await waitForPromises();
});
it('editor is shown in a perpetual loading state', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
});
it('disables the submit button', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
let_it_be(:helper) { Class.new.include(described_class).new }
describe 'redirect_registry_request' do
describe '#redirect_registry_request' do
using RSpec::Parameterized::TableSyntax
let(:options) { {} }
@ -13,7 +13,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } }
before do
allow(helper).to receive(:options).and_return(for: API::NpmInstancePackages)
allow(helper).to receive(:options).and_return(for: described_class)
end
shared_examples 'executing fallback' do
@ -34,38 +34,66 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject
expect_snowplow_event(category: 'API::NpmInstancePackages', action: 'npm_request_forward')
expect_snowplow_event(category: described_class.to_s, action: "#{package_type}_request_forward")
end
end
context 'with npm packages' do
let(:package_type) { :npm }
%i[npm pypi].each do |forwardable_package_type|
context "with #{forwardable_package_type} packages" do
include_context 'dependency proxy helpers context'
where(:application_setting, :forward_to_registry, :example_name) do
true | true | 'executing redirect'
true | false | 'executing fallback'
false | true | 'executing fallback'
false | false | 'executing fallback'
end
let(:package_type) { forwardable_package_type }
with_them do
before do
stub_application_setting(npm_package_requests_forwarding: application_setting)
where(:application_setting, :forward_to_registry, :example_name) do
true | true | 'executing redirect'
true | false | 'executing fallback'
false | true | 'executing fallback'
false | false | 'executing fallback'
end
it_behaves_like params[:example_name]
with_them do
before do
allow_fetch_application_setting(attribute: "#{forwardable_package_type}_package_requests_forwarding", return_value: application_setting)
end
it_behaves_like params[:example_name]
end
end
end
context 'with non-forwardable packages' do
context 'with non-forwardable package type' do
let(:forward_to_registry) { true }
before do
stub_application_setting(npm_package_requests_forwarding: true)
stub_application_setting(pypi_package_requests_forwarding: true)
end
Packages::Package.package_types.keys.without('npm').each do |pkg_type|
Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
context "#{pkg_type}" do
let(:package_type) { pkg_type.to_sym }
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError, "Can't find application setting for package_type #{package_type}")
end
end
end
end
describe '#registry_url' do
subject { helper.registry_url(package_type, package_name: 'test') }
where(:package_type, :expected_result) do
:npm | 'https://registry.npmjs.org/test'
:pypi | 'https://pypi.org/simple/test/'
end
with_them do
it { is_expected.to eq(expected_result) }
end
Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
context "with non-forwardable package type #{pkg_type}" do
let(:package_type) { pkg_type }
it 'raises an error' do

View File

@ -23,7 +23,8 @@ RSpec.describe API::PypiPackages do
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package.name}" }
let(:package_name) { package.name }
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" }
let(:snowplow_gitlab_standard_context) { {} }
it_behaves_like 'pypi simple API endpoint'
@ -40,7 +41,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package GET requests'
context 'with group path as id' do
let(:url) { "/groups/#{CGI.escape(group.full_path)}/-/packages/pypi/simple/#{package.name}" }
let(:url) { "/groups/#{CGI.escape(group.full_path)}/-/packages/pypi/simple/#{package_name}"}
it_behaves_like 'deploy token for package GET requests'
end
@ -60,7 +61,8 @@ RSpec.describe API::PypiPackages do
end
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package.name}" }
let(:package_name) { package.name }
let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
it_behaves_like 'pypi simple API endpoint'

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
RSpec.shared_context 'dependency proxy helpers context' do
def allow_fetch_application_setting(attribute:, return_value:)
attributes = double
allow(::Gitlab::CurrentSettings.current_application_settings).to receive(:attributes).and_return(attributes)
allow(attributes).to receive(:fetch).with(attribute, false).and_return(return_value)
end
end

View File

@ -46,6 +46,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
shared_examples 'handling all conditions' do
include_context 'dependency proxy helpers context'
where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
nil | :scoped_naming_convention | true | :public | nil | :accept | :ok
nil | :scoped_naming_convention | false | :public | nil | :accept | :ok
@ -243,7 +245,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
package.update!(name: package_name) unless package_name == 'non-existing-package'
stub_application_setting(npm_package_requests_forwarding: request_forward)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
end
example_name = "#{params[:expected_result]} metadata request"

View File

@ -10,9 +10,10 @@ end
RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax
include_context 'dependency proxy helpers context'
before do
stub_application_setting(npm_package_requests_forwarding: false)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: false)
end
context 'with valid package name' do

View File

@ -228,6 +228,35 @@ RSpec.shared_examples 'pypi simple API endpoint' do
it_behaves_like 'PyPI package versions', :developer, :success
end
context 'package request forward' do
include_context 'dependency proxy helpers context'
where(:forward, :package_in_project, :shared_examples_name, :expected_status) do
true | true | 'PyPI package versions' | :success
true | false | 'process PyPI api request' | :redirect
false | true | 'PyPI package versions' | :success
false | false | 'process PyPI api request' | :not_found
end
with_them do
let_it_be(:package) { create(:pypi_package, project: project, name: 'foobar') }
let(:package_name) do
if package_in_project
'foobar'
else
'barfoo'
end
end
before do
allow_fetch_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward)
end
it_behaves_like params[:shared_examples_name], :reporter, params[:expected_status]
end
end
end
RSpec.shared_examples 'pypi file download endpoint' do