summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md37
-rw-r--r--app/assets/javascripts/batch_comments/mixins/resolved_status.js13
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js5
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js3
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js5
-rw-r--r--app/assets/javascripts/boards/boards_util.js7
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js3
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js3
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js3
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js4
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js21
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service_error.js4
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js9
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue155
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue11
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue190
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue3
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js8
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/utils.js7
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js7
-rw-r--r--app/assets/javascripts/raven/raven_config.js9
-rw-r--r--app/controllers/projects/environments_controller.rb26
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/helpers/mirror_helper.rb4
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/repository.rb9
-rw-r--r--app/views/admin/application_settings/_logging.html.haml1
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--babel.config.js5
-rw-r--r--changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml5
-rw-r--r--changelogs/unreleased/58294-discussion-notes-component.yml5
-rw-r--r--changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml5
-rw-r--r--changelogs/unreleased/60687-enviro-dropdown.yml5
-rw-r--r--changelogs/unreleased/60821-deployment-jobs-broken-as-of-11-10-0.yml5
-rw-r--r--changelogs/unreleased/60855-mr-popover-is-not-attached-in-system-notes.yml5
-rw-r--r--changelogs/unreleased/60906-fix-wiki-links.yml5
-rw-r--r--changelogs/unreleased/da-sentry-client-side-settings.yml5
-rw-r--r--changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml5
-rw-r--r--changelogs/unreleased/fix-environment-on-stop-not-work.yml5
-rw-r--r--changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml6
-rw-r--r--changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml5
-rw-r--r--changelogs/unreleased/fj-60827-fix-web-strategy-error.yml5
-rw-r--r--changelogs/unreleased/lock-pipeline-schedule-worker.yml5
-rw-r--r--changelogs/unreleased/secure-disallow-read-user-scope-to-read-project-events.yml5
-rw-r--r--changelogs/unreleased/sh-fix-slow-partial-rendering.yml5
-rw-r--r--changelogs/unreleased/winh-boards-drag-selection.yml5
-rw-r--r--config/gitlab.yml.example1
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/karma.config.js2
-rw-r--r--db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb17
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/container_registry.md7
-rw-r--r--doc/administration/custom_hooks.md65
-rw-r--r--doc/analytics/README.md5
-rw-r--r--doc/analytics/contribution_analytics.md5
-rw-r--r--doc/api/merge_requests.md90
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/ci/junit_test_reports.md6
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/topics/git/troubleshooting_git.md27
-rw-r--r--doc/user/project/labels.md2
-rw-r--r--doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.pngbin0 -> 20867 bytes
-rw-r--r--doc/user/project/merge_requests/index.md22
-rw-r--r--doc/user/project/quick_actions.md7
-rw-r--r--jest.config.js1
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml26
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml30
-rw-r--r--lib/gitlab/file_detector.rb1
-rw-r--r--lib/gitlab/gl_repository.rb12
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/base_service.rb73
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb51
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb15
-rw-r--r--lib/gitlab/metrics/dashboard/project_dashboard_service.rb47
-rw-r--r--lib/gitlab/metrics/dashboard/service.rb40
-rw-r--r--lib/gitlab/metrics/dashboard/stages/base_stage.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/system_dashboard_service.rb47
-rw-r--r--locale/gitlab.pot90
-rw-r--r--package.json3
-rwxr-xr-xscripts/lint-doc.sh2
-rw-r--r--security.txt6
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb91
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js139
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js4
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js111
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js23
-rw-r--r--spec/javascripts/pdf/index_spec.js4
-rw-r--r--spec/javascripts/pdf/page_spec.js4
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb21
-rw-r--r--spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/service_spec.rb42
-rw-r--r--spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb32
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/repository_spec.rb1
-rw-r--r--spec/routing/group_routing_spec.rb14
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb43
-rw-r--r--spec/support/shared_examples/application_setting_examples.rb4
115 files changed, 1562 insertions, 522 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e1ffeaebd5..17b185358ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,36 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.10.4 (2019-05-01)
+
+### Fixed (12 changes)
+
+- Fix MR popover on ToDos page. !27382
+- Fix 500 in general pipeline settings when passing an invalid build timeout. !27416
+- Fix bug where system note MR has no popover. !27589
+- Fix bug when project export to remote url fails. !27614
+- `on_stop` is not automatically triggered with pipelines for merge requests. !27618
+- Update Workhorse to v8.5.2. !27631
+- Show proper wiki links in search results. !27634
+- Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent. !27663
+- Fix Kubernetes service template deployment jobs broken as of 11.10.0. !27687
+- Prevent text selection when dragging in issue boards. !27724
+- Fix pipelines for merge requests does not show pipeline page when source branch is removed. !27803
+- Fix Metrics Environments dropdown.
+
+### Performance (2 changes)
+
+- Prevent concurrent execution of PipelineScheduleWorker. !27781
+- Fix slow performance with compiling HAML templates. !27782
+
+
+## 11.10.3 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
## 11.10.2 (2019-04-25)
### Security (4 changes)
@@ -632,6 +662,13 @@ entry.
- Removes EE differences for jobs/getters.js.
+## 11.8.10 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
## 11.8.8 (2019-04-23)
### Fixed (5 changes)
diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
new file mode 100644
index 00000000000..20c31d9f8a4
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
@@ -0,0 +1,13 @@
+export default {
+ computed: {
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
+
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
+
+ return title;
+ },
+ },
+};
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 9a33a060c76..c3541e62568 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Clipboard from 'clipboard';
+import { sprintf, __ } from '~/locale';
function showTooltip(target, title) {
const $target = $(target);
@@ -16,7 +17,7 @@ function showTooltip(target, title) {
}
function genericSuccess(e) {
- showTooltip(e.trigger, 'Copied');
+ showTooltip(e.trigger, __('Copied'));
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
@@ -33,7 +34,7 @@ function genericError(e) {
} else {
key = 'Ctrl';
}
- showTooltip(e.trigger, `Press ${key}-C to copy`);
+ showTooltip(e.trigger, sprintf(__(`Press %{key}-C to copy`), { key }));
}
export default function initCopyToClipboard() {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
index 20c7fa8a9ab..9a2e2c03213 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import { Node } from 'tiptap';
+import { __ } from '~/locale';
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
export default class TableOfContents extends Node {
@@ -22,7 +23,7 @@ export default class TableOfContents extends Node {
priority: 51,
},
],
- toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
+ toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')],
};
}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 7adccbb062f..35874140bf9 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -22,7 +22,7 @@ function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
-MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
+MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.');
MarkdownPreview.prototype.ajaxCache = {};
@@ -40,7 +40,7 @@ MarkdownPreview.prototype.showPreview = function($form) {
preview.text(this.emptyMessage);
this.hideReferencedUsers($form);
} else {
- preview.addClass('md-preview-loading').text('Loading...');
+ preview.addClass('md-preview-loading').text(__('Loading...'));
this.fetchMarkdownPreview(
mdText,
url,
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index c1ea67f9293..530ab0bd4d9 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import '../commons/bootstrap';
import { isInIssuePage } from '../lib/utils/common_utils';
+import { __ } from '~/locale';
// Quick Submit behavior
//
@@ -65,7 +66,9 @@ $(document).on(
}
const $this = $(this);
- const title = isMac() ? 'You can also press ⌘-Enter' : 'You can also press Ctrl-Enter';
+ const title = isMac()
+ ? __('You can also press ⌘-Enter')
+ : __('You can also press Ctrl-Enter');
$this.tooltip({
container: 'body',
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
new file mode 100644
index 00000000000..3178bda93b8
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -0,0 +1,7 @@
+export function getMilestone() {
+ return null;
+}
+
+export default {
+ getMilestone,
+};
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index a5f9d65e4d5..a06db359c94 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
export default Vue.extend({
props: {
@@ -13,7 +14,7 @@ export default Vue.extend({
$(this.$el).tooltip('hide');
// eslint-disable-next-line no-alert
- if (window.confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm(__('Are you sure you want to delete this list?'))) {
this.list.destroy();
}
},
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 70daba352f9..dc1bdc23b5e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
+import { getMilestone } from 'ee_else_ce/boards/boards_util';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -51,11 +52,14 @@ export default {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
+ const milestone = getMilestone(this.list);
+
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
assignees,
+ milestone,
project_id: this.selectedProject.id,
});
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 915d1676e62..c587b276fa3 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -45,7 +45,7 @@ export default Vue.extend({
return Object.keys(this.issue).length;
},
milestoneTitle() {
- return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
+ return this.issue.milestone ? this.issue.milestone.title : __('No Milestone');
},
canRemove() {
return !this.list.preset;
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 009ae5dd331..4995a8d9367 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -123,7 +123,7 @@ export default () => {
this.loading = false;
})
.catch(() => {
- Flash('An error occurred while fetching the board lists. Please try again.');
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
});
},
methods: {
@@ -223,7 +223,7 @@ export default () => {
},
tooltipTitle() {
if (this.disabled) {
- return 'Please add a list to your board first';
+ return __('Please add a list to your board first');
}
return '';
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index a34222b6887..70861fbf2b3 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
const boardsStore = {
disabled: false,
@@ -78,7 +79,7 @@ const boardsStore = {
this.addList({
id: 'blank',
list_type: 'blank',
- title: 'Welcome to your Issue Board!',
+ title: __('Welcome to your Issue Board!'),
position: 0,
});
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 0b24d9fc920..e020628a473 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default IssuableTokenKeys => {
const wipToken = {
key: 'wip',
@@ -5,7 +7,7 @@ export default IssuableTokenKeys => {
param: '',
symbol: '',
icon: 'admin',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: true,
capitalizeTokenValue: true,
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index d9a4d06b549..dad188f6f98 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -14,7 +15,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 0264f934914..a2312de289d 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -17,7 +18,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
preprocessing,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index efd03ec952f..78fbb3696cc 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -14,6 +14,7 @@ import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class FilteredSearchManager {
constructor({
@@ -64,7 +65,7 @@ export default class FilteredSearchManager {
.catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new Flash('An error occurred while parsing recent searches');
+ new Flash(__('An error occurred while parsing recent searches'));
// Gracefully fail to empty array
return [];
})
@@ -340,7 +341,7 @@ export default class FilteredSearchManager {
handleInputPlaceholder() {
const query = DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
+ const placeholder = __('Search or filter results...');
const currentPlaceholder = this.filteredSearchInput.placeholder;
if (query.length === 0 && currentPlaceholder !== placeholder) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 11ed85504ec..0a9579bf491 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class FilteredSearchTokenKeys {
constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
this.tokenKeys = tokenKeys;
@@ -79,7 +81,7 @@ export default class FilteredSearchTokenKeys {
param: '',
symbol: '',
icon: 'eye-slash',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: false,
capitalizeTokenValue: true,
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index fd61030eb13..6c3d9e33420 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -1,4 +1,5 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys';
+import { __ } from '~/locale';
export const tokenKeys = [
{
@@ -60,52 +61,52 @@ export const conditions = [
{
url: 'assignee_id=None',
tokenKey: 'assignee',
- value: 'None',
+ value: __('None'),
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=None',
tokenKey: 'milestone',
- value: 'None',
+ value: __('None'),
},
{
url: 'milestone_title=Any',
tokenKey: 'milestone',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
- value: 'Upcoming',
+ value: __('Upcoming'),
},
{
url: 'milestone_title=%23started',
tokenKey: 'milestone',
- value: 'Started',
+ value: __('Started'),
},
{
url: 'label_name[]=None',
tokenKey: 'label',
- value: 'None',
+ value: __('None'),
},
{
url: 'label_name[]=Any',
tokenKey: 'label',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
- value: 'None',
+ value: __('None'),
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
- value: 'Any',
+ value: __('Any'),
},
];
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
index 5917b223d63..011b37e218d 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
@@ -1,7 +1,9 @@
+import { __ } from '~/locale';
+
class RecentSearchesServiceError {
constructor(message) {
this.name = 'RecentSearchesServiceError';
- this.message = message || 'Recent Searches Service is unavailable';
+ this.message = message || __('Recent Searches Service is unavailable');
}
}
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index a9d5ba8faa8..38327472cb3 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -5,6 +5,7 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
+import { __ } from '~/locale';
export default class VisualTokenValue {
constructor(tokenValue, tokenType) {
@@ -77,7 +78,7 @@ export default class VisualTokenValue {
matchingLabel.text_color,
);
})
- .catch(() => new Flash('An error occurred while fetching label colors.'));
+ .catch(() => new Flash(__('An error occurred while fetching label colors.')));
}
static setTokenStyle(tokenValueContainer, backgroundColor, textColor) {
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 08b858305ab..a7746bb3a0b 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
+import { __ } from '~/locale';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -65,10 +66,10 @@ export default class IntegrationSettingsForm {
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
- let btnLabel = 'Save changes';
+ let btnLabel = __('Save changes');
if (serviceActive && this.canTestService) {
- btnLabel = 'Test settings and save changes';
+ btnLabel = __('Test settings and save changes');
}
this.$submitBtnLabel.text(btnLabel);
@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) {
flashActions = {
- title: 'Save anyway',
+ title: __('Save anyway'),
clickHandler: e => {
e.preventDefault();
this.$form.submit();
@@ -121,7 +122,7 @@ export default class IntegrationSettingsForm {
this.toggleSubmitBtnState(false);
})
.catch(() => {
- flash('Something went wrong on our end.');
+ flash(__('Something went wrong on our end.'));
this.toggleSubmitBtnState(false);
});
}
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 37b5409a51d..37b17f0fe23 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,3 +1,5 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
// tell webpack to load assets from origin so that web workers don't break
// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
@@ -5,7 +7,7 @@ export function resetServiceWorkersPublicPath() {
// the webpack publicPath setting at runtime.
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
// monaco-editor-webpack-plugin currently (incorrectly) references the
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
new file mode 100644
index 00000000000..5b6163a6214
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -0,0 +1,155 @@
+<script>
+import { mapGetters } from 'vuex';
+import { SYSTEM_NOTE } from '../constants';
+import { __ } from '~/locale';
+import NoteableNote from './noteable_note.vue';
+import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import NoteEditedText from './note_edited_text.vue';
+
+export default {
+ name: 'DiscussionNotes',
+ components: {
+ ToggleRepliesWidget,
+ NoteEditedText,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ isExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ diffLine: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ shouldGroupReplies: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ ...mapGetters(['userCanReply']),
+ hasReplies() {
+ return !!this.replies.length;
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
+ firstNote() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ resolvedText() {
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
+ },
+ commit() {
+ if (!this.discussion.for_commit) {
+ return null;
+ }
+
+ return {
+ id: this.discussion.commit_id,
+ url: this.discussion.discussion_path,
+ };
+ },
+ },
+ methods: {
+ componentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === SYSTEM_NOTE) {
+ return PlaceholderSystemNote;
+ }
+
+ return PlaceholderNote;
+ }
+
+ if (note.system) {
+ return SystemNote;
+ }
+
+ return NoteableNote;
+ },
+ componentData(note) {
+ return note.isPlaceholderNote ? note.notes[0] : note;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-notes">
+ <ul class="notes">
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ @handle-delete-note="$emit('deleteNote')"
+ @start-replying="$emit('startReplying')"
+ >
+ <note-edited-text
+ v-if="discussion.resolved"
+ slot="discussion-resolved-text"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="$emit('toggleDiscussion')"
+ />
+ <template v-if="isExpanded">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="line"
+ @handle-delete-note="$emit('deleteNote')"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="diffLine"
+ @handle-delete-note="$emit('deleteNote')"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index aabb77f6a85..7e8f23d6a96 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status';
import ReplyButton from './note_actions/reply_button.vue';
export default {
@@ -14,6 +15,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [resolvedStatusMixin],
props: {
authorId: {
type: Number,
@@ -98,15 +100,6 @@ export default {
currentUserId() {
return this.getUserDataByProp('id');
},
- resolveButtonTitle() {
- let title = 'Mark as resolved';
-
- if (this.resolvedBy) {
- title = `Resolved by ${this.resolvedBy.name}`;
- }
-
- return title;
- },
},
methods: {
onEdit() {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 1e47bef7b61..2c549e7abdd 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -5,44 +5,35 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
-import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
-import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
-import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
-import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
import eventHub from '../event_hub';
+import DiscussionNotes from './discussion_notes.vue';
import DiscussionActions from './discussion_actions.vue';
export default {
name: 'NoteableDiscussion',
components: {
icon,
- noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
noteForm,
- toggleRepliesWidget,
- placeholderNote,
- placeholderSystemNote,
- systemNote,
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
TimelineEntryItem,
+ DiscussionNotes,
DiscussionActions,
},
directives: {
@@ -91,6 +82,7 @@ export default {
...mapGetters([
'convertedDisscussionIds',
'getNoteableData',
+ 'userCanReply',
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
@@ -102,21 +94,12 @@ export default {
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
- canReply() {
- return this.getNoteableData.current_user.can_create_note;
- },
newNotePath() {
return this.getNoteableData.create_note_path;
},
- hasReplies() {
- return this.discussion.notes.length > 1;
- },
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
- replies() {
- return this.discussion.notes.slice(1);
- },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -222,18 +205,8 @@ export default {
return null;
},
- commit() {
- if (!this.discussion.for_commit) {
- return null;
- }
-
- return {
- id: this.discussion.commit_id,
- url: this.discussion.discussion_path,
- };
- },
resolveWithIssuePath() {
- return !this.discussionResolved && this.discussion.resolve_with_issue_path;
+ return !this.discussionResolved ? this.discussion.resolve_with_issue_path : '';
},
},
created() {
@@ -252,24 +225,6 @@ export default {
'removeConvertedDiscussion',
]),
truncateSha,
- componentName(note) {
- if (note.isPlaceholderNote) {
- if (note.placeholderType === SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
-
- return placeholderNote;
- }
-
- if (note.system) {
- return systemNote;
- }
-
- return noteableNote;
- },
- componentData(note) {
- return note.isPlaceholderNote ? note.notes[0] : note;
- },
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
@@ -399,97 +354,56 @@ Please check your network connection and try again.`;
v-bind="wrapperComponentProps"
class="card discussion-wrapper"
>
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="canReply"
- @handleDeleteNote="deleteNoteHandler"
- @startReplying="showReplyForm"
- >
- <note-edited-text
- v-if="discussion.resolved"
- slot="discussion-resolved-text"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- <slot slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="!isExpanded"
- :replies="replies"
- @toggle="toggleDiscussionHandler"
+ <discussion-notes
+ :discussion="discussion"
+ :diff-line="diffLine"
+ :help-page-path="helpPagePath"
+ :is-expanded="isExpanded"
+ :line="line"
+ :should-group-replies="shouldGroupReplies"
+ @startReplying="showReplyForm"
+ @toggleDiscussion="toggleDiscussionHandler"
+ @deleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #footer="{ showReplies }">
+ <draft-note
+ v-if="showDraft(discussion.reply_id)"
+ :key="`draft_${discussion.id}`"
+ :draft="draftForDiscussion(discussion.reply_id)"
+ />
+ <div
+ v-else-if="showReplies"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <discussion-actions
+ v-if="!isReplying && userCanReply"
+ :discussion="discussion"
+ :is-resolving="isResolving"
+ :resolve-button-title="resolveButtonTitle"
+ :resolve-with-issue-path="resolveWithIssuePath"
+ :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion"
+ @showReplyForm="showReplyForm"
+ @resolve="resolveHandler"
+ @jumpToNextDiscussion="jumpToNextDiscussion"
/>
- <template v-if="isExpanded">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
- :line="line"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
- <component
- :is="componentName(note)"
- v-for="(note, index) in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
:line="diffLine"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- </template>
- </ul>
- <draft-note
- v-if="showDraft(discussion.reply_id)"
- :key="`draft_${discussion.id}`"
- :draft="draftForDiscussion(discussion.reply_id)"
- />
- <div
- v-else-if="isExpanded || !hasReplies"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <discussion-actions
- v-if="!isReplying && canReply"
- :discussion="discussion"
- :is-resolving="isResolving"
- :resolve-button-title="resolveButtonTitle"
- :resolve-with-issue-path="resolveWithIssuePath"
- :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion"
- @showReplyForm="showReplyForm"
- @resolve="resolveHandler"
- @jumpToNextDiscussion="jumpToNextDiscussion"
- />
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
- </div>
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!userCanReply" />
+ </div>
+ </template>
+ </discussion-notes>
</component>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index e2bd59f7631..0f1976db37d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -67,6 +67,7 @@ export default {
'isLoading',
'commentsDisabled',
'getNoteableData',
+ 'userCanReply',
]),
noteableType() {
return this.noteableData.noteableType;
@@ -83,7 +84,7 @@ export default {
return this.discussions;
},
canReply() {
- return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled;
+ return this.userCanReply && !this.commentsDisabled;
},
},
watch: {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 4f45f912479..b161773f5f1 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
+import { s__ } from '~/locale';
export default {
methods: {
initAutoSave(noteable, extraKeys = []) {
let keys = [
- 'Note',
+ s__('Autosave|Note'),
capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType),
noteable.id,
];
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1a0dba69a7c..970e6551092 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -255,7 +255,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', noteData.flashContainer);
+ Flash(__('Commands applied'), 'notice', noteData.flashContainer);
}
if (commandsChanges) {
@@ -269,7 +269,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
})
.catch(() => {
Flash(
- 'Something went wrong while adding your award. Please try again.',
+ __('Something went wrong while adding your award. Please try again.'),
'alert',
noteData.flashContainer,
);
@@ -311,7 +311,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
data: state,
successCallback: resp =>
resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
- errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
+ errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
});
if (!Visibility.hidden()) {
@@ -347,7 +347,7 @@ export const fetchData = ({ commit, state, getters }) => {
.poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
- .catch(() => Flash('Something went wrong while fetching latest comments.'));
+ .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
};
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index fcc8889b0c7..2d150e64ef7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,6 +20,8 @@ export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+export const userCanReply = state => !!state.noteableData.current_user.can_create_note;
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 4b0feb0f94d..029fde348fb 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -1,12 +1,13 @@
import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { sprintf, __ } from '~/locale';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => {
- let text = 'Applying command';
+ let text = __('Applying command');
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => {
@@ -16,10 +17,10 @@ export const getQuickActionText = note => {
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
- text = 'Applying multiple commands';
+ text = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- text = `Applying command to ${commandDescription}`;
+ text = sprintf(__('Applying command to %{commandDescription}', { commandDescription }));
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index b803da798d5..def2f091947 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class ProtectedTagAccessDropdown {
constructor(options) {
this.options = options;
@@ -15,7 +17,7 @@ export default class ProtectedTagAccessDropdown {
if ($el.is('.is-active')) {
return item.text;
}
- return 'Select';
+ return __('Select');
},
clicked(options) {
options.e.preventDefault();
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index fddf2674cbb..03a5fe6b353 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagCreate {
constructor() {
@@ -27,7 +28,7 @@ export default class ProtectedTagCreate {
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
- defaultToggleLabel: 'Protected Tag',
+ defaultToggleLabel: __('Protected Tag'),
fieldName: 'protected_tag[name]',
onSelect: this.onSelectCallback,
getData: ProtectedTagCreate.getProtectedTags,
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index c52497e62f2..70bfd71abce 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,6 +1,7 @@
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagEdit {
constructor(options) {
@@ -47,7 +48,11 @@ export default class ProtectedTagEdit {
.catch(() => {
this.$allowedToCreateDropdownButton.enable();
- flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
+ flash(
+ __('Failed to update tag!'),
+ 'alert',
+ document.querySelector('.js-protected-tags-list'),
+ );
});
}
}
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index b4a8c263954..7259e0df104 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -1,5 +1,6 @@
import Raven from 'raven-js';
import $ from 'jquery';
+import { __ } from '~/locale';
const IGNORE_ERRORS = [
// Random plugins/extensions
@@ -9,9 +10,9 @@ const IGNORE_ERRORS = [
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
- "Can't find variable: ZiteReader",
- 'jigsaw is not defined',
- 'ComboSearch is not defined',
+ __("Can't find variable: ZiteReader"),
+ __('jigsaw is not defined'),
+ __('ComboSearch is not defined'),
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
@@ -80,7 +81,7 @@ const RavenConfig = {
handleRavenErrors(event, req, config, err) {
const error = err || req.statusText;
- const responseText = req.responseText || 'Unknown response text';
+ const responseText = req.responseText || __('Unknown response text');
Raven.captureMessage(error, {
extra: {
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4aa572ade73..d8812c023ca 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -13,6 +13,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:metrics_time_window)
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
+ push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
end
def index
@@ -158,15 +159,28 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics_dashboard
- return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, @project)
+ return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project)
- result = Gitlab::Metrics::Dashboard::Service.new(@project, @current_user, environment: environment).get_dashboard
+ if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
+ result = dashboard_finder.find(project, current_user, environment, params[:dashboard])
+
+ result[:all_dashboards] = project.repository.metrics_dashboard_paths
+ else
+ result = dashboard_finder.find(project, current_user, environment)
+ end
respond_to do |format|
if result[:status] == :success
- format.json { render status: :ok, json: result }
+ format.json do
+ render status: :ok, json: result.slice(:all_dashboards, :dashboard, :status)
+ end
else
- format.json { render status: result[:http_status], json: result }
+ format.json do
+ render(
+ status: result[:http_status],
+ json: result.slice(:all_dashboards, :message, :status)
+ )
+ end
end
end
end
@@ -211,6 +225,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
params.require([:start, :end])
end
+ def dashboard_finder
+ Gitlab::Metrics::Dashboard::Finder
+ end
+
def search_environment_names
return [] unless params[:query]
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 48f4d7a586d..e88c46144ef 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -36,10 +36,10 @@ class ProjectsController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def new
- namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
- return access_denied! if namespace && !can?(current_user, :create_projects, namespace)
+ @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
+ return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace)
- @project = Project.new(namespace_id: namespace&.id)
+ @project = Project.new(namespace_id: @namespace&.id)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 65c7cd82832..921c79ab771 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -7,4 +7,8 @@ module MirrorHelper
project_mirror_endpoint: project_mirror_path(@project, :json)
}
end
+
+ def mirror_lfs_sync_message
+ _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ end
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 557215ff4dc..ee12a1d09f3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -196,7 +196,7 @@ module ApplicationSettingImplementation
end
def clientside_sentry_dsn
- Gitlab.config.sentry.dsn || read_attribute(:clientside_sentry_dsn)
+ Gitlab.config.sentry.clientside_dsn || read_attribute(:clientside_sentry_dsn)
end
def performance_bar_allowed_group
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 8b728c4f6b2..f495a03ad8e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,7 +39,8 @@ class Repository
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names xcode_project?).freeze
+ issue_template_names merge_request_template_names
+ metrics_dashboard_paths xcode_project?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -57,6 +58,7 @@ class Repository
avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names,
+ metrics_dashboard: :metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -602,6 +604,11 @@ class Repository
end
cache_method :merge_request_template_names, fallback: []
+ def metrics_dashboard_paths
+ Gitlab::Metrics::Dashboard::Finder.find_all_paths_from_source(project)
+ end
+ cache_method :metrics_dashboard_paths
+
def readme
head_tree&.readme
end
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index 4145ef94de8..1da5f6fccd6 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -5,7 +5,6 @@
%strong
NOTE:
These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml.
- The specific client side DSN setting is already handled as a component from a Sentry perspective anb will be removed.
In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production.
%fieldset
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 35a6885318a..33e5a6e67c3 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -7,7 +7,7 @@
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
- %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ %li= mirror_lfs_sync_message
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.')
diff --git a/babel.config.js b/babel.config.js
index 78d14095b0b..df30892731d 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -38,8 +38,9 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
-// Jest is running in node environment
-if (BABEL_ENV === 'jest') {
+// Jest is running in node environment, so we need additional plugins
+const isJest = !!process.env.JEST_WORKER_ID;
+if (isJest) {
plugins.push('@babel/plugin-transform-modules-commonjs');
/*
without the following, babel-plugin-istanbul throws an error:
diff --git a/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml b/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml
deleted file mode 100644
index 8b4f4894048..00000000000
--- a/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 in general pipeline settings when passing an invalid build timeout.
-merge_request: 27416
-author:
-type: fixed
diff --git a/changelogs/unreleased/58294-discussion-notes-component.yml b/changelogs/unreleased/58294-discussion-notes-component.yml
new file mode 100644
index 00000000000..fbe08360a9a
--- /dev/null
+++ b/changelogs/unreleased/58294-discussion-notes-component.yml
@@ -0,0 +1,5 @@
+---
+title: Extract DiscussionNotes component from NoteableDiscussion
+merge_request: 27066
+author:
+type: other
diff --git a/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml b/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml
deleted file mode 100644
index 7b5fae016bb..00000000000
--- a/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix MR popover on ToDos page
-merge_request: 27382
-author:
-type: fixed
diff --git a/changelogs/unreleased/60687-enviro-dropdown.yml b/changelogs/unreleased/60687-enviro-dropdown.yml
deleted file mode 100644
index 1fc5a7dd6f5..00000000000
--- a/changelogs/unreleased/60687-enviro-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Metrics Environments dropdown
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/60821-deployment-jobs-broken-as-of-11-10-0.yml b/changelogs/unreleased/60821-deployment-jobs-broken-as-of-11-10-0.yml
deleted file mode 100644
index 88584352d42..00000000000
--- a/changelogs/unreleased/60821-deployment-jobs-broken-as-of-11-10-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Kubernetes service template deployment jobs broken as of 11.10.0
-merge_request: 27687
-author:
-type: fixed
diff --git a/changelogs/unreleased/60855-mr-popover-is-not-attached-in-system-notes.yml b/changelogs/unreleased/60855-mr-popover-is-not-attached-in-system-notes.yml
deleted file mode 100644
index d8bc0fbb4d4..00000000000
--- a/changelogs/unreleased/60855-mr-popover-is-not-attached-in-system-notes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug where system note MR has no popover
-merge_request: 27747
-author:
-type: fixed
diff --git a/changelogs/unreleased/60906-fix-wiki-links.yml b/changelogs/unreleased/60906-fix-wiki-links.yml
deleted file mode 100644
index cc65a1382bf..00000000000
--- a/changelogs/unreleased/60906-fix-wiki-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show proper wiki links in search results
-merge_request: 27634
-author:
-type: fixed
diff --git a/changelogs/unreleased/da-sentry-client-side-settings.yml b/changelogs/unreleased/da-sentry-client-side-settings.yml
new file mode 100644
index 00000000000..e36ac7c354b
--- /dev/null
+++ b/changelogs/unreleased/da-sentry-client-side-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Sentry client-side DSN to be passed on gitlab.yml
+merge_request: 27967
+author:
+type: added
diff --git a/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml b/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml
deleted file mode 100644
index c34bc6d8b52..00000000000
--- a/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent
-merge_request: 27663
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-environment-on-stop-not-work.yml b/changelogs/unreleased/fix-environment-on-stop-not-work.yml
deleted file mode 100644
index 72e58b26c4d..00000000000
--- a/changelogs/unreleased/fix-environment-on-stop-not-work.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "`on_stop` is not automatically triggered with pipelines for merge requests"
-merge_request: 27618
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml b/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml
deleted file mode 100644
index 8803f9b52a4..00000000000
--- a/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix pipelines for merge requests does not show pipeline page when source branch
- is removed
-merge_request: 27803
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
new file mode 100644
index 00000000000..80936245f3e
--- /dev/null
+++ b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webpack assets handling when relative url root is '/'
+merge_request: 27909
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml b/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml
deleted file mode 100644
index ad707ca0225..00000000000
--- a/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug when project export to remote url fails
-merge_request: 27614
-author:
-type: fixed
diff --git a/changelogs/unreleased/lock-pipeline-schedule-worker.yml b/changelogs/unreleased/lock-pipeline-schedule-worker.yml
deleted file mode 100644
index 1b889f01620..00000000000
--- a/changelogs/unreleased/lock-pipeline-schedule-worker.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent concurrent execution of PipelineScheduleWorker
-merge_request: 27781
-author:
-type: performance
diff --git a/changelogs/unreleased/secure-disallow-read-user-scope-to-read-project-events.yml b/changelogs/unreleased/secure-disallow-read-user-scope-to-read-project-events.yml
deleted file mode 100644
index 4a91bfa8827..00000000000
--- a/changelogs/unreleased/secure-disallow-read-user-scope-to-read-project-events.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to see project events only with api scope token
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-fix-slow-partial-rendering.yml b/changelogs/unreleased/sh-fix-slow-partial-rendering.yml
deleted file mode 100644
index 0f65a6f8d69..00000000000
--- a/changelogs/unreleased/sh-fix-slow-partial-rendering.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix slow performance with compiling HAML templates
-merge_request: 27782
-author:
-type: performance
diff --git a/changelogs/unreleased/winh-boards-drag-selection.yml b/changelogs/unreleased/winh-boards-drag-selection.yml
deleted file mode 100644
index 84b23ab664b..00000000000
--- a/changelogs/unreleased/winh-boards-drag-selection.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent text selection when dragging in issue boards
-merge_request: 27724
-author:
-type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 06530194907..2f822805b25 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -320,6 +320,7 @@ production: &base
sentry:
# enabled: false
# dsn: https://<key>@sentry.io/<project>
+ # clientside_dsn: https://<key>@sentry.io/<project>
# environment: 'production' # e.g. development, staging, production
#
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 154de3bc1b0..d56bd7654af 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -224,6 +224,7 @@ Settings['sentry'] ||= Settingslogic.new({})
Settings.sentry['enabled'] ||= false
Settings.sentry['dsn'] ||= nil
Settings.sentry['environment'] ||= nil
+Settings.sentry['clientside_dsn'] ||= nil
#
# Pages
diff --git a/config/karma.config.js b/config/karma.config.js
index 7e1e89f3c10..dfcb5c4646e 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -110,7 +110,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],
files: [
{ pattern: 'spec/javascripts/test_bundle.js', watched: false },
- { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.png)', included: false },
+ { pattern: `spec/javascripts/fixtures/**/*@(.json|.html|.png|.bmpr|.pdf)`, included: false },
],
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
diff --git a/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
new file mode 100644
index 00000000000..eb0e7c41f98
--- /dev/null
+++ b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddMergeTrainEnabledToCiCdSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :project_ci_cd_settings, :merge_trains_enabled
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5a486b369e3..2e77bbb51e5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1612,6 +1612,7 @@ ActiveRecord::Schema.define(version: 20190426180107) do
t.integer "project_id", null: false
t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled"
+ t.boolean "merge_trains_enabled", default: false, null: false
t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
end
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 05beb724d4d..48f599fa7e6 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -388,6 +388,9 @@ desired.
**Omnibus GitLab installations**
+> **Note:**
+`regionendpoint` is only required when configuring an S3 compatible service such as Minio, by entering a URL such as http://127.0.0.1:9000
+
To configure the storage driver in Omnibus:
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -398,7 +401,8 @@ To configure the storage driver in Omnibus:
'accesskey' => 's3-access-key',
'secretkey' => 's3-secret-key-for-access-key',
'bucket' => 'your-s3-bucket',
- 'region' => 'your-s3-region'
+ 'region' => 'your-s3-region',
+ 'regionendpoint' => 'your-s3-regionendpoint'
}
}
```
@@ -421,6 +425,7 @@ storage:
secretkey: 'secret123'
bucket: 'gitlab-registry-bucket-AKIAKIAKI'
region: 'your-s3-region'
+ regionendpoint: 'your-s3-regionendpoint'
cache:
blobdescriptor: inmemory
delete:
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 28afaf84f5a..f318cb38ef0 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -1,6 +1,7 @@
-# Custom Git Hooks
+# Custom server-side Git hooks
-> **Note:** Custom Git hooks must be configured on the filesystem of the GitLab
+NOTE: **Note:**
+Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see
@@ -14,15 +15,14 @@ See [Git SCM Server-Side Hooks][hooks] for more information about each hook type
As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
administrators can add custom git hooks to any GitLab project.
-## Setup
+## Create a custom Git hook for a repository
-Normally, Git hooks are placed in the repository or project's `hooks` directory.
-GitLab creates a symlink from each project's `hooks` directory to the
-gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell
-upgrades. As such, custom hooks are implemented a little differently. Behavior
-is exactly the same once the hook is created, though.
-
-Follow the steps below to set up a custom hook:
+Server-side Git hooks are typically placed in the repository's `hooks`
+subdirectory. In GitLab, hook directories are are symlinked to the gitlab-shell
+`hooks` directory for ease of maintenance between gitlab-shell upgrades.
+Custom hooks are implemented differently, but the behavior is exactly the same
+once the hook is created. Follow the steps below to set up a custom hook for a
+repository:
1. Pick a project that needs a custom Git hook.
1. On the GitLab server, navigate to the project's repository directory.
@@ -42,33 +42,56 @@ Follow the steps below to set up a custom hook:
That's it! Assuming the hook code is properly implemented the hook will fire
as appropriate.
+## Set a global Git hook for all repositories
+
+To create a Git hook that applies to all of your repositories in
+your instance, set a global Git hook. Since all the repositories' `hooks`
+directories are symlinked to gitlab-shell's `hooks` directory, adding any hook
+to the gitlab-shell `hooks` directory will also apply it to all repositories. Follow
+the steps below to properly set up a custom hook all for repositories:
+
+1. On the GitLab server, navigate to the configured custom hook directory. The
+ default is in the gitlab-shell directory. The gitlab-shell `hook` directory
+ for an installation from source the path is usually
+ `/home/git/gitlab-shell/hooks`. For Omnibus installs the path is usually
+ `/opt/gitlab/embedded/service/gitlab-shell/hooks`.
+ To look in a different directory for the global custom hooks,
+ set `custom_hooks_dir` in the gitlab-shell config. For
+ Omnibus installations, this can be set in `gitlab.rb`; and in source
+ installations, this can be set in `gitlab-shell/config.yml`.
+1. Create a new directory in this location. Depending on your hook, it will be
+ either a `pre-receive.d`, `post-receive.d`, or `update.d` directory.
+1. Inside this new directory, add your hook. Hooks can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+1. Make the hook file executable and make sure it's owned by Git.
+
+Now test the hook to see that it's functioning properly.
+
## Chained hooks support
> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
-Hooks can be also placed in `hooks/<hook_name>.d` (global) or
-`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+Hooks can be also global or be set per project directories and support a chained
execution of the hooks.
-NOTE: **Note:** `<hook_name>.d` would need to be either `pre-receive.d`,
+NOTE: **Note:**
+`<hook_name>.d` would need to be either `pre-receive.d`,
`post-receive.d`, or `update.d` to work properly. Any other names will be ignored.
-To look in a different directory for the global custom hooks (those in
-`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
-Omnibus installations, this can be set in `gitlab.rb`; and in source
-installations, this can be set in `gitlab-shell/config.yml`.
+NOTE: **Note:**
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
The hooks are searched and executed in this order:
1. `gitlab-shell/hooks` directory as known to Gitaly
-1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
+1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is symlinked to `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
-Files in `.d` directories need to be executable and not match the backup file
-pattern (`*~`).
-
The hooks of the same type are executed in order and execution stops on the
first script exiting with a non-zero value.
diff --git a/doc/analytics/README.md b/doc/analytics/README.md
new file mode 100644
index 00000000000..6b63edb5174
--- /dev/null
+++ b/doc/analytics/README.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter)
diff --git a/doc/analytics/contribution_analytics.md b/doc/analytics/contribution_analytics.md
new file mode 100644
index 00000000000..38d71263bc1
--- /dev/null
+++ b/doc/analytics/contribution_analytics.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html).
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ed4b6281acc..7992af15448 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -93,6 +93,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -227,6 +235,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -351,6 +367,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -445,6 +469,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -629,6 +661,14 @@ Parameters:
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon",
"web_url" : "https://gitlab.example.com/root"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 4,
"target_project_id": 4,
"labels": [ ],
@@ -718,6 +758,7 @@ POST /projects/:id/merge_requests
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
@@ -843,6 +884,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
@@ -885,6 +927,14 @@ Must include at least one non-required attribute from above.
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1030,6 +1080,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1180,6 +1238,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1436,6 +1502,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1557,6 +1631,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1698,6 +1780,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
"web_url": "https://gitlab.example.com/barrett.krajcik"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 3,
"target_project_id": 3,
"labels": [],
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 3b76c19dc07..90d8c033cd6 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -92,7 +92,7 @@ Parameters:
- `description` (optional) - The description of a milestone
- `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone
-- `state_event` (optional) - The state event of the milestone (close|activate)
+- `state_event` (optional) - The state event of the milestone (close or activate)
## Delete project milestone
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index d03c0b68daf..799217c9a08 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -71,11 +71,11 @@ merge request widget.
NOTE: **Note:**
If you also want the ability to browse JUnit output files, include the
-[`artifacts:paths`](yaml/README.md#artifactspaths) keyword.
+[`artifacts:paths`](yaml/README.md#artifactspaths) keyword. An example of this is shown in the Ruby example below.
### Ruby example
-Use the following job in `.gitlab-ci.yml`:
+Use the following job in `.gitlab-ci.yml`. This includes the `artifacts:paths` keyword to provide a link to the JUnit output file.
```yaml
## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec
@@ -85,6 +85,8 @@ ruby:
- bundle install
- rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml
artifacts:
+ paths:
+ - rspec.xml
reports:
junit: rspec.xml
```
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 13efe68ef4e..60a8ffacd76 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -311,7 +311,7 @@ the guide here.
sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
# Disable Redis listening on TCP by setting 'port' to 0
-sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
+sudo sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
# Enable Redis socket for default Debian / Ubuntu path
echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
@@ -320,9 +320,9 @@ echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf
# Create the directory which contains the socket
-mkdir /var/run/redis
-chown redis:redis /var/run/redis
-chmod 755 /var/run/redis
+sudo mkdir -p /var/run/redis
+sudo chown redis:redis /var/run/redis
+sudo chmod 755 /var/run/redis
# Persist the directory which contains the socket, if applicable
if [ -d /etc/tmpfiles.d ]; then
@@ -384,7 +384,7 @@ sudo chmod -R u+rwX tmp/pids/
sudo chmod -R u+rwX tmp/sockets/
# Create the public/uploads/ directory
-sudo -u git -H mkdir public/uploads/
+sudo -u git -H mkdir -p public/uploads/
# Make sure only the GitLab user has access to the public/uploads/ directory
# now that files in public/uploads are served by gitlab-workhorse
@@ -540,6 +540,7 @@ sudo -u git -H make
```sh
# Fetch Gitaly source with Git and compile with Go
+cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production
```
@@ -567,6 +568,7 @@ For more information about configuring Gitaly see
### Initialize Database and Activate Advanced Features
```sh
+cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
diff --git a/doc/integration/github.md b/doc/integration/github.md
index bee68688ace..e145afbdd5e 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
1. Provide the required details.
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com`
+ - Homepage URL: The URL of your GitLab installation. For example, `https://gitlab.example.com`.
- Application description: Fill this in if you wish.
- Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth/github/callback`. Please make sure the port is included if your GitLab instance is not configured on default port.
![Register OAuth App](img/github_register_app.png)
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index d1729d70158..71651fcf421 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -78,6 +78,33 @@ git push
In case you're running an older version of Git (< 2.9), consider upgrading
to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]).
+## `ssh_exchange_identification` error
+
+Users may experience the following error when attempting to push or pull
+using Git over SSH:
+
+```
+Please make sure you have the correct access rights
+and the repository exists.
+...
+ssh_exchange_identification: read: Connection reset by peer
+fatal: Could not read from remote repository.
+```
+
+This error usually indicates that SSH daemon's `MaxStartups` value is throttling
+SSH connections. This setting specifies the maximum number of unauthenticated
+connections to the SSH daemon. This affects users with proper authentication
+credentials (SSH keys) because every connection is 'unauthenticated' in the
+beginning. The default value is `10`.
+
+Increase `MaxStartups` by adding or modifying the value in `/etc/ssh/sshd_config`:
+
+```
+MaxStartups 100
+```
+
+Restart SSHD for the change to take effect.
+
## Timeout during git push/pull
If pulling/pushing from/to your repository ends up taking more than 50 seconds,
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 9c045dfcce4..73d61bedde1 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -42,7 +42,7 @@ Group labels appear in every label list page of the group's child projects.
### New project label from sidebar
-From the sidebar of an issue or a merge request, you can create a create a new **project label** inline immediately, instead of navigating to the project label list page.
+From the sidebar of an issue or a merge request, you can create a new **project label** inline immediately, instead of navigating to the project label list page.
![Labels inline](img/new_label_from_sidebar.gif)
diff --git a/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
new file mode 100644
index 00000000000..9ae6e350798
--- /dev/null
+++ b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ba7d05a7ad7..2765a32c845 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -169,6 +169,28 @@ can easily apply them to the codebase directly from the UI. Read
through the documentation on [Suggest changes](../../discussions/index.md#suggest-changes)
to learn more.
+## Multiple assignees **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004)
+in [GitLab Starter 11.11](https://about.gitlab.com/pricing).
+
+Multiple people often review merge requests at the same time. GitLab allows you to have multiple assignees for merge requests to indicate everyone that is reviewing or accountable for it.
+
+![multiple assignees for merge requests sidebar](img/multiple_assignees_for_merge_requests_sidebar.png)
+
+To assign multiple assignees to a merge request:
+
+1. From a merge request, expand the right sidebar and locate the **Assignees** section.
+1. Click on **Edit** and from the dropdown menu, select as many users as you want
+to assign the merge request to.
+
+Similarly, assignees are removed by deselecting them from the same dropdown menu.
+
+It's also possible to manage multiple assignees:
+
+- When creating a merge request.
+- Using [quick actions](../quick_actions.md#quick-actions-for-issues-and-merge-requests).
+
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 88f4de891a1..2040e2ee004 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -26,9 +26,10 @@ discussions, and descriptions:
| `/award :emoji:` | Toggle emoji award | ✓ | ✓ |
| `/assign me` | Assign yourself | ✓ | ✓ |
| `/assign @user` | Assign one user | ✓ | ✓ |
-| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | |
-| `/unassign` | Remove assignee(s) | ✓ | ✓ |
-| `/reassign @user1 @user2` | Change assignee | ✓ | ✓ |
+| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | ✓ |
+| `/unassign @user1 @user2` | Remove assignee(s) **[STARTER]** | ✓ | ✓ |
+| `/reassign @user1 @user2` | Change assignee **[STARTER]** | ✓ | ✓ |
+| `/unassign` | Remove current assignee | ✓ | ✓ |
| `/milestone %milestone` | Set milestone | ✓ | ✓ |
| `/remove_milestone` | Remove milestone | ✓ | ✓ |
| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ |
diff --git a/jest.config.js b/jest.config.js
index fdbbe977f0b..0868547e654 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -24,6 +24,7 @@ module.exports = {
'^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
'\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
+ 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index 7f80a6e9285..263221329ab 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -20,16 +20,26 @@ dependency_scanning:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
+ - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
- |
docker run \
- --env DS_ANALYZER_IMAGES \
- --env DS_ANALYZER_IMAGE_PREFIX \
- --env DS_ANALYZER_IMAGE_TAG \
- --env DS_DEFAULT_ANALYZERS \
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS \
- --env DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env DS_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env DS_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ DS_ANALYZER_IMAGES \
+ DS_ANALYZER_IMAGE_PREFIX \
+ DS_ANALYZER_IMAGE_TAG \
+ DS_DEFAULT_ANALYZERS \
+ DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ DS_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_VERSION" /code
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index b941e89991e..f0152cd4537 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -20,18 +20,28 @@ sast:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
+ - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
- |
docker run \
- --env SAST_ANALYZER_IMAGES \
- --env SAST_ANALYZER_IMAGE_PREFIX \
- --env SAST_ANALYZER_IMAGE_TAG \
- --env SAST_DEFAULT_ANALYZERS \
- --env SAST_BRAKEMAN_LEVEL \
- --env SAST_GOSEC_LEVEL \
- --env SAST_FLAWFINDER_LEVEL \
- --env SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env SAST_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ SAST_ANALYZER_IMAGES \
+ SAST_ANALYZER_IMAGE_PREFIX \
+ SAST_ANALYZER_IMAGE_TAG \
+ SAST_DEFAULT_ANALYZERS \
+ SAST_BRAKEMAN_LEVEL \
+ SAST_GOSEC_LEVEL \
+ SAST_FLAWFINDER_LEVEL \
+ SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
+ SAST_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 2770469ca9f..9fc2217ad43 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -16,6 +16,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
+ metrics_dashboard: %r{\A\.gitlab/dashboards/[^/]+\.yml\z},
xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index a56ca1e39e7..04dabe423e8 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
+ include Singleton
+
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccess,
@@ -19,7 +21,7 @@ module Gitlab
}.freeze
def self.types
- TYPES
+ instance.types
end
def self.parse(gl_repository)
@@ -39,5 +41,11 @@ module Gitlab
def self.default_type
PROJECT
end
+
+ def types
+ TYPES
+ end
+
+ private_class_method :instance
end
end
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 7abe6c29a25..19915980d7f 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
class RepoType
attr_reader :name,
:access_checker_class,
diff --git a/lib/gitlab/metrics/dashboard/base_service.rb b/lib/gitlab/metrics/dashboard/base_service.rb
new file mode 100644
index 00000000000..94aabd0466c
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/base_service.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+module Gitlab
+ module Metrics
+ module Dashboard
+ class BaseService < ::BaseService
+ DASHBOARD_LAYOUT_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ def get_dashboard
+ return error("#{dashboard_path} could not be found.", :not_found) unless path_available?
+
+ success(dashboard: process_dashboard)
+ rescue DASHBOARD_LAYOUT_ERROR => e
+ error(e.message, :unprocessable_entity)
+ end
+
+ # Summary of all known dashboards for the service.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def all_dashboard_paths(_project)
+ raise NotImplementedError
+ end
+
+ private
+
+ # Returns a new dashboard Hash, supplemented with DB info
+ def process_dashboard
+ Gitlab::Metrics::Dashboard::Processor
+ .new(project, params[:environment], raw_dashboard)
+ .process(insert_project_metrics: insert_project_metrics?)
+ end
+
+ # @return [String] Relative filepath of the dashboard yml
+ def dashboard_path
+ params[:dashboard_path]
+ end
+
+ # Returns an un-processed dashboard from the cache.
+ def raw_dashboard
+ Rails.cache.fetch(cache_key) { get_raw_dashboard }
+ end
+
+ # @return [Hash] an unmodified dashboard
+ def get_raw_dashboard
+ raise NotImplementedError
+ end
+
+ # @return [String]
+ def cache_key
+ raise NotImplementedError
+ end
+
+ # Determines whether custom metrics should be included
+ # in the processed output.
+ def insert_project_metrics?
+ false
+ end
+
+ # Checks if dashboard path exists or should be rejected
+ # as a result of file-changes to the project repository.
+ # @return [Boolean]
+ def path_available?
+ available_paths = Gitlab::Metrics::Dashboard::Finder.find_all_paths(project)
+
+ available_paths.any? do |path_params|
+ path_params[:path] == dashboard_path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
new file mode 100644
index 00000000000..4a41590f000
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Returns DB-supplmented dashboard info for determining
+# the layout of UI. Intended entry-point for the Metrics::Dashboard
+# module.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Finder
+ class << self
+ # Returns a formatted dashboard packed with DB info.
+ # @return [Hash]
+ def find(project, user, environment, dashboard_path = nil)
+ service = system_dashboard?(dashboard_path) ? system_service : project_service
+
+ service
+ .new(project, user, environment: environment, dashboard_path: dashboard_path)
+ .get_dashboard
+ end
+
+ # Summary of all known dashboards.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def find_all_paths(project)
+ project.repository.metrics_dashboard_paths
+ end
+
+ # Summary of all known dashboards. Used to populate repo cache.
+ # Prefer #find_all_paths.
+ def find_all_paths_from_source(project)
+ system_service.all_dashboard_paths(project)
+ .+ project_service.all_dashboard_paths(project)
+ end
+
+ private
+
+ def system_service
+ Gitlab::Metrics::Dashboard::SystemDashboardService
+ end
+
+ def project_service
+ Gitlab::Metrics::Dashboard::ProjectDashboardService
+ end
+
+ def system_dashboard?(filepath)
+ !filepath || system_service.system_dashboard?(filepath)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index cc34ac53051..dd986020693 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -8,12 +8,17 @@ module Gitlab
# the UI. These includes shared metric info, custom metrics
# info, and alerts (only in EE).
class Processor
- SEQUENCE = [
+ SYSTEM_SEQUENCE = [
Stages::CommonMetricsInserter,
Stages::ProjectMetricsInserter,
Stages::Sorter
].freeze
+ PROJECT_SEQUENCE = [
+ Stages::CommonMetricsInserter,
+ Stages::Sorter
+ ].freeze
+
def initialize(project, environment, dashboard)
@project = project
@environment = environment
@@ -22,9 +27,9 @@ module Gitlab
# Returns a new dashboard hash with the results of
# running transforms on the dashboard.
- def process
+ def process(insert_project_metrics:)
@dashboard.deep_symbolize_keys.tap do |dashboard|
- sequence.each do |stage|
+ sequence(insert_project_metrics).each do |stage|
stage.new(@project, @environment, dashboard).transform!
end
end
@@ -32,8 +37,8 @@ module Gitlab
private
- def sequence
- SEQUENCE
+ def sequence(insert_project_metrics)
+ insert_project_metrics ? SYSTEM_SEQUENCE : PROJECT_SEQUENCE
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/project_dashboard_service.rb b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
new file mode 100644
index 00000000000..fdffd067c93
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class ProjectDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ DASHBOARD_ROOT = ".gitlab/dashboards"
+
+ class << self
+ def all_dashboard_paths(project)
+ file_finder(project)
+ .list_files_for(DASHBOARD_ROOT)
+ .map do |filepath|
+ Rails.cache.delete(cache_key(project.id, filepath))
+
+ { path: filepath, default: false }
+ end
+ end
+
+ def file_finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, DASHBOARD_ROOT, '.yml')
+ end
+
+ def cache_key(id, dashboard_path)
+ "project_#{id}_metrics_dashboard_#{dashboard_path}"
+ end
+ end
+
+ private
+
+ # Searches the project repo for a custom-defined dashboard.
+ def get_raw_dashboard
+ yml = self.class.file_finder(project).read(dashboard_path)
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ self.class.cache_key(project.id, dashboard_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/service.rb b/lib/gitlab/metrics/dashboard/service.rb
deleted file mode 100644
index 79d563cce4f..00000000000
--- a/lib/gitlab/metrics/dashboard/service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-# Fetches the metrics dashboard layout and supplemented the output with DB info.
-module Gitlab
- module Metrics
- module Dashboard
- class Service < ::BaseService
- SYSTEM_DASHBOARD_NAME = 'common_metrics'
- SYSTEM_DASHBOARD_PATH = Rails.root.join('config', 'prometheus', "#{SYSTEM_DASHBOARD_NAME}.yml")
-
- # Returns a DB-supplemented json representation of a dashboard config file.
- def get_dashboard
- dashboard_string = Rails.cache.fetch(cache_key) { system_dashboard }
-
- dashboard = process_dashboard(dashboard_string)
-
- success(dashboard: dashboard)
- rescue Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError => e
- error(e.message, :unprocessable_entity)
- end
-
- private
-
- # Returns the base metrics shipped with every GitLab service.
- def system_dashboard
- YAML.safe_load(File.read(SYSTEM_DASHBOARD_PATH))
- end
-
- def cache_key
- "metrics_dashboard_#{SYSTEM_DASHBOARD_NAME}"
- end
-
- # Returns a new dashboard Hash, supplemented with DB info
- def process_dashboard(dashboard)
- Gitlab::Metrics::Dashboard::Processor.new(project, params[:environment], dashboard).process
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/dashboard/stages/base_stage.rb b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
index dd4aae6c115..a6d1f974556 100644
--- a/lib/gitlab/metrics/dashboard/stages/base_stage.rb
+++ b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
@@ -36,7 +36,7 @@ module Gitlab
raise DashboardLayoutError.new('Each "panel" must define an array :metrics')
end
- def for_metrics(dashboard)
+ def for_metrics
missing_panel_groups! unless dashboard[:panel_groups].is_a?(Array)
dashboard[:panel_groups].each do |panel_group|
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
index 3406021bbea..188912bedb4 100644
--- a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -11,7 +11,7 @@ module Gitlab
def transform!
common_metrics = ::PrometheusMetric.common
- for_metrics(dashboard) do |metric|
+ for_metrics do |metric|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
metric[:metric_id] = metric_record.id if metric_record
end
diff --git a/lib/gitlab/metrics/dashboard/system_dashboard_service.rb b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
new file mode 100644
index 00000000000..67509ed4230
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Fetches the system metrics dashboard and formats the output.
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class SystemDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ SYSTEM_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
+
+ class << self
+ def all_dashboard_paths(_project)
+ [{
+ path: SYSTEM_DASHBOARD_PATH,
+ default: true
+ }]
+ end
+
+ def system_dashboard?(filepath)
+ filepath == SYSTEM_DASHBOARD_PATH
+ end
+ end
+
+ private
+
+ def dashboard_path
+ SYSTEM_DASHBOARD_PATH
+ end
+
+ # Returns the base metrics shipped with every GitLab service.
+ def get_raw_dashboard
+ yml = File.read(Rails.root.join(dashboard_path))
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ "metrics_dashboard_#{dashboard_path}"
+ end
+
+ def insert_project_metrics?
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8718df54a7e..1ebb0e603cd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -810,6 +810,9 @@ msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
+msgid "An error occurred while fetching label colors."
+msgstr ""
+
msgid "An error occurred while fetching markdown preview"
msgstr ""
@@ -819,6 +822,9 @@ msgstr ""
msgid "An error occurred while fetching stages."
msgstr ""
+msgid "An error occurred while fetching the board lists. Please try again."
+msgstr ""
+
msgid "An error occurred while fetching the job log."
msgstr ""
@@ -855,6 +861,9 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
+msgid "An error occurred while parsing recent searches"
+msgstr ""
+
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -963,6 +972,15 @@ msgstr ""
msgid "Apply suggestion"
msgstr ""
+msgid "Applying command"
+msgstr ""
+
+msgid "Applying command to %{commandDescription}"
+msgstr ""
+
+msgid "Applying multiple commands"
+msgstr ""
+
msgid "Apr"
msgstr ""
@@ -993,6 +1011,9 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
+msgid "Are you sure you want to delete this list?"
+msgstr ""
+
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
@@ -1185,6 +1206,9 @@ msgstr ""
msgid "Automatically resolved"
msgstr ""
+msgid "Autosave|Note"
+msgstr ""
+
msgid "Available"
msgstr ""
@@ -1593,6 +1617,9 @@ msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
+msgid "Can't find variable: ZiteReader"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -2409,9 +2436,15 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "ComboSearch is not defined"
+msgstr ""
+
msgid "Command line instructions"
msgstr ""
+msgid "Commands applied"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -2672,6 +2705,9 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
+msgid "Copied"
+msgstr ""
+
msgid "Copy %{http_label} clone URL"
msgstr ""
@@ -4036,6 +4072,9 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failed to update tag!"
+msgstr ""
+
msgid "Failed to update."
msgstr ""
@@ -6005,6 +6044,9 @@ msgstr ""
msgid "No %{providerTitle} repositories available to import"
msgstr ""
+msgid "No Milestone"
+msgstr ""
+
msgid "No Tag"
msgstr ""
@@ -6149,6 +6191,9 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Nothing to preview."
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -6661,6 +6706,9 @@ msgstr ""
msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please add a list to your board first"
+msgstr ""
+
msgid "Please choose a group URL with no special characters."
msgstr ""
@@ -6724,6 +6772,9 @@ msgstr ""
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr ""
+msgid "Press %{key}-C to copy"
+msgstr ""
+
msgid "Press Enter or click to search"
msgstr ""
@@ -7402,6 +7453,9 @@ msgstr ""
msgid "Protected"
msgstr ""
+msgid "Protected Tag"
+msgstr ""
+
msgid "Protip:"
msgstr ""
@@ -7507,6 +7561,9 @@ msgstr ""
msgid "Recent Project Activity"
msgstr ""
+msgid "Recent Searches Service is unavailable"
+msgstr ""
+
msgid "Recent searches"
msgstr ""
@@ -7925,6 +7982,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
+msgid "Save anyway"
+msgstr ""
+
msgid "Save application"
msgstr ""
@@ -8428,6 +8488,9 @@ msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
+msgid "Something went wrong while adding your award. Please try again."
+msgstr ""
+
msgid "Something went wrong while applying the suggestion. Please try again."
msgstr ""
@@ -8440,6 +8503,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching latest comments."
+msgstr ""
+
msgid "Something went wrong while fetching related merge requests."
msgstr ""
@@ -8833,6 +8899,9 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
+msgid "Table of Contents"
+msgstr ""
+
msgid "Tag"
msgstr ""
@@ -8956,6 +9025,9 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Test settings and save changes"
+msgstr ""
+
msgid "TestHooks|Ensure one of your projects has merge requests."
msgstr ""
@@ -9940,6 +10012,9 @@ msgstr ""
msgid "Unknown format"
msgstr ""
+msgid "Unknown response text"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -10369,6 +10444,9 @@ msgstr ""
msgid "Webhooks Help"
msgstr ""
+msgid "Welcome to your Issue Board!"
+msgstr ""
+
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
@@ -10563,6 +10641,9 @@ msgstr ""
msgid "Yes"
msgstr ""
+msgid "Yes or No"
+msgstr ""
+
msgid "Yes, add it"
msgstr ""
@@ -10611,6 +10692,12 @@ msgstr ""
msgid "You can also create a project from the command line."
msgstr ""
+msgid "You can also press &#8984;-Enter"
+msgstr ""
+
+msgid "You can also press Ctrl-Enter"
+msgstr ""
+
msgid "You can also star a label to make it a priority label."
msgstr ""
@@ -11068,6 +11155,9 @@ msgstr ""
msgid "it is too large"
msgstr ""
+msgid "jigsaw is not defined"
+msgstr ""
+
msgid "latest"
msgstr ""
diff --git a/package.json b/package.json
index 6fd364251e8..3410bcaa9f5 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,7 @@
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
- "jest": "BABEL_ENV=jest jest",
- "jest-debug": "BABEL_ENV=jest node --inspect-brk node_modules/.bin/jest --runInBand",
+ "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc73225c1bf..f39c64339fe 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -35,7 +35,7 @@ fi
# Do not use 'README.md', instead use 'index.md'
# Number of 'README.md's as of 2018-03-26
-NUMBER_READMES_CE=43
+NUMBER_READMES_CE=44
NUMBER_READMES_EE=46
FIND_READMES=$(find doc/ -name "README.md" | wc -l)
echo '=> Checking for new README.md files...'
diff --git a/security.txt b/security.txt
new file mode 100644
index 00000000000..f7adb43fda6
--- /dev/null
+++ b/security.txt
@@ -0,0 +1,6 @@
+Contact: security@gitlab.com
+Acknowledgments: https://about.gitlab.com/security/vulnerability-acknowledgements/
+Preferred-Languages: en
+Canonical: https://about.gitlab.com/security/disclosure/
+Policy: https://hackerone.com/gitlab
+Hiring: https://about.gitlab.com/jobs/
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index a62422d0229..cf23d937037 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -474,25 +474,102 @@ describe Projects::EnvironmentsController do
end
end
- context 'when prometheus endpoint is enabled' do
+ shared_examples_for '200 response' do |contains_all_dashboards: false|
+ let(:expected_keys) { %w(dashboard status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
it 'returns a json representation of the environment dashboard' do
- get :metrics_dashboard, params: environment_params(format: :json)
+ get :metrics_dashboard, params: environment_params(dashboard_params)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.keys).to contain_exactly('dashboard', 'status')
+ expect(json_response.keys).to contain_exactly(*expected_keys)
expect(json_response['dashboard']).to be_an_instance_of(Hash)
end
+ end
+
+ shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
+ let(:expected_keys) { %w(message status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns an error response' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(status_code)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ end
+ end
+
+ shared_examples_for 'has all dashboards' do
+ it 'includes an index of all available dashboards' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(json_response.keys).to include('all_dashboards')
+ expect(json_response['all_dashboards']).to be_an_instance_of(Array)
+ expect(json_response['all_dashboards']).to all( include('path', 'default') )
+ end
+ end
+
+ context 'when multiple dashboards is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
+ end
+
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response'
context 'when the dashboard could not be provided' do
before do
allow(YAML).to receive(:safe_load).and_return({})
end
- it 'returns an error response' do
- get :metrics_dashboard, params: environment_params(format: :json)
+ it_behaves_like 'error response', :unprocessable_entity
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }
+
+ it_behaves_like '200 response'
+ end
+ end
+
+ context 'when multiple dashboards is enabled' do
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+
+ context 'when a dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }
+
+ context 'when the dashboard is available' do
+ let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
+ let(:project) { create(:project, :custom_repo, files: dashboard_file) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response.keys).to contain_exactly('message', 'status', 'http_status')
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'error response', :not_found, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
end
end
end
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
index c2d3d3d8aca..638ecbcc11f 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -2,7 +2,7 @@ dashboard: 'Test Dashboard'
priority: 1
panel_groups:
- group: Group A
- priority: 10
+ priority: 1
panels:
- title: "Super Chart A1"
type: "area-chart"
@@ -23,7 +23,7 @@ panel_groups:
label: Legend Label
unit: unit
- group: Group B
- priority: 1
+ priority: 10
panels:
- title: "Super Chart B"
type: "area-chart"
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
new file mode 100644
index 00000000000..392c1b6533e
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -0,0 +1,139 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import '~/behaviors/markdown/render_gfm';
+import { SYSTEM_NOTE } from '~/notes/constants';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import NoteableNote from '~/notes/components/noteable_note.vue';
+import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import createStore from '~/notes/stores';
+import {
+ noteableDataMock,
+ discussionMock,
+ notesDataMock,
+} from '../../../javascripts/notes/mock_data';
+
+const localVue = createLocalVue();
+
+describe('DiscussionNotes', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ const store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ wrapper = mount(DiscussionNotes, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isExpanded: false,
+ shouldGroupReplies: false,
+ ...props,
+ },
+ scopedSlots: {
+ footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ },
+ slots: {
+ 'avatar-badge': '<span class="avatar-badge-slot-content" />',
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('renders an element for each note in the discussion', () => {
+ createComponent();
+ const notesCount = discussionMock.notes.length;
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(notesCount);
+ });
+
+ it('renders one element if replies groupping is enabled', () => {
+ createComponent({ shouldGroupReplies: true });
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(1);
+ });
+
+ it('uses proper component to render each note type', () => {
+ const discussion = { ...discussionMock };
+ const notesData = [
+ // PlaceholderSystemNote
+ {
+ id: 1,
+ isPlaceholderNote: true,
+ placeholderType: SYSTEM_NOTE,
+ notes: [{ body: 'PlaceholderSystemNote' }],
+ },
+ // PlaceholderNote
+ {
+ id: 2,
+ isPlaceholderNote: true,
+ notes: [{ body: 'PlaceholderNote' }],
+ },
+ // SystemNote
+ {
+ id: 3,
+ system: true,
+ note: 'SystemNote',
+ },
+ // NoteableNote
+ discussion.notes[0],
+ ];
+ discussion.notes = notesData;
+ createComponent({ discussion });
+ const notes = wrapper.findAll('.notes > li');
+
+ expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
+ expect(notes.at(1).is(PlaceholderNote)).toBe(true);
+ expect(notes.at(2).is(SystemNote)).toBe(true);
+ expect(notes.at(3).is(NoteableNote)).toBe(true);
+ });
+
+ it('renders footer scoped slot with showReplies === true when expanded', () => {
+ createComponent({ isExpanded: true });
+ expect(wrapper.text()).toMatch('showReplies:true');
+ });
+
+ it('renders footer scoped slot with showReplies === false when collapsed', () => {
+ createComponent({ isExpanded: false });
+ expect(wrapper.text()).toMatch('showReplies:false');
+ });
+
+ it('passes down avatar-badge slot content', () => {
+ createComponent();
+ expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
+ });
+ });
+
+ describe('componentData', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should return first note object for placeholder note', () => {
+ const data = {
+ isPlaceholderNote: true,
+ notes: [{ body: 'hello world!' }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data.notes[0]);
+ });
+
+ it('should return given note for nonplaceholder notes', () => {
+ const data = {
+ notes: [{ id: 12 }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
index 68b4f261617..33210794ba1 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -1,5 +1,7 @@
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
-import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const bmprPath = `${FIXTURES_PATH}/blob/balsamiq/test.bmpr`;
describe('Balsamiq integration spec', () => {
let container;
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index acf87580777..6fa3890483c 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
import renderPDF from '~/blob/pdf';
-import testPDF from '../../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let viewer;
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 0bc9da5ad0f..f7f0ab83c21 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,90 +1,103 @@
-import Vue from 'vue';
+import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+const localVue = createLocalVue();
+
describe('DiffDiscussions', () => {
- let vm;
+ let store;
+ let wrapper;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- function createComponent(props = {}) {
- const store = createStore();
-
- vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
- discussions: getDiscussionsMockData(),
- ...props,
- }).$mount();
- }
+ const createComponent = props => {
+ store = createStore();
+ wrapper = mount(localVue.extend(DiffDiscussions), {
+ store,
+ propsData: {
+ discussions: getDiscussionsMockData(),
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ expect(wrapper.find(NoteableDiscussion).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe(
+ discussionsMockData.notes.length,
+ );
});
});
describe('image commenting', () => {
+ const findDiffNotesToggle = () => wrapper.find('.js-diff-notes-toggle');
+
it('renders collapsible discussion button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
- 'diff-notes-collapse',
- );
+ expect(diffNotesToggle.exists()).toBe(true);
+ expect(diffNotesToggle.find(Icon).exists()).toBe(true);
+ expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ spyOn(wrapper.vm.$store, 'dispatch').and.stub();
+ const diffNotesToggle = findDiffNotesToggle();
+ diffNotesToggle.trigger('click');
- spyOn(vm.$store, 'dispatch').and.stub();
-
- vm.$el.querySelector('.js-diff-notes-toggle').click();
-
- expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
- discussionId: vm.discussions[0].id,
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ discussionId: discussionsMockData.id,
});
});
- it('renders expand button when discussion is collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
-
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
- expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
- 'btn-transparent badge badge-pill',
- );
+ it('renders expand button when discussion is collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- done();
- });
+ expect(diffNotesToggle.text().trim()).toBe('1');
+ expect(diffNotesToggle.classes()).toEqual(
+ jasmine.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
+ );
});
- it('hides discussion when collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
+ it('hides discussion when collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
-
- done();
- });
+ expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
- createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
-
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
+ createComponent({ renderAvatarBadge: true });
+ const noteableDiscussion = wrapper.find(NoteableDiscussion);
+
+ expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
+ expect(
+ noteableDiscussion
+ .find('.badge-pill')
+ .text()
+ .trim(),
+ ).toBe('1');
});
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 3304c79cdb7..efa864e7d00 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -130,29 +130,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('componentData', () => {
- it('should return first note object for placeholder note', () => {
- const data = {
- isPlaceholderNote: true,
- notes: [{ body: 'hello world!' }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data.notes[0]);
- });
-
- it('should return given note for nonplaceholder notes', () => {
- const data = {
- notes: [{ id: 12 }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data);
- });
- });
-
describe('action text', () => {
const commitId = 'razupaltuff';
const truncatedCommitId = commitId.substr(0, 8);
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
index 6df4ded92f0..7191b65b4cd 100644
--- a/spec/javascripts/pdf/index_spec.js
+++ b/spec/javascripts/pdf/index_spec.js
@@ -3,7 +3,9 @@ import { GlobalWorkerOptions } from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
import PDFLab from '~/pdf/index.vue';
-import pdf from '../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
GlobalWorkerOptions.workerSrc = workerSrc;
const Component = Vue.extend(PDFLab);
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
index b2e9fa42a63..f899b5b3a0d 100644
--- a/spec/javascripts/pdf/page_spec.js
+++ b/spec/javascripts/pdf/page_spec.js
@@ -4,7 +4,9 @@ import workerSrc from 'vendor/pdf.worker.min';
import PageComponent from '~/pdf/page/index.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import testPDF from 'spec/fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('Page component', () => {
const Component = Vue.extend(PageComponent);
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
new file mode 100644
index 00000000000..e88eb140b35
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+ let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
+
+ describe '.find' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_call) { described_class.find(project, nil, environment, dashboard_path) }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the system dashboard is specified' do
+ let(:dashboard_path) { system_dashboard_path }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when no dashboard is specified' do
+ let(:service_call) { described_class.find(project, nil, environment) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+
+ describe '.find_all_paths' do
+ let(:all_dashboard_paths) { described_class.find_all_paths(project) }
+ let(:system_dashboard) { { path: system_dashboard_path, default: true } }
+
+ it 'includes only the system dashboard by default' do
+ expect(all_dashboard_paths).to eq([system_dashboard])
+ end
+
+ context 'when the project contains dashboards' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it 'includes system and project dashboards' do
+ project_dashboard = { path: dashboard_path, default: false }
+
+ expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
index ee7c93fce8d..be3c1095bd7 100644
--- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -4,12 +4,12 @@ require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Processor do
let(:project) { build(:project) }
- let(:environment) { build(:environment) }
+ let(:environment) { build(:environment, project: project) }
let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
describe 'process' do
let(:process_params) { [project, environment, dashboard_yml] }
- let(:dashboard) { described_class.new(*process_params).process }
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) }
context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
@@ -35,9 +35,9 @@ describe Gitlab::Metrics::Dashboard::Processor do
it 'orders groups by priority and panels by weight' do
expected_metrics_order = [
- 'metric_a2', # group priority 10, panel weight 2
- 'metric_a1', # group priority 10, panel weight 1
- 'metric_b', # group priority 1, panel weight 1
+ 'metric_b', # group priority 10, panel weight 1
+ 'metric_a2', # group priority 1, panel weight 2
+ 'metric_a1', # group priority 1, panel weight 1
project_business_metric.id, # group priority 0, panel weight nil (0)
project_response_metric.id, # group priority -5, panel weight nil (0)
project_system_metric.id, # group priority -10, panel weight nil (0)
@@ -46,6 +46,17 @@ describe Gitlab::Metrics::Dashboard::Processor do
expect(actual_metrics_order).to eq expected_metrics_order
end
+
+ context 'when the dashboard should not include project metrics' do
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) }
+
+ it 'includes only dashboard metrics' do
+ metrics = all_metrics.map { |m| m[:id] }
+
+ expect(metrics.length).to be(3)
+ expect(metrics).to eq %w(metric_b metric_a2 metric_a1)
+ end
+ end
end
shared_examples_for 'errors with message' do |expected_message|
diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
new file mode 100644
index 00000000000..162beb0268a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:user) { build(:user) }
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+ end
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'and the dashboard is then deleted' do
+ it 'does not return the previously cached dashboard' do
+ described_class.new(*service_params).get_dashboard
+
+ delete_project_dashboard(project, user, dashboard_path)
+
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ end
+ end
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/service_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_spec.rb
deleted file mode 100644
index e66c356bf49..00000000000
--- a/spec/lib/gitlab/metrics/dashboard/service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Metrics::Dashboard::Service, :use_clean_rails_memory_store_caching do
- let(:project) { build(:project) }
- let(:environment) { build(:environment) }
-
- describe 'get_dashboard' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
-
- it 'returns a json representation of the environment dashboard' do
- result = described_class.new(project, environment).get_dashboard
-
- expect(result.keys).to contain_exactly(:dashboard, :status)
- expect(result[:status]).to eq(:success)
-
- expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
- end
-
- it 'caches the dashboard for subsequent calls' do
- expect(YAML).to receive(:safe_load).once.and_call_original
-
- described_class.new(project, environment).get_dashboard
- described_class.new(project, environment).get_dashboard
- end
-
- context 'when the dashboard is configured incorrectly' do
- before do
- allow(YAML).to receive(:safe_load).and_return({})
- end
-
- it 'returns an appropriate message and status code' do
- result = described_class.new(project, environment).get_dashboard
-
- expect(result.keys).to contain_exactly(:message, :http_status, :status)
- expect(result[:status]).to eq(:error)
- expect(result[:http_status]).to eq(:unprocessable_entity)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
new file mode 100644
index 00000000000..e71ce2481a3
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH }
+ let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'when called with a non-system dashboard' do
+ let(:dashboard_path) { 'garbage/dashboard/path' }
+
+ # We want to alwaus return the system dashboard.
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index fb7f43b25cf..7457efef013 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2123,7 +2123,7 @@ describe MergeRequest do
end
context 'when merges are not restricted to green builds' do
- subject { build(:merge_request, target_project: build(:project, only_allow_merge_if_pipeline_succeeds: false)) }
+ subject { build(:merge_request, target_project: create(:project, only_allow_merge_if_pipeline_succeeds: false)) }
context 'and a failed pipeline is associated' do
before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 43ec1125087..a6c3d5756aa 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1637,6 +1637,7 @@ describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
+ :metrics_dashboard_paths,
:xcode_project?
])
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 53271550e8b..00ca394a50b 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -133,5 +133,19 @@ describe "Groups", "routing" do
let(:resource) { create(:group, parent: parent, path: 'activity') }
end
end
+
+ describe 'subgroup "boards"' do
+ it 'shows group show page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq/boards', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/boards')).to route_to('groups#show', id: 'gitlabhq/boards')
+ end
+
+ it 'shows boards index page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
+ end
+ end
end
end
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
new file mode 100644
index 00000000000..1f36b0e217c
--- /dev/null
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module MetricsDashboardHelpers
+ def project_with_dashboard(dashboard_path, dashboard_yml = nil)
+ dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')
+
+ create(:project, :custom_repo, files: { dashboard_path => dashboard_yml })
+ end
+
+ def delete_project_dashboard(project, user, dashboard_path)
+ project.repository.delete_file(
+ user,
+ dashboard_path,
+ branch_name: 'master',
+ message: 'Delete dashboard'
+ )
+
+ project.repository.refresh_method_caches([:metrics_dashboard])
+ end
+
+ shared_examples_for 'misconfigured dashboard service response' do |status_code|
+ it 'returns an appropriate message and status code' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(status_code)
+ end
+ end
+
+ shared_examples_for 'valid dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it 'returns a json representation of the dashboard' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+ end
+end
diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
index d8f7ba1185e..421303c97be 100644
--- a/spec/support/shared_examples/application_setting_examples.rb
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -260,6 +260,7 @@ RSpec.shared_examples 'application settings examples' do
allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(nil)
expect(setting.sentry_enabled).to eq true
expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
@@ -277,12 +278,13 @@ RSpec.shared_examples 'application settings examples' do
allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
expect(setting).not_to receive(:read_attribute)
expect(setting.sentry_enabled).to eq true
expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
expect(setting.clientside_sentry_enabled).to eq true
- expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43'
end
end
end