Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d7ed3b4766
commit
3902d464d6
42 changed files with 944 additions and 523 deletions
|
@ -130,10 +130,13 @@ export default class MilestoneSelect {
|
|||
fieldName: $dropdown.data('fieldName'),
|
||||
text: milestone => escape(milestone.title),
|
||||
id: milestone => {
|
||||
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
|
||||
return milestone.name;
|
||||
if (milestone !== undefined) {
|
||||
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
|
||||
return milestone.name;
|
||||
}
|
||||
|
||||
return milestone.id;
|
||||
}
|
||||
return milestone.id;
|
||||
},
|
||||
hidden: () => {
|
||||
$selectBox.hide();
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
<template>
|
||||
<div class="d-flex flex-grow-1 flex-column h-100">
|
||||
<edit-header class="py-2" :title="title" />
|
||||
<rich-content-editor v-model="editableContent" class="mb-9" />
|
||||
<rich-content-editor v-model="editableContent" class="mb-9 h-100" />
|
||||
<publish-toolbar
|
||||
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
|
||||
:return-url="returnUrl"
|
||||
|
|
|
@ -158,7 +158,7 @@ export default {
|
|||
<div class="d-inline-block ml-md-2 ml-0">
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="* "
|
||||
tag="- "
|
||||
:button-title="__('Add a bullet list')"
|
||||
icon="list-bulleted"
|
||||
/>
|
||||
|
@ -170,7 +170,7 @@ export default {
|
|||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="* [ ] "
|
||||
tag="- [ ] "
|
||||
:button-title="__('Add a task list')"
|
||||
icon="list-task"
|
||||
/>
|
||||
|
|
|
@ -24,6 +24,7 @@ const TOOLBAR_ITEM_CONFIGS = [
|
|||
{ isDivider: true },
|
||||
{ icon: 'dash', command: 'HR', tooltip: __('Add a line') },
|
||||
{ icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
|
||||
{ icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
|
||||
{ isDivider: true },
|
||||
{ icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
|
||||
];
|
||||
|
|
|
@ -34,3 +34,10 @@ export const addCustomEventListener = (editorInstance, event, handler) => {
|
|||
editorInstance.eventManager.addEventType(event);
|
||||
editorInstance.eventManager.listen(event, handler);
|
||||
};
|
||||
|
||||
export const removeCustomEventListener = (editorInstance, event, handler) =>
|
||||
editorInstance.eventManager.removeEventHandler(event, handler);
|
||||
|
||||
export const addImage = ({ editor }, image) => editor.exec('AddImage', image);
|
||||
|
||||
export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown');
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
import { GlModal, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
imageUrl: null,
|
||||
altText: null,
|
||||
modalTitle: __('Image Details'),
|
||||
okTitle: __('Insert'),
|
||||
urlLabel: __('Image URL'),
|
||||
descriptionLabel: __('Description'),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.error = null;
|
||||
this.imageUrl = null;
|
||||
this.altText = null;
|
||||
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
onOk(event) {
|
||||
if (!this.isValid()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageUrl, altText } = this;
|
||||
|
||||
this.$emit('addImage', { imageUrl, altText: altText || __('image') });
|
||||
},
|
||||
isValid() {
|
||||
if (!isSafeURL(this.imageUrl)) {
|
||||
this.error = __('Please provide a valid URL');
|
||||
this.$refs.urlInput.$el.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
modal-id="add-image-modal"
|
||||
:title="modalTitle"
|
||||
:ok-title="okTitle"
|
||||
@ok="onOk"
|
||||
>
|
||||
<gl-form-group
|
||||
:label="urlLabel"
|
||||
label-for="url-input"
|
||||
:state="!Boolean(error)"
|
||||
:invalid-feedback="error"
|
||||
>
|
||||
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group :label="descriptionLabel" label-for="description-input">
|
||||
<gl-form-input id="description-input" ref="descriptionInput" v-model="altText" />
|
||||
</gl-form-group>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -2,6 +2,7 @@
|
|||
import 'codemirror/lib/codemirror.css';
|
||||
import '@toast-ui/editor/dist/toastui-editor.css';
|
||||
|
||||
import AddImageModal from './modals/add_image_modal.vue';
|
||||
import {
|
||||
EDITOR_OPTIONS,
|
||||
EDITOR_TYPES,
|
||||
|
@ -10,7 +11,12 @@ import {
|
|||
CUSTOM_EVENTS,
|
||||
} from './constants';
|
||||
|
||||
import { addCustomEventListener } from './editor_service';
|
||||
import {
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
addImage,
|
||||
getMarkdown,
|
||||
} from './editor_service';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -18,6 +24,7 @@ export default {
|
|||
import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
|
||||
toast => toast.Editor,
|
||||
),
|
||||
AddImageModal,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
|
@ -49,13 +56,20 @@ export default {
|
|||
editorOptions() {
|
||||
return { ...EDITOR_OPTIONS, ...this.options };
|
||||
},
|
||||
editorInstance() {
|
||||
return this.$refs.editor;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
removeCustomEventListener(
|
||||
this.editorInstance,
|
||||
CUSTOM_EVENTS.openAddImageModal,
|
||||
this.onOpenAddImageModal,
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
onContentChanged() {
|
||||
this.$emit('input', this.getMarkdown());
|
||||
},
|
||||
getMarkdown() {
|
||||
return this.$refs.editor.invoke('getMarkdown');
|
||||
this.$emit('input', getMarkdown(this.editorInstance));
|
||||
},
|
||||
onLoad(editorInstance) {
|
||||
addCustomEventListener(
|
||||
|
@ -65,20 +79,26 @@ export default {
|
|||
);
|
||||
},
|
||||
onOpenAddImageModal() {
|
||||
// TODO - add image modal (next MR)
|
||||
this.$refs.addImageModal.show();
|
||||
},
|
||||
onAddImage(image) {
|
||||
addImage(this.editorInstance, image);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<toast-editor
|
||||
ref="editor"
|
||||
:initial-value="value"
|
||||
:options="editorOptions"
|
||||
:preview-style="previewStyle"
|
||||
:initial-edit-type="initialEditType"
|
||||
:height="height"
|
||||
@change="onContentChanged"
|
||||
@load="onLoad"
|
||||
/>
|
||||
<div>
|
||||
<toast-editor
|
||||
ref="editor"
|
||||
:initial-value="value"
|
||||
:options="editorOptions"
|
||||
:preview-style="previewStyle"
|
||||
:initial-edit-type="initialEditType"
|
||||
:height="height"
|
||||
@change="onContentChanged"
|
||||
@load="onLoad"
|
||||
/>
|
||||
<add-image-modal ref="addImageModal" @addImage="onAddImage" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,7 +15,7 @@ module ImportState
|
|||
def refresh_jid_expiration
|
||||
return unless jid
|
||||
|
||||
Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
|
||||
Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
|
||||
end
|
||||
|
||||
def self.jid_by(project_id:, status:)
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") })
|
||||
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") })
|
||||
= markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") })
|
||||
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: _("Add a bullet list") })
|
||||
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
|
||||
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
|
||||
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: _("Add a task list") })
|
||||
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a task list") })
|
||||
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
|
||||
- if show_fullscreen_button
|
||||
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
|
||||
|
|
|
@ -6,7 +6,7 @@ module ProjectImportOptions
|
|||
IMPORT_RETRY_COUNT = 5
|
||||
|
||||
included do
|
||||
sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
|
||||
sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
|
||||
|
||||
# We only want to mark the project as failed once we exhausted all retries
|
||||
sidekiq_retries_exhausted do |job|
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
class StuckImportJobsWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include Gitlab::Import::StuckImportJob
|
||||
|
||||
IMPORT_JOBS_EXPIRATION = Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
|
||||
|
||||
private
|
||||
|
||||
def track_metrics(with_jid_count, without_jid_count)
|
||||
|
|
5
changelogs/unreleased/216640-insert-image-modal.yml
Normal file
5
changelogs/unreleased/216640-insert-image-modal.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to insert an image via SSE
|
||||
merge_request: 33029
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index on id and type for Snippets
|
||||
merge_request: 32885
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add support for artifacts/exclude configuration
|
||||
merge_request: 33170
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/markdown-toolbar-list-style.yml
Normal file
5
changelogs/unreleased/markdown-toolbar-list-style.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Set markdown toolbar to use hyphens for lists
|
||||
merge_request: 31426
|
||||
author:
|
||||
type: changed
|
|
@ -65,7 +65,23 @@ module.exports = {
|
|||
}),
|
||||
new YarnCheck({
|
||||
rootDirectory: ROOT_PATH,
|
||||
exclude: /ts-jest/,
|
||||
exclude: new RegExp(
|
||||
[
|
||||
/*
|
||||
chokidar has a newer version which do not depend on fsevents,
|
||||
is faster and only compatible with newer node versions (>=8)
|
||||
|
||||
Their actual interface remains the same and we can safely _force_
|
||||
newer versions to get performance and security benefits.
|
||||
|
||||
This can be removed once all dependencies are up to date:
|
||||
https://gitlab.com/gitlab-org/gitlab/-/issues/219353
|
||||
*/
|
||||
'chokidar',
|
||||
// We are ignoring ts-jest, because we force a newer version, compatible with our current jest version
|
||||
'ts-jest',
|
||||
].join('|'),
|
||||
),
|
||||
forceKill: true,
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnSnippetTypeAndId < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :snippets, [:id, :type]
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :snippets, [:id, :type]
|
||||
end
|
||||
end
|
|
@ -10745,6 +10745,8 @@ CREATE INDEX index_snippets_on_description_trigram ON public.snippets USING gin
|
|||
|
||||
CREATE INDEX index_snippets_on_file_name_trigram ON public.snippets USING gin (file_name public.gin_trgm_ops);
|
||||
|
||||
CREATE INDEX index_snippets_on_id_and_type ON public.snippets USING btree (id, type);
|
||||
|
||||
CREATE INDEX index_snippets_on_project_id_and_visibility_level ON public.snippets USING btree (project_id, visibility_level);
|
||||
|
||||
CREATE INDEX index_snippets_on_title_trigram ON public.snippets USING gin (title public.gin_trgm_ops);
|
||||
|
@ -14025,6 +14027,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200521225327
|
||||
20200521225337
|
||||
20200521225346
|
||||
20200522235146
|
||||
20200525114553
|
||||
20200525121014
|
||||
20200526000407
|
||||
|
|
|
@ -117,7 +117,7 @@ The following table lists available parameters for jobs:
|
|||
| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
|
@ -2555,6 +2555,33 @@ job:
|
|||
- path/*xyz/*
|
||||
```
|
||||
|
||||
#### `artifacts:exclude`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15122) in GitLab 13.1
|
||||
> - Requires GitLab Runner 13.1
|
||||
|
||||
`exclude` makes it possible to prevent files from being added to an artifacts
|
||||
archive.
|
||||
|
||||
Similar to [`artifacts:paths`](#artifactspaths), `exclude` paths are relative
|
||||
to the project directory. Wildcards can be used that follow the
|
||||
[glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns and
|
||||
[`filepath.Match`](https://golang.org/pkg/path/filepath/#Match).
|
||||
|
||||
For example, to store all files in `binaries/`, but not `*.o` files located in
|
||||
subdirectories of `binaries/`:
|
||||
|
||||
```yaml
|
||||
artifacts:
|
||||
paths:
|
||||
- binaries/
|
||||
exclude:
|
||||
- binaries/**/*.o
|
||||
```
|
||||
|
||||
Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
|
||||
`artifacts:exclude` too.
|
||||
|
||||
#### `artifacts:expose_as`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15018) in GitLab 12.5.
|
||||
|
@ -2701,6 +2728,15 @@ artifacts:
|
|||
- binaries/
|
||||
```
|
||||
|
||||
Send all untracked files but [exclude](#artifactsexclude) `*.txt`:
|
||||
|
||||
```yaml
|
||||
artifacts:
|
||||
untracked: true
|
||||
exclude:
|
||||
- *.txt
|
||||
```
|
||||
|
||||
#### `artifacts:when`
|
||||
|
||||
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
#
|
||||
module Features
|
||||
def self.artifacts_exclude_enabled?
|
||||
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: false)
|
||||
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: true)
|
||||
end
|
||||
|
||||
def self.ensure_scheduling_type_enabled?
|
||||
|
|
|
@ -14,7 +14,7 @@ module Gitlab
|
|||
jid = generate_jid(import_state)
|
||||
|
||||
Gitlab::SidekiqStatus
|
||||
.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
|
||||
.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
|
||||
|
||||
import_state.update_column(:jid, jid)
|
||||
end
|
||||
|
|
2
lib/gitlab/phabricator_import/cache/map.rb
vendored
2
lib/gitlab/phabricator_import/cache/map.rb
vendored
|
@ -63,7 +63,7 @@ module Gitlab
|
|||
def timeout
|
||||
# Setting the timeout to the same one as we do for clearing stuck jobs
|
||||
# this makes sure all cache is available while the import is running.
|
||||
StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
|
||||
Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,7 +40,7 @@ module Gitlab
|
|||
def timeout
|
||||
# Make sure we get rid of all the information after a job is marked
|
||||
# as failed/succeeded
|
||||
StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
|
||||
Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -133,6 +133,8 @@ module Gitlab
|
|||
releases: count(Release),
|
||||
remote_mirrors: count(RemoteMirror),
|
||||
snippets: count(Snippet),
|
||||
personal_snippets: count(PersonalSnippet),
|
||||
project_snippets: count(ProjectSnippet),
|
||||
suggestions: count(Suggestion),
|
||||
terraform_reports: count(::Ci::JobArtifact.terraform_reports),
|
||||
terraform_states: count(::Terraform::State),
|
||||
|
|
|
@ -11600,6 +11600,12 @@ msgstr ""
|
|||
msgid "Ignored"
|
||||
msgstr ""
|
||||
|
||||
msgid "Image Details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Image URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Image: %{image}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11858,12 +11864,18 @@ msgstr ""
|
|||
msgid "Input your repository URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert a code block"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert a quote"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert an image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert code"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16038,6 +16050,9 @@ msgstr ""
|
|||
msgid "Please provide a name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid email address."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26260,6 +26275,9 @@ msgstr ""
|
|||
msgid "https://your-bitbucket-server"
|
||||
msgstr ""
|
||||
|
||||
msgid "image"
|
||||
msgstr ""
|
||||
|
||||
msgid "image diff"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@
|
|||
"markdownlint-cli": "0.18.0",
|
||||
"md5": "^2.2.1",
|
||||
"node-sass": "^4.12.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"nodemon": "^2.0.4",
|
||||
"pixelmatch": "^4.0.2",
|
||||
"postcss": "^7.0.14",
|
||||
"prettier": "1.18.2",
|
||||
|
@ -212,8 +212,9 @@
|
|||
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.md#bootstrapvue"
|
||||
},
|
||||
"resolutions": {
|
||||
"vue-jest/ts-jest": "24.0.0",
|
||||
"monaco-editor": "0.18.1"
|
||||
"chokidar": "^3.4.0",
|
||||
"monaco-editor": "0.18.1",
|
||||
"vue-jest/ts-jest": "24.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0",
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
FROM ruby:2.6-stretch
|
||||
LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
ENV DOCKER_VERSION="17.09.0-ce"
|
||||
ENV CHROME_VERSION="83.0.4103.61-1"
|
||||
ENV CHROME_DRIVER_VERSION="83.0.4103.39"
|
||||
ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}"
|
||||
ENV K3D_VERSION="1.3.4"
|
||||
|
||||
##
|
||||
# Add support for stretch-backports
|
||||
|
@ -21,28 +28,31 @@ RUN apt-get -y -t stretch-backports install git git-lfs
|
|||
##
|
||||
# Install Docker
|
||||
#
|
||||
RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
|
||||
tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
|
||||
rm docker-17.09.0-ce.tgz
|
||||
RUN wget -q "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" && \
|
||||
tar -zxf "docker-${DOCKER_VERSION}.tgz" && mv docker/docker /usr/local/bin/docker && \
|
||||
rm "docker-${DOCKER_VERSION}.tgz"
|
||||
|
||||
##
|
||||
# Install Google Chrome version with headless support
|
||||
# Download from our local S3 bucket, populated by https://gitlab.com/gitlab-org/gitlab-build-images/-/blob/master/scripts/cache-google-chrome
|
||||
#
|
||||
RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
|
||||
RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clean
|
||||
RUN curl --silent --show-error --fail -O "${CHROME_URL}" && \
|
||||
dpkg -i "./${CHROME_DEB}" || true && \
|
||||
apt-get install -f -y && \
|
||||
rm -f "./${CHROME_DEB}"
|
||||
|
||||
##
|
||||
# Install chromedriver to make it work with Selenium
|
||||
#
|
||||
RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip
|
||||
RUN wget -q "https://chromedriver.storage.googleapis.com/${CHROME_DRIVER_VERSION}/chromedriver_linux64.zip"
|
||||
RUN unzip chromedriver_linux64.zip -d /usr/local/bin
|
||||
RUN rm -f chromedriver_linux64.zip
|
||||
|
||||
##
|
||||
# Install K3d local cluster support
|
||||
# https://github.com/rancher/k3d
|
||||
#
|
||||
RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG=v1.3.4 bash
|
||||
RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG="v${K3D_VERSION}" bash
|
||||
|
||||
##
|
||||
# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s
|
||||
|
|
1
qa/qa.rb
1
qa/qa.rb
|
@ -43,6 +43,7 @@ module QA
|
|||
|
||||
module API
|
||||
autoload :Client, 'qa/runtime/api/client'
|
||||
autoload :RepositoryStorageMoves, 'qa/runtime/api/repository_storage_moves'
|
||||
autoload :Request, 'qa/runtime/api/request'
|
||||
end
|
||||
|
||||
|
|
|
@ -91,6 +91,10 @@ module QA
|
|||
super
|
||||
end
|
||||
|
||||
def has_file?(file_path)
|
||||
repository_tree.any? { |file| file[:path] == file_path }
|
||||
end
|
||||
|
||||
def api_get_path
|
||||
"/projects/#{CGI.escape(path_with_namespace)}"
|
||||
end
|
||||
|
@ -115,6 +119,10 @@ module QA
|
|||
"#{api_get_path}/repository/branches"
|
||||
end
|
||||
|
||||
def api_repository_tree_path
|
||||
"#{api_get_path}/repository/tree"
|
||||
end
|
||||
|
||||
def api_pipelines_path
|
||||
"#{api_get_path}/pipelines"
|
||||
end
|
||||
|
@ -155,11 +163,9 @@ module QA
|
|||
raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
|
||||
end
|
||||
|
||||
wait_until do
|
||||
reload!
|
||||
|
||||
api_response[:repository_storage] == new_storage
|
||||
end
|
||||
wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished') }
|
||||
rescue Support::Repeater::RepeaterConditionExceededError
|
||||
raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the repository storage move to finish'
|
||||
end
|
||||
|
||||
def import_status
|
||||
|
@ -187,8 +193,11 @@ module QA
|
|||
end
|
||||
|
||||
def repository_branches
|
||||
response = get Runtime::API::Request.new(api_client, api_repository_branches_path).url
|
||||
parse_body(response)
|
||||
parse_body(get(Runtime::API::Request.new(api_client, api_repository_branches_path).url))
|
||||
end
|
||||
|
||||
def repository_tree
|
||||
parse_body(get(Runtime::API::Request.new(api_client, api_repository_tree_path).url))
|
||||
end
|
||||
|
||||
def pipelines
|
||||
|
|
33
qa/qa/runtime/api/repository_storage_moves.rb
Normal file
33
qa/qa/runtime/api/repository_storage_moves.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Runtime
|
||||
module API
|
||||
module RepositoryStorageMoves
|
||||
extend self
|
||||
extend Support::Api
|
||||
|
||||
RepositoryStorageMovesError = Class.new(RuntimeError)
|
||||
|
||||
def has_status?(project, status)
|
||||
all.any? do |move|
|
||||
move[:project][:path_with_namespace] == project.path_with_namespace &&
|
||||
move[:state] == status &&
|
||||
move[:destination_storage_name] == Env.additional_repository_storage
|
||||
end
|
||||
end
|
||||
|
||||
def all
|
||||
Logger.debug('Getting repository storage moves')
|
||||
parse_body(get(Request.new(api_client, '/project_repository_storage_moves').url))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_client
|
||||
@api_client ||= Client.as_admin
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create' do
|
||||
describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'repo-storage-status'
|
||||
project.initialize_with_readme = true
|
||||
end
|
||||
end
|
||||
|
||||
it 'confirms a `finished` status after moving project repository storage' do
|
||||
expect(project).to have_file('README.md')
|
||||
|
||||
project.change_repository_storage(QA::Runtime::Env.additional_repository_storage)
|
||||
|
||||
expect(Runtime::API::RepositoryStorageMoves).to have_status(project, 'finished')
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.file_name = 'new_file'
|
||||
push.file_content = '# This is a new file'
|
||||
push.commit_message = 'Add new file'
|
||||
push.new_branch = false
|
||||
end
|
||||
|
||||
expect(project).to have_file('README.md')
|
||||
expect(project).to have_file('new_file')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,8 +7,9 @@ module QA
|
|||
module Repeater
|
||||
DEFAULT_MAX_WAIT_TIME = 60
|
||||
|
||||
RetriesExceededError = Class.new(RuntimeError)
|
||||
WaitExceededError = Class.new(RuntimeError)
|
||||
RepeaterConditionExceededError = Class.new(RuntimeError)
|
||||
RetriesExceededError = Class.new(RepeaterConditionExceededError)
|
||||
WaitExceededError = Class.new(RepeaterConditionExceededError)
|
||||
|
||||
def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
|
||||
attempts = 0
|
||||
|
|
|
@ -25,13 +25,13 @@ describe('init markdown', () => {
|
|||
insertMarkdownText({
|
||||
textArea,
|
||||
text: textArea.value,
|
||||
tag: '* ',
|
||||
tag: '- ',
|
||||
blockTag: null,
|
||||
selected: '',
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
expect(textArea.value).toEqual(`${initialValue}- `);
|
||||
});
|
||||
|
||||
it('inserts the tag on a new line if the current one is not empty', () => {
|
||||
|
@ -43,13 +43,13 @@ describe('init markdown', () => {
|
|||
insertMarkdownText({
|
||||
textArea,
|
||||
text: textArea.value,
|
||||
tag: '* ',
|
||||
tag: '- ',
|
||||
blockTag: null,
|
||||
selected: '',
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}\n* `);
|
||||
expect(textArea.value).toEqual(`${initialValue}\n- `);
|
||||
});
|
||||
|
||||
it('inserts the tag on the same line if the current line only contains spaces', () => {
|
||||
|
@ -61,13 +61,13 @@ describe('init markdown', () => {
|
|||
insertMarkdownText({
|
||||
textArea,
|
||||
text: textArea.value,
|
||||
tag: '* ',
|
||||
tag: '- ',
|
||||
blockTag: null,
|
||||
selected: '',
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
expect(textArea.value).toEqual(`${initialValue}- `);
|
||||
});
|
||||
|
||||
it('inserts the tag on the same line if the current line only contains tabs', () => {
|
||||
|
@ -79,13 +79,13 @@ describe('init markdown', () => {
|
|||
insertMarkdownText({
|
||||
textArea,
|
||||
text: textArea.value,
|
||||
tag: '* ',
|
||||
tag: '- ',
|
||||
blockTag: null,
|
||||
selected: '',
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
expect(textArea.value).toEqual(`${initialValue}- `);
|
||||
});
|
||||
|
||||
it('places the cursor inside the tags', () => {
|
||||
|
|
|
@ -185,7 +185,7 @@ describe('Markdown field component', () => {
|
|||
markdownButton.trigger('click');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(textarea.value).toContain('* testing');
|
||||
expect(textarea.value).toContain('- testing');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -197,7 +197,7 @@ describe('Markdown field component', () => {
|
|||
markdownButton.trigger('click');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(textarea.value).toContain('* testing\n* 123');
|
||||
expect(textarea.value).toContain('- testing\n- 123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import {
|
||||
generateToolbarItem,
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
addImage,
|
||||
getMarkdown,
|
||||
} from '~/vue_shared/components/rich_content_editor/editor_service';
|
||||
|
||||
describe('Editor Service', () => {
|
||||
const mockInstance = {
|
||||
eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
|
||||
editor: { exec: jest.fn() },
|
||||
invoke: jest.fn(),
|
||||
};
|
||||
const event = 'someCustomEvent';
|
||||
const handler = jest.fn();
|
||||
|
||||
describe('generateToolbarItem', () => {
|
||||
const config = {
|
||||
icon: 'bold',
|
||||
|
@ -11,6 +22,7 @@ describe('Editor Service', () => {
|
|||
tooltip: 'Some Tooltip',
|
||||
event: 'some-event',
|
||||
};
|
||||
|
||||
const generatedItem = generateToolbarItem(config);
|
||||
|
||||
it('generates the correct command', () => {
|
||||
|
@ -33,10 +45,6 @@ describe('Editor Service', () => {
|
|||
});
|
||||
|
||||
describe('addCustomEventListener', () => {
|
||||
const mockInstance = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
|
||||
const event = 'someCustomEvent';
|
||||
const handler = jest.fn();
|
||||
|
||||
it('registers an event type on the instance and adds an event handler', () => {
|
||||
addCustomEventListener(mockInstance, event, handler);
|
||||
|
||||
|
@ -44,4 +52,30 @@ describe('Editor Service', () => {
|
|||
expect(mockInstance.eventManager.listen).toHaveBeenCalledWith(event, handler);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeCustomEventListener', () => {
|
||||
it('removes an event handler from the instance', () => {
|
||||
removeCustomEventListener(mockInstance, event, handler);
|
||||
|
||||
expect(mockInstance.eventManager.removeEventHandler).toHaveBeenCalledWith(event, handler);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addImage', () => {
|
||||
it('calls the exec method on the instance', () => {
|
||||
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
|
||||
|
||||
addImage(mockInstance, mockImage);
|
||||
|
||||
expect(mockInstance.editor.exec).toHaveBeenCalledWith('AddImage', mockImage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMarkdown', () => {
|
||||
it('calls the invoke method on the instance', () => {
|
||||
getMarkdown(mockInstance);
|
||||
|
||||
expect(mockInstance.invoke).toHaveBeenCalledWith('getMarkdown');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
|
||||
|
||||
describe('Add Image Modal', () => {
|
||||
let wrapper;
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
|
||||
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(AddImageModal);
|
||||
});
|
||||
|
||||
describe('when content is loaded', () => {
|
||||
it('renders a modal component', () => {
|
||||
expect(findModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an input to add an image URL', () => {
|
||||
expect(findUrlInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an input to add an image description', () => {
|
||||
expect(findDescriptionInput().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add image', () => {
|
||||
it('emits an addImage event when a valid URL is specified', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const mockImage = { imageUrl: '/some/valid/url.png', altText: 'some description' };
|
||||
wrapper.setData({ ...mockImage });
|
||||
|
||||
findModal().vm.$emit('ok', { preventDefault });
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted('addImage')).toEqual([[mockImage]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
|
||||
import {
|
||||
EDITOR_OPTIONS,
|
||||
EDITOR_TYPES,
|
||||
|
@ -8,11 +9,17 @@ import {
|
|||
CUSTOM_EVENTS,
|
||||
} from '~/vue_shared/components/rich_content_editor/constants';
|
||||
|
||||
import { addCustomEventListener } from '~/vue_shared/components/rich_content_editor/editor_service';
|
||||
import {
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
addImage,
|
||||
} from '~/vue_shared/components/rich_content_editor/editor_service';
|
||||
|
||||
jest.mock('~/vue_shared/components/rich_content_editor/editor_service', () => ({
|
||||
...jest.requireActual('~/vue_shared/components/rich_content_editor/editor_service'),
|
||||
addCustomEventListener: jest.fn(),
|
||||
removeCustomEventListener: jest.fn(),
|
||||
addImage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Rich Content Editor', () => {
|
||||
|
@ -20,6 +27,7 @@ describe('Rich Content Editor', () => {
|
|||
|
||||
const value = '## Some Markdown';
|
||||
const findEditor = () => wrapper.find({ ref: 'editor' });
|
||||
const findAddImageModal = () => wrapper.find(AddImageModal);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(RichContentEditor, {
|
||||
|
@ -77,4 +85,34 @@ describe('Rich Content Editor', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editor is destroyed', () => {
|
||||
it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
|
||||
const mockInstance = { eventManager: { removeEventHandler: jest.fn() } };
|
||||
|
||||
wrapper.vm.$refs.editor = mockInstance;
|
||||
wrapper.vm.$destroy();
|
||||
|
||||
expect(removeCustomEventListener).toHaveBeenCalledWith(
|
||||
mockInstance,
|
||||
CUSTOM_EVENTS.openAddImageModal,
|
||||
wrapper.vm.onOpenAddImageModal,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add image modal', () => {
|
||||
it('renders an addImageModal component', () => {
|
||||
expect(findAddImageModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls the onAddImage method when the addImage event is emitted', () => {
|
||||
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
|
||||
const mockInstance = { exec: jest.fn() };
|
||||
wrapper.vm.$refs.editor = mockInstance;
|
||||
|
||||
findAddImageModal().vm.$emit('addImage', mockImage);
|
||||
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ describe Gitlab::Import::SetAsyncJid do
|
|||
it 'sets the JID in Redis' do
|
||||
expect(Gitlab::SidekiqStatus)
|
||||
.to receive(:set)
|
||||
.with("async-import/project-import-state/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
|
||||
.with("async-import/project-import-state/#{project.id}", Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
|
||||
.and_call_original
|
||||
|
||||
described_class.set_jid(project.import_state)
|
||||
|
|
|
@ -66,7 +66,7 @@ describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do
|
|||
end
|
||||
|
||||
expect(set_data).to eq({ classname: 'Issue', database_id: issue.id.to_s })
|
||||
expect(ttl).to be_within(1.second).of(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
|
||||
expect(ttl).to be_within(1.second).of(Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -124,6 +124,8 @@ module UsageDataHelpers
|
|||
releases
|
||||
remote_mirrors
|
||||
snippets
|
||||
personal_snippets
|
||||
project_snippets
|
||||
suggestions
|
||||
terraform_reports
|
||||
terraform_states
|
||||
|
|
|
@ -17,7 +17,7 @@ describe ProjectImportOptions do
|
|||
end
|
||||
|
||||
it 'sets default status expiration' do
|
||||
expect(worker_class.sidekiq_options['status_expiration']).to eq(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
|
||||
expect(worker_class.sidekiq_options['status_expiration']).to eq(Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
|
||||
end
|
||||
|
||||
describe '.sidekiq_retries_exhausted' do
|
||||
|
|
Loading…
Reference in a new issue