Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bf1990164b
commit
8cdf31a1f9
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
33162af4ef99c32d3c5b38479e407d4911a8d3dce53407dbee6e5745c8e945ae
|
|
@ -0,0 +1 @@
|
|||
1bdbcc6ef5ccf7a2bfb1f9571885e218e230a81b632a2d993302bd87432963f3
|
|
@ -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))
|
||||
);
|
||||
|
|
|
@ -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`. |
|
||||
|
|
|
@ -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 |
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue