summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue19
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js34
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js22
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue121
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue178
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue150
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_item_dropdown.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/emoji/no_emoji_validator.js44
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js9
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue43
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/stores/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js2
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js4
-rw-r--r--app/assets/javascripts/image_diff/view_types.js2
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js6
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js36
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue37
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue11
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue20
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue4
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js15
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue34
-rw-r--r--app/assets/javascripts/operation_settings/index.js9
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js38
-rw-r--r--app/assets/javascripts/operation_settings/store/index.js16
-rw-r--r--app/assets/javascripts/operation_settings/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/operation_settings/store/mutations.js7
-rw-r--r--app/assets/javascripts/operation_settings/store/state.js5
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/form.js43
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue5
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/length_validator.js32
-rw-r--r--app/assets/javascripts/profile/account/index.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js6
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js4
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue1
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue7
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.graphql2
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue36
-rw-r--r--app/assets/javascripts/serverless/constants.js4
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js3
-rw-r--r--app/assets/javascripts/serverless/store/actions.js29
-rw-r--r--app/assets/javascripts/serverless/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/serverless/store/mutations.js15
-rw-r--r--app/assets/javascripts/serverless/store/state.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/validators/input_validator.js34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue121
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss4
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss29
-rw-r--r--app/assets/stylesheets/framework/lists.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss6
-rw-r--r--app/assets/stylesheets/pages/login.scss3
-rw-r--r--app/assets/stylesheets/pages/members.scss59
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss46
-rw-r--r--app/assets/stylesheets/pages/note_form.scss5
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/concerns/import_url_params.rb19
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/dashboard/projects_controller.rb19
-rw-r--r--app/controllers/projects/git_http_client_controller.rb1
-rw-r--r--app/controllers/projects/imports_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/pages_domains_controller.rb4
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/search_controller.rb1
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/finders/clusters/knative_services_finder.rb112
-rw-r--r--app/finders/projects/serverless/functions_finder.rb40
-rw-r--r--app/graphql/gitlab_schema.rb25
-rw-r--r--app/graphql/mutations/merge_requests/base.rb2
-rw-r--r--app/graphql/resolvers/issues_resolver.rb4
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb4
-rw-r--r--app/graphql/types/base_field.rb15
-rw-r--r--app/graphql/types/base_object.rb5
-rw-r--r--app/graphql/types/ci/pipeline_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/graphql/types/project_statistics_type.rb1
-rw-r--r--app/graphql/types/tree/blob_type.rb4
-rw-r--r--app/graphql/types/tree/tree_entry_type.rb4
-rw-r--r--app/graphql/types/tree/tree_type.rb10
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/ci_variables_helper.rb2
-rw-r--r--app/helpers/emails_helper.rb7
-rw-r--r--app/helpers/notifications_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/tracking_helper.rb7
-rw-r--r--app/helpers/visibility_level_helper.rb38
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/ci/pipeline_schedule.rb25
-rw-r--r--app/models/clusters/applications/jupyter.rb11
-rw-r--r--app/models/clusters/applications/knative.rb48
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/commit.rb11
-rw-r--r--app/models/concerns/cache_markdown_field.rb88
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/concerns/noteable.rb12
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/update_project_statistics.rb52
-rw-r--r--app/models/diff_note.rb20
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/merge_request.rb48
-rw-r--r--app/models/notification_recipient.rb8
-rw-r--r--app/models/notification_setting.rb9
-rw-r--r--app/models/project.rb12
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/todo.rb3
-rw-r--r--app/presenters/blob_presenter.rb4
-rw-r--r--app/presenters/ci/build_runner_presenter.rb5
-rw-r--r--app/presenters/member_presenter.rb5
-rw-r--r--app/presenters/tree_entry_presenter.rb9
-rw-r--r--app/serializers/issue_entity.rb8
-rw-r--r--app/services/ci/pipeline_schedule_service.rb13
-rw-r--r--app/services/clusters/applications/base_service.rb2
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb26
-rw-r--r--app/services/merge_requests/close_service.rb5
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb37
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb20
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb82
-rw-r--r--app/services/projects/update_service.rb1
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/service_response.rb15
-rw-r--r--app/services/todos/destroy/base_service.rb2
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb35
-rw-r--r--app/views/abuse_reports/new.html.haml8
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml8
-rw-r--r--app/views/ci/variables/_content.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml12
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml7
-rw-r--r--app/views/profiles/keys/_key_details.html.haml3
-rw-r--r--app/views/projects/_import_project_pane.html.haml21
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml4
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml2
-rw-r--r--app/views/projects/pages_domains/_form.html.haml87
-rw-r--r--app/views/projects/pages_domains/_helper_text.html.haml9
-rw-r--r--app/views/projects/pages_domains/edit.html.haml1
-rw-r--r--app/views/projects/pages_domains/new.html.haml1
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml3
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml3
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml27
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml19
-rw-r--r--app/views/shared/members/_member.html.haml41
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/auto_merge_process_worker.rb14
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb41
-rw-r--r--app/workers/pipeline_success_worker.rb9
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb26
-rw-r--r--app/workers/todos_destroyer/confidential_issue_worker.rb4
240 files changed, 2183 insertions, 1073 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 670f66b005e..c8eb96a625c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts {
}
// Sanity check: Make sure the selected text comes from a discussion : it can either contain a message...
- let foundMessage = !!documentFragment.querySelector('.md');
+ let foundMessage = Boolean(documentFragment.querySelector('.md'));
// ... Or come from a message
if (!foundMessage) {
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index b8882203cc7..179148b6887 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -66,7 +66,7 @@ export default {
eventHub.$emit('clearDetailIssue');
} else {
eventHub.$emit('newDetailIssue', this.issue);
- boardsStore.detail.list = this.list;
+ boardsStore.setListDetail(this.list);
}
}
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index c9972d051aa..b1a8b13f3ac 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -142,8 +142,10 @@ export default {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
- boardsStore.moving.list = card.list;
- boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId);
+
+ const { list } = card;
+ const issue = list.findIssue(Number(e.item.dataset.issueId));
+ boardsStore.startMoving(list, issue);
sortableStart();
},
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index dc1bdc23b5e..cc6af8e88cd 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -72,8 +72,8 @@ export default {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.detail.issue = issue;
- boardsStore.detail.list = this.list;
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 17de7b2cf1e..a8516f178fc 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
@@ -136,23 +135,7 @@ export default {
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
- this.applyFilter(filter);
- },
- applyFilter(filter) {
- const filterPath = boardsStore.filter.path.split('&');
- const filterIndex = filterPath.indexOf(filter);
-
- if (filterIndex === -1) {
- filterPath.push(filter);
- } else {
- filterPath.splice(filterIndex, 1);
- }
-
- boardsStore.filter.path = filterPath.join('&');
-
- boardsStore.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
+ boardsStore.toggleFilter(filter);
},
labelStyle(label) {
return {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 8e09e265cfb..defa1f75ba2 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -124,7 +124,7 @@ export default {
data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ issue.selected = Boolean(foundSelectedIssue);
this.issues.push(issue);
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index bc6a3cf212e..e9cab3e3bba 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -168,7 +168,7 @@ export default () => {
});
}
- boardsStore.detail.issue = newIssue;
+ boardsStore.setIssueDetail(newIssue);
},
clearDetailIssue() {
boardsStore.clearDetailIssue();
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7e5d0e0f888..08aecfab8a4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -37,8 +37,8 @@ class List {
this.type = obj.list_type;
const typeInfo = this.getTypeInfo(this.type);
- this.preset = !!typeInfo.isPreset;
- this.isExpandable = !!typeInfo.isExpandable;
+ this.preset = Boolean(typeInfo.isPreset);
+ this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = true;
this.page = 1;
this.loading = true;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 05efcbaa3cc..4b3b44574a8 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,7 @@ 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';
+import eventHub from '../eventhub';
const boardsStore = {
disabled: false,
@@ -109,6 +110,11 @@ const boardsStore = {
});
listFrom.update();
},
+
+ startMoving(list, issue) {
+ Object.assign(this.moving, { list, issue });
+ },
+
moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
@@ -183,12 +189,38 @@ const boardsStore = {
findListByLabelId(id) {
return this.state.lists.find(list => list.type === 'label' && list.label.id === id);
},
+
+ toggleFilter(filter) {
+ const filterPath = this.filter.path.split('&');
+ const filterIndex = filterPath.indexOf(filter);
+
+ if (filterIndex === -1) {
+ filterPath.push(filter);
+ } else {
+ filterPath.splice(filterIndex, 1);
+ }
+
+ this.filter.path = filterPath.join('&');
+
+ this.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+
+ setListDetail(newList) {
+ this.detail.list = newList;
+ },
+
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
clearDetailIssue() {
- this.detail.issue = {};
+ this.setIssueDetail({});
+ },
+
+ setIssueDetail(issueDetail) {
+ this.detail.issue = issueDetail;
},
};
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index f34496f84c6..f4c3fa185d8 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -23,7 +23,7 @@ class DeleteModal {
const branchData = e.currentTarget.dataset;
this.branchName = branchData.branchName || '';
this.deletePath = branchData.deletePath || '';
- this.isMerged = !!branchData.isMerged;
+ this.isMerged = Boolean(branchData.isMerged);
this.updateModal();
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 70af333a0dd..aacfa0d87e6 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -142,8 +142,7 @@ export default class Clusters {
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
- eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
- eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
+ eventHub.$on('updateApplication', data => this.updateApplication(data));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
@@ -155,8 +154,7 @@ export default class Clusters {
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
- eventHub.$off('upgradeApplication', this.upgradeApplication);
- eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
+ eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
eventHub.$off('uninstallApplication');
@@ -331,19 +329,13 @@ export default class Clusters {
});
}
- upgradeApplication(data) {
- const appId = data.id;
-
+ updateApplication({ id: appId, params }) {
this.store.updateApplication(appId);
- this.service.installApplication(appId, data.params).catch(() => {
+ this.service.installApplication(appId, params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
}
- dismissUpgradeSuccess(appId) {
- this.store.acknowledgeSuccessfulUpdate(appId);
- }
-
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
if (externalIp !== newExternalIp) {
this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
@@ -353,8 +345,10 @@ export default class Clusters {
saveKnativeDomain(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.updateApplication(appId, data.params);
+ this.store.updateApplication(appId);
+ this.service.updateApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
setKnativeHostname(data) {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 5f7675bb432..4771090aa7e 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -2,7 +2,7 @@
/* eslint-disable vue/require-default-prop */
import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
-import { s__, sprintf } from '../../locale';
+import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
@@ -85,10 +85,14 @@ export default {
type: String,
required: false,
},
- upgradeAvailable: {
+ updateAvailable: {
type: Boolean,
required: false,
},
+ updateable: {
+ type: Boolean,
+ default: true,
+ },
updateSuccessful: {
type: Boolean,
required: false,
@@ -109,11 +113,6 @@ export default {
required: false,
default: false,
},
- updateAcknowledged: {
- type: Boolean,
- required: false,
- default: true,
- },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -138,7 +137,7 @@ export default {
);
},
hasLogo() {
- return !!this.logoUrl;
+ return Boolean(this.logoUrl);
},
identiconId() {
// generate a deterministic integer id for the identicon background
@@ -170,11 +169,11 @@ export default {
installButtonLabel() {
let label;
if (this.canInstall) {
- label = s__('ClusterIntegration|Install');
+ label = __('Install');
} else if (this.isInstalling) {
- label = s__('ClusterIntegration|Installing');
+ label = __('Installing');
} else if (this.installed) {
- label = s__('ClusterIntegration|Installed');
+ label = __('Installed');
}
return label;
@@ -183,7 +182,7 @@ export default {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
- return s__('ClusterIntegration|Manage');
+ return __('Manage');
},
hasError() {
return this.installFailed || this.uninstallFailed;
@@ -203,42 +202,42 @@ export default {
},
versionLabel() {
if (this.updateFailed) {
- return s__('ClusterIntegration|Upgrade failed');
- } else if (this.isUpgrading) {
- return s__('ClusterIntegration|Upgrading');
+ return __('Update failed');
+ } else if (this.isUpdating) {
+ return __('Updating');
}
- return s__('ClusterIntegration|Upgraded');
+ return __('Updated');
},
- upgradeFailureDescription() {
+ updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
- upgradeSuccessDescription() {
- return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
+ updateSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), {
title: this.title,
});
},
- upgradeButtonLabel() {
+ updateButtonLabel() {
let label;
- if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
- label = s__('ClusterIntegration|Upgrade');
- } else if (this.isUpgrading) {
- label = s__('ClusterIntegration|Updating');
+ if (this.updateAvailable && !this.updateFailed && !this.isUpdating) {
+ label = __('Update');
+ } else if (this.isUpdating) {
+ label = __('Updating');
} else if (this.updateFailed) {
- label = s__('ClusterIntegration|Retry update');
+ label = __('Retry update');
}
return label;
},
- isUpgrading() {
+ isUpdating() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return this.status === APPLICATION_STATUS.UPDATING;
},
- shouldShowUpgradeDetails() {
+ shouldShowUpdateDetails() {
// This method only returns true when;
- // Upgrade was successful OR Upgrade failed
- // AND new upgrade is unavailable AND version information is present.
- return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
+ // Update was successful OR Update failed
+ // AND new update is unavailable AND version information is present.
+ return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version;
},
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
@@ -249,7 +248,7 @@ export default {
watch: {
updateSuccessful(updateSuccessful) {
if (updateSuccessful) {
- this.$toast.show(this.upgradeSuccessDescription);
+ this.$toast.show(this.updateSuccessDescription);
}
},
uninstallSuccessful(uninstallSuccessful) {
@@ -265,8 +264,8 @@ export default {
params: this.installApplicationRequestParams,
});
},
- upgradeClicked() {
- eventHub.$emit('upgradeApplication', {
+ updateClicked() {
+ eventHub.$emit('updateApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
@@ -326,36 +325,38 @@ export default {
</ul>
</div>
- <div
- v-if="shouldShowUpgradeDetails"
- class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
- >
- {{ versionLabel }}
- <span v-if="updateSuccessful">to</span>
-
- <gl-link
- v-if="updateSuccessful"
- :href="chartRepo"
- target="_blank"
- class="js-cluster-application-upgrade-version"
- >chart v{{ version }}</gl-link
+ <div v-if="updateable">
+ <div
+ v-if="shouldShowUpdateDetails"
+ class="form-text text-muted label p-0 js-cluster-application-update-details"
>
- </div>
+ {{ versionLabel }}
+ <span v-if="updateSuccessful">to</span>
- <div
- v-if="updateFailed && !isUpgrading"
- class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
- >
- {{ upgradeFailureDescription }}
+ <gl-link
+ v-if="updateSuccessful"
+ :href="chartRepo"
+ target="_blank"
+ class="js-cluster-application-update-version"
+ >chart v{{ version }}</gl-link
+ >
+ </div>
+
+ <div
+ v-if="updateFailed && !isUpdating"
+ class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details"
+ >
+ {{ updateFailureDescription }}
+ </div>
+ <loading-button
+ v-if="updateAvailable || updateFailed || isUpdating"
+ class="btn btn-primary js-cluster-application-update-button mt-2"
+ :loading="isUpdating"
+ :disabled="isUpdating"
+ :label="updateButtonLabel"
+ @click="updateClicked"
+ />
</div>
- <loading-button
- v-if="upgradeAvailable || updateFailed || isUpgrading"
- class="btn btn-primary js-cluster-application-upgrade-button mt-2"
- :loading="isUpgrading"
- :disabled="isUpgrading"
- :label="upgradeButtonLabel"
- @click="upgradeClicked"
- />
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 73760da9b98..970f5a7b297 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
@@ -25,6 +26,7 @@ export default {
clipboardButton,
LoadingButton,
GlLoadingIcon,
+ KnativeDomainEditor,
},
props: {
type: {
@@ -154,64 +156,21 @@ export default {
knative() {
return this.applications.knative;
},
- knativeInstalled() {
- return (
- this.knative.status === APPLICATION_STATUS.INSTALLED ||
- this.knativeUpgrading ||
- this.knativeUpgradeFailed ||
- this.knative.status === APPLICATION_STATUS.UPDATED
- );
- },
- knativeUpgrading() {
- return (
- this.knative.status === APPLICATION_STATUS.UPDATING ||
- this.knative.status === APPLICATION_STATUS.SCHEDULED
- );
- },
- knativeUpgradeFailed() {
- return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
- knativeExternalEndpoint() {
- return this.knative.externalIp || this.knative.externalHostname;
- },
- knativeDescription() {
- return sprintf(
- _.escape(
- s__(
- `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`,
- ),
- ),
- {
- pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
- target="_blank" rel="noopener noreferrer">
- ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
- },
- false,
- );
- },
- canUpdateKnativeEndpoint() {
- return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
- },
- knativeHostname: {
- get() {
- return this.knative.hostname;
- },
- set(hostname) {
- eventHub.$emit('setKnativeHostname', {
- id: 'knative',
- hostname,
- });
- },
- },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
methods: {
- saveKnativeDomain() {
+ saveKnativeDomain(hostname) {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
- params: { hostname: this.knative.hostname },
+ params: { hostname },
+ });
+ },
+ setKnativeHostname(hostname) {
+ eventHub.$emit('setKnativeHostname', {
+ id: 'knative',
+ hostname,
});
},
},
@@ -318,9 +277,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
@@ -330,9 +289,9 @@ export default {
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</template>
<template v-if="!ingressInstalled">
@@ -361,9 +320,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
- <label for="cert-manager-issuer-email">{{
- s__('ClusterIntegration|Issuer Email')
- }}</label>
+ <label for="cert-manager-issuer-email">
+ {{ s__('ClusterIntegration|Issuer Email') }}
+ </label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
@@ -417,7 +376,7 @@ export default {
:request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
- :upgrade-available="applications.runner.upgradeAvailable"
+ :update-available="applications.runner.updateAvailable"
:installed="applications.runner.installed"
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
@@ -491,9 +450,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
</template>
@@ -514,6 +473,7 @@ export default {
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
+ :updateable="false"
:disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
@@ -525,9 +485,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
<br />
</span>
@@ -540,83 +500,13 @@ export default {
}}
</p>
- <div class="row">
- <template v-if="knativeInstalled || (helmInstalled && rbac)">
- <div
- :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }"
- class="form-group col-sm-12 mb-0"
- >
- <label for="knative-domainname">
- <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
- </label>
- <input
- id="knative-domainname"
- v-model="knativeHostname"
- type="text"
- class="form-control js-knative-domainname"
- />
- </div>
- </template>
- <template v-if="knativeInstalled">
- <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
- <label for="knative-endpoint">
- <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
- </label>
- <div v-if="knativeExternalEndpoint" class="input-group">
- <input
- id="knative-endpoint"
- :value="knativeExternalEndpoint"
- type="text"
- class="form-control js-knative-endpoint"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="knativeExternalEndpoint"
- :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
- class="input-group-text js-knative-endpoint-clipboard-btn"
- />
- </span>
- </div>
- <div v-else class="input-group">
- <input type="text" class="form-control js-endpoint" readonly />
- <gl-loading-icon
- class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
- />
- </div>
- </div>
-
- <p class="form-text text-muted col-12">
- {{
- s__(
- `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
- )
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
- </p>
-
- <p
- v-if="!knativeExternalEndpoint"
- class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
- >
- {{
- s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
- }}
- </p>
-
- <button
- v-if="canUpdateKnativeEndpoint"
- class="btn btn-success js-knative-save-domain-button mt-3 ml-3"
- @click="saveKnativeDomain"
- >
- {{ s__('ClusterIntegration|Save changes') }}
- </button>
- </template>
- </div>
+ <knative-domain-editor
+ v-if="knative.installed || (helmInstalled && rbac)"
+ :knative="knative"
+ :ingress-dns-help-path="ingressDnsHelpPath"
+ @save="saveKnativeDomain"
+ @set="setKnativeHostname"
+ />
</div>
</application-row>
</div>
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
new file mode 100644
index 00000000000..480228619a5
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -0,0 +1,150 @@
+<script>
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+export default {
+ components: {
+ LoadingButton,
+ ClipboardButton,
+ GlLoadingIcon,
+ },
+ props: {
+ knative: {
+ type: Object,
+ required: true,
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ saveButtonDisabled() {
+ return [UNINSTALLING, UPDATING].includes(this.knative.status);
+ },
+ saving() {
+ return [UPDATING].includes(this.knative.status);
+ },
+ saveButtonLabel() {
+ return this.saving ? this.__('Saving') : this.__('Save changes');
+ },
+ knativeInstalled() {
+ return this.knative.installed;
+ },
+ knativeExternalEndpoint() {
+ return this.knative.externalIp || this.knative.externalHostname;
+ },
+ knativeUpdateSuccessful() {
+ return this.knative.updateSuccessful;
+ },
+ knativeHostname: {
+ get() {
+ return this.knative.hostname;
+ },
+ set(hostname) {
+ this.$emit('set', hostname);
+ },
+ },
+ },
+ watch: {
+ knativeUpdateSuccessful(updateSuccessful) {
+ if (updateSuccessful) {
+ this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row">
+ <div
+ v-if="knative.updateFailed"
+ class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
+ >
+ {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
+ </div>
+
+ <template>
+ <div
+ :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
+ class="form-group col-sm-12 mb-0"
+ >
+ <label for="knative-domainname">
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="knativeHostname"
+ type="text"
+ class="form-control js-knative-domainname"
+ />
+ </div>
+ </template>
+ <template v-if="knativeInstalled">
+ <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
+ <label for="knative-endpoint">
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
+ </label>
+ <div v-if="knativeExternalEndpoint" class="input-group">
+ <input
+ id="knative-endpoint"
+ :value="knativeExternalEndpoint"
+ type="text"
+ class="form-control js-knative-endpoint"
+ readonly
+ />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="knativeExternalEndpoint"
+ :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
+ class="input-group-text js-knative-endpoint-clipboard-btn"
+ />
+ </span>
+ </div>
+ <div v-else class="input-group">
+ <input type="text" class="form-control js-endpoint" readonly />
+ <gl-loading-icon
+ class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
+ />
+ </div>
+ </div>
+
+ <p class="form-text text-muted col-12">
+ {{
+ s__(
+ `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
+ )
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ <p
+ v-if="!knativeExternalEndpoint"
+ class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
+ >
+ {{
+ s__(`ClusterIntegration|The endpoint is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ }}
+ </p>
+
+ <loading-button
+ class="btn-success js-knative-save-domain-button mt-3 ml-3"
+ :loading="saving"
+ :disabled="saveButtonDisabled"
+ :label="saveButtonLabel"
+ @click="$emit('save', knativeHostname)"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 14b80a116a7..17ea4d77795 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -123,7 +123,6 @@ const applicationStateMachine = {
target: INSTALLED,
effects: {
updateSuccessful: true,
- updateAcknowledged: false,
},
},
[UPDATE_ERRORED]: {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 1b4d7e8372c..f64f0ca616f 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -56,8 +56,7 @@ export default class ClusterStore {
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
- upgradeAvailable: null,
- updateAcknowledged: true,
+ updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
},
@@ -77,6 +76,8 @@ export default class ClusterStore {
isEditingHostName: false,
externalIp: null,
externalHostname: null,
+ updateSuccessful: false,
+ updateFailed: false,
},
},
};
@@ -134,10 +135,6 @@ export default class ClusterStore {
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
- acknowledgeSuccessfulUpdate(appId) {
- this.state.applications[appId].updateAcknowledged = true;
- }
-
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
@@ -152,7 +149,7 @@ export default class ClusterStore {
status,
status_reason: statusReason,
version,
- update_available: upgradeAvailable,
+ update_available: updateAvailable,
can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
@@ -189,7 +186,7 @@ export default class ClusterStore {
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
- this.state.applications.runner.upgradeAvailable = upgradeAvailable;
+ this.state.applications.runner.updateAvailable = updateAvailable;
}
});
}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 37a3ceb5341..5bfe158ceda 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -40,7 +40,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
},
selectable: true,
filterable: true,
- filterRemote: !!$dropdown.data('refsUrl'),
+ filterRemote: Boolean($dropdown.data('refsUrl')),
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 916b190f469..fa0f04c7d82 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -12,7 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
- this.getDataRemote = !!options.filterRemote;
+ this.getDataRemote = Boolean(options.filterRemote);
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 35297b7c264..479afc50113 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -211,11 +211,12 @@ export const scrollToLineIfNeededParallel = (_, line) => {
}
};
-export const loadCollapsedDiff = ({ commit, getters }, file) =>
+export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
axios
.get(file.load_collapsed_diff_url, {
params: {
commit_id: getters.commitId,
+ w: state.showWhitespace ? '0' : '1',
},
})
.then(res => {
diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js
index 0fd4dd74953..384d62a133a 100644
--- a/app/assets/javascripts/emoji/no_emoji_validator.js
+++ b/app/assets/javascripts/emoji/no_emoji_validator.js
@@ -1,10 +1,11 @@
import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
+import InputValidator from '../validators/input_validator';
-const invalidInputClass = 'gl-field-error-outline';
-
-export default class NoEmojiValidator {
+export default class NoEmojiValidator extends InputValidator {
constructor(opts = {}) {
+ super();
+
const container = opts.container || '';
this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
@@ -19,45 +20,14 @@ export default class NoEmojiValidator {
const { value } = this.inputDomElement;
+ this.errorMessage = __('Invalid input, please avoid emojis');
+
this.validatePattern(value);
this.setValidationStateAndMessage();
}
validatePattern(value) {
const pattern = emojiRegex();
- this.hasEmojis = new RegExp(pattern).test(value);
-
- if (this.hasEmojis) {
- this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
- } else {
- this.inputDomElement.setCustomValidity('');
- }
- }
-
- setValidationStateAndMessage() {
- if (!this.inputDomElement.checkValidity()) {
- this.setInvalidState();
- } else {
- this.clearFieldValidationState();
- }
- }
-
- clearFieldValidationState() {
- this.inputDomElement.classList.remove(invalidInputClass);
- this.inputErrorMessage.classList.add('hide');
- }
-
- setInvalidState() {
- this.inputDomElement.classList.add(invalidInputClass);
- this.setErrorMessage();
- }
-
- setErrorMessage() {
- if (this.hasEmojis) {
- this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
- } else {
- this.inputErrorMessage.innerHTML = this.inputDomElement.title;
- }
- this.inputErrorMessage.classList.remove('hide');
+ this.invalidInput = new RegExp(pattern).test(value);
}
}
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
index e39452353f5..ce315963723 100644
--- a/app/assets/javascripts/error_tracking_settings/index.js
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -1,10 +1,8 @@
import Vue from 'vue';
import ErrorTrackingSettings from './components/app.vue';
import createStore from './store';
-import initSettingsPanels from '~/settings_panels';
export default () => {
- initSettingsPanels();
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
index a008b181907..d77e5f15469 100644
--- a/app/assets/javascripts/error_tracking_settings/store/getters.js
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -2,10 +2,10 @@ import _ from 'underscore';
import { __, s__, sprintf } from '~/locale';
import { getDisplayName } from '../utils';
-export const hasProjects = state => !!state.projects && state.projects.length > 0;
+export const hasProjects = state => Boolean(state.projects) && state.projects.length > 0;
export const isProjectInvalid = (state, getters) =>
- !!state.selectedProject &&
+ Boolean(state.selectedProject) &&
getters.hasProjects &&
!state.projects.some(project => _.isMatch(state.selectedProject, project));
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index 3dd89a82a42..ba62ab67e50 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -51,7 +51,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
const params = {
simple: true,
per_page: 20,
- membership: !!gon.current_user_id,
+ membership: Boolean(gon.current_user_id),
};
if (state.namespace === 'projects') {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index f437954881c..0af9aabd8cf 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import 'at.js';
import _ from 'underscore';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a143d79097b..05f34391323 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -307,8 +307,8 @@ GitLabDropdown = (function() {
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
- this.highlight = !!this.options.highlight;
- this.icon = !!this.options.icon;
+ this.highlight = Boolean(this.options.highlight);
+ this.icon = Boolean(this.options.icon);
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
@@ -565,6 +565,11 @@ GitLabDropdown = (function() {
!$target.data('isLink')
) {
e.stopPropagation();
+
+ // This prevents automatic scrolling to the top
+ if ($target.is('a')) {
+ return false;
+ }
}
return true;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 5a6d44ef838..a66555838ba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -13,7 +13,7 @@ export default class GLForm {
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
if (item !== 'emojis') {
- this.enableGFM[item] = !!dataSources[item];
+ this.enableGFM[item] = Boolean(dataSources[item]);
}
});
// Before we start, we should clean up any previous data for this form
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 7c769ab7fa0..7b4e03be8eb 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -78,7 +78,7 @@ export default {
data-container="body"
data-placement="right"
type="button"
- class="ide-sidebar-link js-ide-commit-mode"
+ class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
@click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
>
<icon name="commit" />
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 1824a0f6147..685d8a6b245 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,23 +1,24 @@
<script>
import _ from 'underscore';
-import { mapActions, mapState, mapGetters, createNamespacedHelpers } from 'vuex';
+import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, __ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
+import NewMergeRequestOption from './new_merge_request_option.vue';
-const { mapState: mapCommitState, mapGetters: mapCommitGetters } = createNamespacedHelpers(
+const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespacedHelpers(
'commit',
);
export default {
components: {
RadioGroup,
+ NewMergeRequestOption,
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
- ...mapCommitState(['commitAction', 'shouldCreateMR', 'shouldDisableNewMrOption']),
- ...mapGetters(['currentProject', 'currentBranch', 'currentMergeRequest']),
- ...mapCommitGetters(['shouldDisableNewMrOption']),
+ ...mapCommitState(['commitAction']),
+ ...mapGetters(['currentBranch']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -25,12 +26,12 @@ export default {
false,
);
},
- disableMergeRequestRadio() {
+ containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
watch: {
- disableMergeRequestRadio() {
+ containsStagedChanges() {
this.updateSelectedCommitAction();
},
},
@@ -38,11 +39,11 @@ export default {
this.updateSelectedCommitAction();
},
methods: {
- ...mapActions('commit', ['updateCommitAction', 'toggleShouldCreateMR']),
+ ...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() {
if (this.currentBranch && !this.currentBranch.can_push) {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
- } else if (this.disableMergeRequestRadio) {
+ } else if (this.containsStagedChanges) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
@@ -56,7 +57,7 @@ export default {
</script>
<template>
- <div class="append-bottom-15 ide-commit-radios">
+ <div class="append-bottom-15 ide-commit-options">
<radio-group
:value="$options.commitToCurrentBranch"
:disabled="currentBranch && !currentBranch.can_push"
@@ -69,17 +70,6 @@ export default {
:label="__('Create a new branch')"
:show-input="true"
/>
- <hr class="my-2" />
- <label class="mb-0">
- <input
- :checked="shouldCreateMR"
- :disabled="shouldDisableNewMrOption"
- type="checkbox"
- @change="toggleShouldCreateMR"
- />
- <span class="prepend-left-10" :class="{ 'text-secondary': shouldDisableNewMrOption }">
- {{ __('Start a new merge request') }}
- </span>
- </label>
+ <new-merge-request-option />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
new file mode 100644
index 00000000000..b2e7b15089c
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapGetters, createNamespacedHelpers } from 'vuex';
+
+const {
+ mapState: mapCommitState,
+ mapGetters: mapCommitGetters,
+ mapActions: mapCommitActions,
+} = createNamespacedHelpers('commit');
+
+export default {
+ computed: {
+ ...mapCommitState(['shouldCreateMR']),
+ ...mapCommitGetters(['isCommittingToCurrentBranch', 'isCommittingToDefaultBranch']),
+ ...mapGetters(['hasMergeRequest', 'isOnDefaultBranch']),
+ currentBranchHasMr() {
+ return this.hasMergeRequest && this.isCommittingToCurrentBranch;
+ },
+ showNewMrOption() {
+ return (
+ this.isCommittingToDefaultBranch || !this.currentBranchHasMr || this.isCommittingToNewBranch
+ );
+ },
+ },
+ mounted() {
+ this.setShouldCreateMR();
+ },
+ methods: {
+ ...mapCommitActions(['toggleShouldCreateMR', 'setShouldCreateMR']),
+ },
+};
+</script>
+
+<template>
+ <div v-if="showNewMrOption">
+ <hr class="my-2" />
+ <label class="mb-0">
+ <input :checked="shouldCreateMR" type="checkbox" @change="toggleShouldCreateMR" />
+ <span class="prepend-left-10">
+ {{ __('Start a new merge request') }}
+ </span>
+ </label>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 412b07553dc..f67666f1fbf 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -134,6 +134,7 @@ export default {
<template>
<gl-modal
id="ide-new-entry"
+ class="qa-new-file-modal"
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index c98dda00817..6999746f115 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -105,7 +105,7 @@ export default {
.then(() => {
this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: {
- isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
+ isFile: p => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])),
readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
},
});
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 99f1d4a573d..5201c33b1b4 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -30,7 +30,7 @@ export default {
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return Boolean(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index e35595ab1fd..dac2a8e8b51 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -11,7 +11,7 @@ export const defaultEditorOptions = {
export default [
{
- readOnly: model => !!model.file.file_lock,
+ readOnly: model => Boolean(model.file.file_lock),
quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 7a88ac5b116..406903129db 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -42,9 +42,10 @@ export const emptyRepo = state =>
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+export const hasChanges = state =>
+ Boolean(state.changedFiles.length) || Boolean(state.stagedFiles.length);
-export const hasMergeRequest = state => !!state.currentMergeRequestId;
+export const hasMergeRequest = state => Boolean(state.currentMergeRequestId);
export const allBlobs = state =>
Object.keys(state.entries)
@@ -70,7 +71,7 @@ export const isCommitModeActive = state => state.currentActivityView === activit
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommittedChanges = state =>
- !!(state.changedFiles.length || state.stagedFiles.length);
+ Boolean(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length;
@@ -96,7 +97,12 @@ export const lastCommit = (state, getters) => {
export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name;
+
export const packageJson = state => state.entries[packageJsonPath];
+export const isOnDefaultBranch = (_state, getters) =>
+ getters.currentProject && getters.currentProject.default_branch === getters.branchName;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f81bdb8a30e..51062f092ad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -18,15 +18,34 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
-export const updateCommitAction = ({ commit, rootGetters }, commitAction) => {
+export const updateCommitAction = ({ commit, dispatch }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, {
commitAction,
- currentMergeRequest: rootGetters.currentMergeRequest,
});
+ dispatch('setShouldCreateMR');
};
export const toggleShouldCreateMR = ({ commit }) => {
commit(types.TOGGLE_SHOULD_CREATE_MR);
+ commit(types.INTERACT_WITH_NEW_MR);
+};
+
+export const setShouldCreateMR = ({
+ commit,
+ getters,
+ rootGetters,
+ state: { interactedWithNewMR },
+}) => {
+ const committingToExistingMR =
+ getters.isCommittingToCurrentBranch &&
+ rootGetters.hasMergeRequest &&
+ !rootGetters.isOnDefaultBranch;
+
+ if ((getters.isCommittingToDefaultBranch && !interactedWithNewMR) || committingToExistingMR) {
+ commit(types.TOGGLE_SHOULD_CREATE_MR, false);
+ } else if (!interactedWithNewMR) {
+ commit(types.TOGGLE_SHOULD_CREATE_MR, true);
+ }
};
export const updateBranchName = ({ commit }, branchName) => {
@@ -102,7 +121,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
});
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 6aa5d22a4ea..64779e9e4df 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -48,8 +48,11 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
-export const shouldDisableNewMrOption = (state, _getters, _rootState, rootGetters) =>
- rootGetters.currentMergeRequest && state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
+export const isCommittingToCurrentBranch = state =>
+ state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
+
+export const isCommittingToDefaultBranch = (_state, getters, _rootState, rootGetters) =>
+ getters.isCommittingToCurrentBranch && rootGetters.isOnDefaultBranch;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 7ad8f3570b7..b81918156b0 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,3 +3,4 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
+export const INTERACT_WITH_NEW_MR = 'INTERACT_WITH_NEW_MR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index be0f894c059..14957d283bb 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -1,5 +1,4 @@
import * as types from './mutation_types';
-import consts from './constants';
export default {
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
@@ -7,14 +6,8 @@ export default {
commitMessage,
});
},
- [types.UPDATE_COMMIT_ACTION](state, { commitAction, currentMergeRequest }) {
- Object.assign(state, {
- commitAction,
- shouldCreateMR:
- commitAction === consts.COMMIT_TO_CURRENT_BRANCH && currentMergeRequest
- ? false
- : state.shouldCreateMR,
- });
+ [types.UPDATE_COMMIT_ACTION](state, { commitAction }) {
+ Object.assign(state, { commitAction });
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
Object.assign(state, {
@@ -26,9 +19,12 @@ export default {
submitCommitLoading,
});
},
- [types.TOGGLE_SHOULD_CREATE_MR](state) {
+ [types.TOGGLE_SHOULD_CREATE_MR](state, shouldCreateMR) {
Object.assign(state, {
- shouldCreateMR: !state.shouldCreateMR,
+ shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
+ [types.INTERACT_WITH_NEW_MR](state) {
+ Object.assign(state, { interactedWithNewMR: true });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index 5c0e6a41ca1..53647a7e3e3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: false,
+ interactedWithNewMR: false,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index ef7cd4ff8e8..1d127d915d7 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,6 +1,6 @@
import { states } from './constants';
-export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline);
export const pipelineFailed = state =>
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 344b189decf..ae42b87c9a7 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -142,7 +142,7 @@ export default {
Object.assign(state.entries[file.path], {
raw: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
staged: false,
prevPath: '',
moved: false,
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
index 05000c73052..7051a968dac 100644
--- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -14,7 +14,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
- const willRemove = !!commentIndicatorEl;
+ const willRemove = Boolean(commentIndicatorEl);
let meta = {};
if (willRemove) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 3587f073a00..26c1b0ec7be 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -6,8 +6,8 @@ import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
- this.canCreateNote = !!(options && options.canCreateNote);
- this.renderCommentBadge = !!(options && options.renderCommentBadge);
+ this.canCreateNote = Boolean(options && options.canCreateNote);
+ this.renderCommentBadge = Boolean(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js
index ab0a595571f..1a5123de220 100644
--- a/app/assets/javascripts/image_diff/view_types.js
+++ b/app/assets/javascripts/image_diff/view_types.js
@@ -5,5 +5,5 @@ export const viewTypes = {
};
export function isValidViewType(validate) {
- return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
+ return Boolean(Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate));
}
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index f51c7a2f990..16f88cddce3 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -12,7 +12,7 @@ export default class IssuableIndex {
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
- const alreadyInitialized = !!this.bulkUpdateSidebar;
+ const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index ab0b4231255..e88ca4747c5 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -156,7 +156,7 @@ export default {
return this.store.formState;
},
hasUpdated() {
- return !!this.state.updatedAt;
+ return Boolean(this.state.updatedAt);
},
issueChanged() {
const {
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index d2f33dc31a7..1e1dce5f4fc 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -71,7 +71,7 @@ export default {
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
- class="title"
+ class="title qa-title"
dir="auto"
v-html="titleHtml"
></h2>
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index c6dd21cd2d4..7064731a5ea 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -53,7 +53,7 @@ export default class LabelManager {
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle(
'hidden',
- !!this.prioritizedLabels[0].querySelector(':scope > li'),
+ Boolean(this.prioritizedLabels[0].querySelector(':scope > li')),
);
}
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
index 1d18992af63..39cffedcac6 100644
--- a/app/assets/javascripts/lib/utils/accessor.js
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -2,7 +2,7 @@ function isPropertyAccessSafe(base, property) {
let safe;
try {
- safe = !!base[property];
+ safe = Boolean(base[property]);
} catch (error) {
safe = false;
}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 32cafb74d91..d3e6851496b 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -513,7 +513,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
+ const isNonZero = Boolean(unitValue);
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 84a617acb42..b7922e29bb0 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -223,9 +223,9 @@ export function insertMarkdownText({
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) {
- return '' + val.replace(tag, '');
+ return String(val.replace(tag, ''));
} else {
- return '' + tag + val;
+ return String(tag) + val;
}
})
.join('\n');
@@ -233,7 +233,7 @@ export function insertMarkdownText({
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else {
- textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' ');
+ textToInsert = String(startChar) + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index bdfd06fc250..b5474fc5c71 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -121,4 +121,40 @@ export function webIDEUrl(route = undefined) {
return returnUrl;
}
+/**
+ * Returns current base URL
+ */
+export function getBaseURL() {
+ const { protocol, host } = window.location;
+ return `${protocol}//${host}`;
+}
+
+/**
+ * Returns true if url is an absolute or root-relative URL
+ *
+ * @param {String} url
+ */
+export function isAbsoluteOrRootRelative(url) {
+ return /^(https?:)?\//.test(url);
+}
+
+/**
+ * Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
+ *
+ * @param {String} url that will be checked
+ * @returns {Boolean}
+ */
+export function isSafeURL(url) {
+ if (!isAbsoluteOrRootRelative(url)) {
+ return false;
+ }
+
+ try {
+ const parsedUrl = new URL(url, getBaseURL());
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch (e) {
+ return false;
+ }
+}
+
export { join as joinPaths } from 'path';
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
new file mode 100644
index 00000000000..b03a6ca1806
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+ inheritAttrs: false,
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Number,
+ required: true,
+ },
+ unit: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ valueWithUnit() {
+ return `${this.value}${this.unit}`;
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph col-12 col-lg-6">
+ <div class="prometheus-graph-header">
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ title }}</h5>
+ </div>
+ <gl-single-stat :value="valueWithUnit" :title="title" variant="success" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index a55a47c277d..78744c0a0a9 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -38,7 +38,7 @@ export default {
GlModalDirective,
},
props: {
- externalDashboardPath: {
+ externalDashboardUrl: {
type: String,
required: false,
default: '',
@@ -172,7 +172,7 @@ export default {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
- this.fetchData(getTimeDiff(this.timeWindows.eightHours));
+ this.fetchData(getTimeDiff(this.selectedTimeWindow));
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
@@ -248,7 +248,7 @@ export default {
>
</gl-dropdown>
</div>
- <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
+ <div v-if="showTimeWindowDropdown" class="d-flex align-items-center prepend-left-8">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
@@ -299,10 +299,11 @@ export default {
</gl-modal>
</div>
<gl-button
- v-if="externalDashboardPath.length"
+ v-if="externalDashboardUrl.length"
class="js-external-dashboard-link prepend-left-8"
variant="primary"
- :href="externalDashboardPath"
+ :href="externalDashboardUrl"
+ target="_blank"
>
{{ __('View full dashboard') }}
<icon name="external-link" />
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 74c4ae64712..3fd9e07fa8b 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -8,8 +8,5 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
-export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT';
-export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT';
-export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
index b10e9f9f9f1..e48cfcd9564 100644
--- a/app/assets/javascripts/mr_notes/stores/getters.js
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -1,5 +1,5 @@
export default {
isLoggedIn(state, getters) {
- return !!getters.getUserData.id;
+ return Boolean(getters.getUserData.id);
},
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d03f4508fb8..1c9ca180100 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -7,6 +7,10 @@ no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
+/*
+old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
+ */
+
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 688c06878ac..075c28e8d07 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -337,6 +337,8 @@ Please check your network connection and try again.`;
v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getNoteableData)"
+ :locked-issue-docs-path="lockedIssueDocsPath"
+ :confidential-issue-docs-path="confidentialIssueDocsPath"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index c469a6b7bcd..53f509185a8 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,12 +1,24 @@
<script>
+import { GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
import Issuable from '~/vue_shared/mixins/issuable';
+import issuableStateMixin from '../mixins/issuable_state';
export default {
components: {
Icon,
+ GlLink,
+ },
+ mixins: [Issuable, issuableStateMixin],
+ computed: {
+ lockedIssueWarning() {
+ return sprintf(
+ __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
+ },
},
- mixins: [Issuable],
};
</script>
@@ -15,7 +27,11 @@ export default {
<span class="issuable-note-warning inline">
<icon :size="16" name="lock" class="icon" />
<span>
- This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.
+ {{ lockedIssueWarning }}
+
+ <gl-link :href="lockedIssueDocsPath" target="_blank" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 5b6163a6214..228bb652597 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -49,7 +49,7 @@ export default {
computed: {
...mapGetters(['userCanReply']),
hasReplies() {
- return !!this.replies.length;
+ return Boolean(this.replies.length);
},
replies() {
return this.discussion.notes.slice(1);
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index c9c40cb6acf..844d0c3e376 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -195,7 +195,7 @@ export default {
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
- <a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a>
+ <a :href="reportAbusePath">{{ __('Report abuse to admin') }}</a>
</li>
<li v-if="noteUrl">
<button
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index acbb91ce7be..09ecb695214 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -234,6 +234,8 @@ export default {
v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getNoteableData)"
+ :locked-issue-docs-path="lockedIssueDocsPath"
+ :confidential-issue-docs-path="confidentialIssueDocsPath"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 47d74c2f892..aa80e25a3e0 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -10,7 +10,7 @@ import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
-import noteBody from './note_body.vue';
+import NoteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -21,7 +21,7 @@ export default {
userAvatarLink,
noteHeader,
noteActions,
- noteBody,
+ NoteBody,
TimelineEntryItem,
},
mixins: [noteable, resolvable, draftMixin],
@@ -75,7 +75,7 @@ export default {
};
},
canReportAsAbuse() {
- return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
+ return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@@ -209,7 +209,10 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.note.note = noteText;
+ const { noteBody } = this.$refs;
+ if (noteBody) {
+ noteBody.note.note = noteText;
+ }
},
},
};
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 0f1976db37d..4d00e957973 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -127,6 +127,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ beforeDestroy() {
+ this.stopPolling();
+ },
methods: {
...mapActions([
'setLoadingState',
@@ -144,6 +147,7 @@ export default {
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
+ 'stopPolling',
]),
fetchNotes() {
if (this.isFetching) return null;
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
index 97f3ea0d5de..d97d9f6850a 100644
--- a/app/assets/javascripts/notes/mixins/issuable_state.js
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -1,11 +1,22 @@
+import { mapGetters } from 'vuex';
+
export default {
+ computed: {
+ ...mapGetters(['getNoteableDataByProp']),
+ lockedIssueDocsPath() {
+ return this.getNoteableDataByProp('locked_discussion_docs_path');
+ },
+ confidentialIssueDocsPath() {
+ return this.getNoteableDataByProp('confidential_issues_docs_path');
+ },
+ },
methods: {
isConfidential(issue) {
- return !!issue.confidential;
+ return Boolean(issue.confidential);
},
isLocked(issue) {
- return !!issue.discussion_locked;
+ return Boolean(issue.discussion_locked);
},
hasWarning(issue) {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 2d150e64ef7..d7982be3e4b 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,7 +20,7 @@ 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 userCanReply = state => Boolean(state.noteableData.current_user.can_create_note);
export const openState = state => state.noteableData.state;
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
index 0a87d193b72..ed518611d0b 100644
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -1,4 +1,5 @@
<script>
+import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
export default {
@@ -8,26 +9,34 @@ export default {
GlFormInput,
GlLink,
},
- props: {
- externalDashboardPath: {
- type: String,
- required: false,
- default: '',
- },
- externalDashboardHelpPagePath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'externalDashboardHelpPagePath',
+ 'externalDashboardUrl',
+ 'operationsSettingsEndpoint',
+ ]),
+ userDashboardUrl: {
+ get() {
+ return this.externalDashboardUrl;
+ },
+ set(url) {
+ this.setExternalDashboardUrl(url);
+ },
},
},
+ methods: {
+ ...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']),
+ },
};
</script>
<template>
- <section class="settings expanded">
+ <section class="settings no-animate">
<div class="settings-header">
<h4 class="js-section-header">
{{ s__('ExternalMetrics|External Dashboard') }}
</h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{
s__(
@@ -44,11 +53,12 @@ export default {
:description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
>
<gl-form-input
- :value="externalDashboardPath"
+ v-model="userDashboardUrl"
placeholder="https://my-org.gitlab.io/my-dashboards"
+ @keydown.enter.native.prevent="updateExternalDashboardUrl"
/>
</gl-form-group>
- <gl-button variant="success">
+ <gl-button variant="success" @click="updateExternalDashboardUrl">
{{ __('Save Changes') }}
</gl-button>
</form>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index 1171f3ece9f..6946578e6d2 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import store from './store';
import ExternalDashboardForm from './components/external_dashboard.vue';
export default () => {
@@ -14,13 +15,9 @@ export default () => {
return new Vue({
el,
+ store: store(el.dataset),
render(createElement) {
- return createElement(ExternalDashboardForm, {
- props: {
- ...el.dataset,
- expanded: false,
- },
- });
+ return createElement(ExternalDashboardForm);
},
});
};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
new file mode 100644
index 00000000000..ec05b0c76cf
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -0,0 +1,38 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as mutationTypes from './mutation_types';
+
+export const setExternalDashboardUrl = ({ commit }, url) =>
+ commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url);
+
+export const updateExternalDashboardUrl = ({ state, dispatch }) =>
+ axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: state.externalDashboardUrl,
+ },
+ },
+ })
+ .then(() => dispatch('receiveExternalDashboardUpdateSuccess'))
+ .catch(error => dispatch('receiveExternalDashboardUpdateError', error));
+
+export const receiveExternalDashboardUpdateSuccess = () => {
+ /**
+ * The operations_controller currently handles successful requests
+ * by creating a flash banner messsage to notify the user.
+ */
+ refreshCurrentPage();
+};
+
+export const receiveExternalDashboardUpdateError = (_, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js
new file mode 100644
index 00000000000..e96bb1e8aad
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: createState(initialState),
+ actions,
+ mutations,
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/operation_settings/store/mutation_types.js b/app/assets/javascripts/operation_settings/store/mutation_types.js
new file mode 100644
index 00000000000..237d2b6122f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutation_types.js
@@ -0,0 +1,3 @@
+/* eslint-disable import/prefer-default-export */
+
+export const SET_EXTERNAL_DASHBOARD_URL = 'SET_EXTERNAL_DASHBOARD_URL';
diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js
new file mode 100644
index 00000000000..64bb33bb89f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutations.js
@@ -0,0 +1,7 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_EXTERNAL_DASHBOARD_URL](state, url) {
+ state.externalDashboardUrl = url;
+ },
+};
diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js
new file mode 100644
index 00000000000..72167141c48
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/state.js
@@ -0,0 +1,5 @@
+export default (initialState = {}) => ({
+ externalDashboardUrl: initialState.externalDashboardUrl || '',
+ operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
+ externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath,
+});
diff --git a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js b/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
new file mode 100644
index 00000000000..27e4433ad4d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
@@ -0,0 +1,3 @@
+import initForm from '~/pages/projects/pages_domains/form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pages_domains/form.js b/app/assets/javascripts/pages/projects/pages_domains/form.js
new file mode 100644
index 00000000000..1d0dbfe0406
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/form.js
@@ -0,0 +1,43 @@
+import setupToggleButtons from '~/toggle_buttons';
+
+export default () => {
+ const toggleContainer = document.querySelector('.js-auto-ssl-toggle-container');
+
+ if (toggleContainer) {
+ const onToggleButtonClicked = isAutoSslEnabled => {
+ Array.from(document.querySelectorAll('.js-shown-if-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.classList.remove('d-none');
+ } else {
+ el.classList.add('d-none');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-shown-unless-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.classList.add('d-none');
+ } else {
+ el.classList.remove('d-none');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-enabled-if-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.removeAttribute('disabled');
+ } else {
+ el.setAttribute('disabled', 'disabled');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-enabled-unless-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.setAttribute('disabled', 'disabled');
+ } else {
+ el.removeAttribute('disabled');
+ }
+ });
+ };
+
+ setupToggleButtons(toggleContainer, onToggleButtonClicked);
+ }
+};
diff --git a/app/assets/javascripts/pages/projects/pages_domains/new/index.js b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
new file mode 100644
index 00000000000..27e4433ad4d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
@@ -0,0 +1,3 @@
+import initForm from '~/pages/projects/pages_domains/form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index bd4309e47ad..bb490919a9a 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -29,7 +29,7 @@ export default {
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
- return !!(this.customInputEnabled || !this.intervalIsPreset);
+ return Boolean(this.customInputEnabled || !this.intervalIsPreset);
},
},
watch: {
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 5270a7924ec..98e19705976 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,7 +1,9 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
+ initSettingsPanels();
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 19d9903c988..dea7c586868 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -175,11 +175,6 @@ export default {
if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
},
-
- buildsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
- },
},
methods: {
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index e1a3f42a71f..3f5a3e15c2c 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import LengthValidator from './length_validator';
import UsernameValidator from './username_validator';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
@@ -6,6 +7,7 @@ import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
+ new LengthValidator(); // eslint-disable-line no-new
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/sessions/new/length_validator.js b/app/assets/javascripts/pages/sessions/new/length_validator.js
new file mode 100644
index 00000000000..3d687ca08cc
--- /dev/null
+++ b/app/assets/javascripts/pages/sessions/new/length_validator.js
@@ -0,0 +1,32 @@
+import InputValidator from '../../../validators/input_validator';
+
+const errorMessageClass = 'gl-field-error';
+
+export default class LengthValidator extends InputValidator {
+ constructor(opts = {}) {
+ super();
+
+ const container = opts.container || '';
+ const validateLengthElements = document.querySelectorAll(`${container} .js-validate-length`);
+
+ validateLengthElements.forEach(element =>
+ element.addEventListener('input', this.eventHandler.bind(this)),
+ );
+ }
+
+ eventHandler(event) {
+ this.inputDomElement = event.target;
+ this.inputErrorMessage = this.inputDomElement.parentElement.querySelector(
+ `.${errorMessageClass}`,
+ );
+
+ const { value } = this.inputDomElement;
+ const { maxLengthMessage, maxLength } = this.inputDomElement.dataset;
+
+ this.errorMessage = maxLengthMessage;
+
+ this.invalidInput = value.length > parseInt(maxLength, 10);
+
+ this.setValidationStateAndMessage();
+ }
+}
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 59c13e1a042..f0d9642a2b2 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -35,7 +35,7 @@ export default () => {
return createElement('delete-account-modal', {
props: {
actionUrl: deleteAccountModalEl.dataset.actionUrl,
- confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
+ confirmWithPassword: Boolean(deleteAccountModalEl.dataset.confirmWithPassword),
username: deleteAccountModalEl.dataset.username,
},
});
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
index 4834a856271..f05ad7773a2 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -57,7 +57,7 @@ export const validateProjectBilling = ({ dispatch, commit, state }) =>
resp => {
const { billingEnabled } = resp.result;
- commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
+ commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
dispatch('setIsValidatingProjectBilling', false);
resolve();
},
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
index e39f02d0894..f9e2e2f74fb 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -1,3 +1,3 @@
-export const hasProject = state => !!state.selectedProject.projectId;
-export const hasZone = state => !!state.selectedZone;
-export const hasMachineType = state => !!state.selectedMachineType;
+export const hasProject = state => Boolean(state.selectedProject.projectId);
+export const hasZone = state => Boolean(state.selectedZone);
+export const hasMachineType = state => Boolean(state.selectedMachineType);
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 1ac699c538f..8ace6657ad1 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -9,7 +9,7 @@ export default {
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
- canDelete: !!el.destroy_path,
+ canDelete: Boolean(el.destroy_path),
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
@@ -42,7 +42,7 @@ export default {
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
- canDelete: !!element.destroy_path,
+ canDelete: Boolean(element.destroy_path),
}));
},
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index cccde1bb278..d2198bcccfe 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -134,6 +134,7 @@ export default {
:current-path="path"
:path="entry.flatPath"
:type="entry.type"
+ :url="entry.webUrl"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 9a264bef87e..764882a7936 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -21,6 +21,11 @@ export default {
type: String,
required: true,
},
+ url: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
routerLinkTo() {
@@ -59,7 +64,7 @@ export default {
<tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
<td class="tree-item-file-name">
<i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
- <component :is="linkComponent" :to="routerLinkTo" class="str-truncated">
+ <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
{{ fullPath }}
</component>
<template v-if="isSubmodule">
diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql
index a9b61d28560..7d92bc46455 100644
--- a/app/assets/javascripts/repository/queries/getFiles.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.graphql
@@ -23,6 +23,7 @@ query getFiles(
edges {
node {
...TreeEntry
+ webUrl
}
}
pageInfo {
@@ -43,6 +44,7 @@ query getFiles(
edges {
node {
...TreeEntry
+ webUrl
}
}
pageInfo {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 72e061df573..930c0d5e958 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -82,9 +82,9 @@ Sidebar.prototype.toggleTodo = function(e) {
ajaxType = $this.data('deletePath') ? 'delete' : 'post';
if ($this.data('deletePath')) {
- url = '' + $this.data('deletePath');
+ url = String($this.data('deletePath'));
} else {
- url = '' + $this.data('createPath');
+ url = String($this.data('createPath'));
}
$this.tooltip('hide');
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index a0780f36b7b..6aca4067ba7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -379,7 +379,7 @@ export class SearchAutocomplete {
}
}
}
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
}
onSearchInputFocus() {
@@ -396,7 +396,7 @@ export class SearchAutocomplete {
onClearInputClick(e) {
e.preventDefault();
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
return this.searchInput.val('').focus();
}
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index f9b4e789563..94341050b86 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
+import { CHECKING_INSTALLED } from '../constants';
export default {
components: {
@@ -13,10 +14,6 @@ export default {
GlLoadingIcon,
},
props: {
- installed: {
- type: Boolean,
- required: true,
- },
clustersPath: {
type: String,
required: true,
@@ -31,8 +28,15 @@ export default {
},
},
computed: {
- ...mapState(['isLoading', 'hasFunctionData']),
+ ...mapState(['installed', 'isLoading', 'hasFunctionData']),
...mapGetters(['getFunctions']),
+
+ checkingInstalled() {
+ return this.installed === CHECKING_INSTALLED;
+ },
+ isInstalled() {
+ return this.installed === true;
+ },
},
created() {
this.fetchFunctions({
@@ -47,15 +51,16 @@ export default {
<template>
<section id="serverless-functions">
- <div v-if="installed">
+ <gl-loading-icon
+ v-if="checkingInstalled"
+ :size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+
+ <div v-else-if="isInstalled">
<div v-if="hasFunctionData">
- <gl-loading-icon
- v-if="isLoading"
- :size="2"
- class="prepend-top-default append-bottom-default"
- />
- <template v-else>
- <div class="groups-list-tree-container">
+ <template>
+ <div class="groups-list-tree-container js-functions-wrapper">
<ul class="content-list group-list-tree">
<environment-row
v-for="(env, index) in getFunctions"
@@ -66,6 +71,11 @@ export default {
</ul>
</div>
</template>
+ <gl-loading-icon
+ v-if="isLoading"
+ :size="2"
+ class="prepend-top-default append-bottom-default js-functions-loader"
+ />
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
index 35f77205f2c..2fa15e56ccb 100644
--- a/app/assets/javascripts/serverless/constants.js
+++ b/app/assets/javascripts/serverless/constants.js
@@ -1,3 +1,7 @@
export const MAX_REQUESTS = 3; // max number of times to retry
export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
+
+export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
+
+export const TIMEOUT = 'timeout';
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 2d3f086ffee..ed3b633d766 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -45,7 +45,7 @@ export default class Serverless {
},
});
} else {
- const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ const { statusPath, clustersPath, helpPath } = document.querySelector(
'.js-serverless-functions-page',
).dataset;
@@ -56,7 +56,6 @@ export default class Serverless {
render(createElement) {
return createElement(Functions, {
props: {
- installed: installed !== undefined,
clustersPath,
helpPath,
statusPath,
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index 826501c9022..a0a9fdf7ace 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import { MAX_REQUESTS } from '../constants';
+import { __ } from '~/locale';
+import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }) =>
- commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsPartial = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
+export const receiveFunctionsTimeout = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
export const receiveFunctionsError = ({ commit }, error) =>
commit(types.RECEIVE_FUNCTIONS_ERROR, error);
@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
let retryCount = 0;
+ const functionsPartiallyFetched = data => {
+ if (data.functions !== null && data.functions.length) {
+ dispatch('receiveFunctionsPartial', data);
+ }
+ };
+
dispatch('requestFunctionsLoading');
backOff((next, stop) => {
axios
.get(functionsPath)
.then(response => {
- if (response.status === statusCodes.NO_CONTENT) {
+ if (response.data.knative_installed === CHECKING_INSTALLED) {
retryCount += 1;
if (retryCount < MAX_REQUESTS) {
+ functionsPartiallyFetched(response.data);
next();
} else {
- stop(null);
+ stop(TIMEOUT);
}
} else {
stop(response.data);
@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
.catch(stop);
})
.then(data => {
- if (data !== null) {
+ if (data === TIMEOUT) {
+ dispatch('receiveFunctionsTimeout');
+ createFlash(__('Loading functions timed out. Please reload the page to try again.'));
+ } else if (data.functions !== null && data.functions.length) {
dispatch('receiveFunctionsSuccess', data);
} else {
- dispatch('receiveFunctionsNoDataSuccess');
+ dispatch('receiveFunctionsNoDataSuccess', data);
}
})
.catch(error => {
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
index 25b2f7ac38a..b8fa9ea1a01 100644
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -1,5 +1,7 @@
export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
+export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
index 991f32a275d..2685a5b11ff 100644
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -5,12 +5,23 @@ export default {
state.isLoading = true;
},
[types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
- state.functions = data;
+ state.functions = data.functions;
+ state.installed = data.knative_installed;
state.isLoading = false;
state.hasFunctionData = true;
},
- [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+ [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
+ state.functions = data.functions;
+ state.installed = true;
+ state.isLoading = true;
+ state.hasFunctionData = true;
+ },
+ [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
state.isLoading = false;
+ state.installed = data.knative_installed;
state.hasFunctionData = false;
},
[types.RECEIVE_FUNCTIONS_ERROR](state, error) {
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index afc3f37d7ba..fdd29299749 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,5 +1,6 @@
export default () => ({
error: null,
+ installed: 'checking',
isLoading: true,
// functions
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index c03b2a68c78..d84d5344935 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -49,10 +49,10 @@ export default {
},
computed: {
hasTimeSpent() {
- return !!this.timeSpent;
+ return Boolean(this.timeSpent);
},
hasTimeEstimate() {
- return !!this.timeEstimate;
+ return Boolean(this.timeEstimate);
},
showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent;
@@ -67,7 +67,7 @@ export default {
return !this.hasTimeEstimate && !this.hasTimeSpent;
},
showHelpState() {
- return !!this.showHelp;
+ return Boolean(this.showHelp);
},
},
created() {
diff --git a/app/assets/javascripts/validators/input_validator.js b/app/assets/javascripts/validators/input_validator.js
new file mode 100644
index 00000000000..f37373977b8
--- /dev/null
+++ b/app/assets/javascripts/validators/input_validator.js
@@ -0,0 +1,34 @@
+const invalidInputClass = 'gl-field-error-outline';
+
+export default class InputValidator {
+ constructor() {
+ this.inputDomElement = {};
+ this.inputErrorMessage = {};
+ this.errorMessage = null;
+ this.invalidInput = null;
+ }
+
+ setValidationStateAndMessage() {
+ this.setValidationMessage();
+
+ const isInvalidInput = !this.inputDomElement.checkValidity();
+ this.inputDomElement.classList.toggle(invalidInputClass, isInvalidInput);
+ this.inputErrorMessage.classList.toggle('hide', !isInvalidInput);
+ }
+
+ setValidationMessage() {
+ if (this.invalidInput) {
+ this.inputDomElement.setCustomValidity(this.errorMessage);
+ this.inputErrorMessage.innerHTML = this.errorMessage;
+ } else {
+ this.resetValidationMessage();
+ }
+ }
+
+ resetValidationMessage() {
+ if (this.inputDomElement.validationMessage === this.errorMessage) {
+ this.inputDomElement.setCustomValidity('');
+ this.inputErrorMessage.innerHTML = this.inputDomElement.title;
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index ad0464a3a98..abe5bdd2901 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -77,16 +77,16 @@ export default {
return this.deployment.external_url;
},
hasExternalUrls() {
- return !!(this.deployment.external_url && this.deployment.external_url_formatted);
+ return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
hasDeploymentTime() {
- return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
},
hasDeploymentMeta() {
- return !!(this.deployment.url && this.deployment.name);
+ return Boolean(this.deployment.url && this.deployment.name);
},
hasMetrics() {
- return !!this.deployment.metrics_url;
+ return Boolean(this.deployment.metrics_url);
},
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
index 040315b3c66..19a222462b3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="m-3 ml-5" :class="messageClass">
+ <div class="m-3 ml-7" :class="messageClass">
<slot></slot>
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
<icon :size="16" name="question-o" class="align-middle" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 0686409a785..03a15ba81ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -56,7 +56,7 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
- return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
+ return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index a3a44dd8e99..83e7d6db9fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -35,9 +35,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children">
<span class="bold">
- <template v-if="mr.mergeError"
- >{{ mr.mergeError }}.</template
- >
+ <template v-if="mr.mergeError">{{ mr.mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 5aa1f0799fc..615d59a7b8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -330,6 +330,7 @@ export default {
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
:is-fast-forward-enabled="mr.ffOnlyEnabled"
+ :class="{ 'border-bottom': mr.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index b1f5655a15a..accb9d9fef1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -29,8 +29,8 @@ export default {
</script>
<template>
- <div class="accept-control inline">
- <label class="merge-param-checkbox">
+ <div class="inline">
+ <label>
<input
:checked="value"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bf175eb5f69..d02bb2f341d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { __ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -97,7 +97,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
+ return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
},
shouldRenderSourceBranchRemovalStatus() {
return (
@@ -125,6 +125,11 @@ export default {
this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
);
},
+ mergeError() {
+ return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), {
+ mergeError: this.mr.mergeError,
+ });
+ },
},
watch: {
state(newVal, oldVal) {
@@ -370,6 +375,10 @@ export default {
}}
</mr-widget-alert-message>
+ <mr-widget-alert-message v-if="mr.mergeError" type="danger">
+ {{ mergeError }}
+ </mr-widget-alert-message>
+
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 448cbaa6fc9..32badb0fb08 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -79,7 +79,7 @@ export default class MergeRequestStore {
this.autoMergeStrategy = data.auto_merge_strategy;
this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
- this.shouldBeRebased = !!data.should_be_rebased;
+ this.shouldBeRebased = Boolean(data.should_be_rebased);
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
@@ -92,9 +92,9 @@ export default class MergeRequestStore {
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
- this.canMerge = !!data.merge_path;
+ this.canMerge = Boolean(data.merge_path);
this.canCreateIssue = currentUser.can_create_issue || false;
- this.canCancelAutomaticMerge = !!data.cancel_auto_merge_path;
+ this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
new file mode 100644
index 00000000000..7d49c87271d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import Icon from './icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ primaryButtonClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ dropdownClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ actions: {
+ type: Array,
+ required: true,
+ },
+ defaultAction: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedAction: this.defaultAction,
+ };
+ },
+ computed: {
+ selectedActionTitle() {
+ return this.actions[this.selectedAction].title;
+ },
+ buttonSizeClass() {
+ return `btn-${this.size}`;
+ },
+ },
+ methods: {
+ handlePrimaryActionClick() {
+ this.$emit('onActionClick', this.actions[this.selectedAction]);
+ },
+ handleActionClick(selectedAction) {
+ this.selectedAction = selectedAction;
+ this.$emit('onActionSelect', selectedAction);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="btn-group droplab-dropdown comment-type-dropdown">
+ <gl-button :class="primaryButtonClass" :size="size" @click.prevent="handlePrimaryActionClick">
+ {{ selectedActionTitle }}
+ </gl-button>
+ <button
+ :class="buttonSizeClass"
+ type="button"
+ class="btn dropdown-toggle pl-2 pr-2"
+ data-display="static"
+ data-toggle="dropdown"
+ >
+ <icon name="arrow-down" aria-label="toggle dropdown" />
+ </button>
+ <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
+ <template v-for="(action, index) in actions">
+ <li :key="index" :class="{ 'droplab-item-selected': selectedAction === index }">
+ <gl-button class="btn-transparent" @click.prevent="handleActionClick(index)">
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>{{ action.title }}</strong>
+ <p>{{ action.description }}</p>
+ </div>
+ </gl-button>
+ </li>
+ <li v-if="index === 0" :key="`${index}-separator`" class="divider droplab-item-ignore"></li>
+ </template>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
index 7e79e63aa1e..715cf97f0ac 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -62,6 +62,15 @@ export default {
assigneeName: assignee.name,
});
},
+ // This method is for backward compat
+ // since Graph query would return camelCase
+ // props while Rails would return snake_case
+ webUrl(assignee) {
+ return assignee.web_url || assignee.webUrl;
+ },
+ avatarUrl(assignee) {
+ return assignee.avatar_url || assignee.avatarUrl;
+ },
},
};
</script>
@@ -70,9 +79,9 @@ export default {
<user-avatar-link
v-for="assignee in assigneesToShow"
:key="assignee.id"
- :link-href="assignee.web_url"
+ :link-href="webUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatar_url"
+ :img-src="avatarUrl(assignee)"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index 53e6efa6ea3..9b2ee5062b1 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -19,10 +19,14 @@ export default {
},
computed: {
milestoneDue() {
- return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
+ const dueDate = this.milestone.due_date || this.milestone.dueDate;
+
+ return dueDate ? parsePikadayDate(dueDate) : null;
},
milestoneStart() {
- return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
+ const startDate = this.milestone.start_date || this.milestone.startDate;
+
+ return startDate ? parsePikadayDate(startDate) : null;
},
isMilestoneStarted() {
if (!this.milestoneStart) {
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index e92babc499b..e438ff16a41 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,9 +1,17 @@
<script>
+import { GlLink } from '@gitlab/ui';
+import _ from 'underscore';
+import { sprintf } from '~/locale';
import icon from '../../../vue_shared/components/icon.vue';
+function buildDocsLinkStart(path) {
+ return `<a href="${_.escape(path)}" target="_blank" rel="noopener noreferrer">`;
+}
+
export default {
components: {
icon,
+ GlLink,
},
props: {
isLocked: {
@@ -16,6 +24,16 @@ export default {
default: false,
required: false,
},
+ lockedIssueDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ confidentialIssueDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
warningIcon() {
@@ -27,6 +45,17 @@ export default {
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
+ confidentialAndLockedDiscussionText() {
+ return sprintf(
+ 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.',
+ {
+ confidentialLinkStart: buildDocsLinkStart(this.confidentialIssueDocsPath),
+ lockedLinkStart: buildDocsLinkStart(this.lockedIssueDocsPath),
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
},
};
</script>
@@ -35,20 +64,26 @@ export default {
<icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential">
- {{ __('This issue is confidential and locked.') }}
+ <span v-html="confidentialAndLockedDiscussionText"></span>
{{
- __(`People without permission will never
-get a notification and won't be able to comment.`)
+ __(`People without permission will never get a notification and won't be able to comment.`)
}}
</span>
<span v-else-if="isConfidential">
{{ __('This is a confidential issue.') }}
- {{ __('Your comment will not be visible to the public.') }}
+ {{ __('People without permission will never get a notification.') }}
+ <gl-link :href="confidentialIssueDocsPath" target="_blank">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
<span v-else-if="isLocked">
- {{ __('This issue is locked.') }} {{ __('Only project members can comment.') }}
+ {{ __('This issue is locked.') }}
+ {{ __('Only project members can comment.') }}
+ <gl-link :href="lockedIssueDocsPath" target="_blank">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
new file mode 100644
index 00000000000..bf59a6abf3f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -0,0 +1,121 @@
+<script>
+import $ from 'jquery';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import Clipboard from 'clipboard';
+
+export default {
+ components: {
+ GlButton,
+ Icon,
+ },
+
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+
+ props: {
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ container: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ modalId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ target: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+
+ copySuccessText: __('Copied'),
+
+ computed: {
+ modalDomId() {
+ return this.modalId ? `#${this.modalId}` : '';
+ },
+ },
+
+ mounted() {
+ this.$nextTick(() => {
+ this.clipboard = new Clipboard(this.$el, {
+ container:
+ document.querySelector(`${this.modalDomId} div.modal-content`) ||
+ document.getElementById(this.container) ||
+ document.body,
+ });
+ this.clipboard
+ .on('success', e => {
+ this.updateTooltip(e.trigger);
+ this.$emit('success', e);
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ $(e.trigger).blur();
+ })
+ .on('error', e => this.$emit('error', e));
+ });
+ },
+
+ destroyed() {
+ if (this.clipboard) {
+ this.clipboard.destroy();
+ }
+ },
+
+ methods: {
+ updateTooltip(target) {
+ const $target = $(target);
+ const originalTitle = $target.data('originalTitle');
+
+ if ($target.tooltip) {
+ /**
+ * The original tooltip will continue staying there unless we remove it by hand.
+ * $target.tooltip('hide') isn't working.
+ */
+ $('.tooltip').remove();
+ $target.attr('title', this.$options.copySuccessText);
+ $target.tooltip('_fixTitle');
+ $target.tooltip('show');
+ $target.attr('title', originalTitle);
+ $target.tooltip('_fixTitle');
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-button
+ v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
+ :data-clipboard-target="target"
+ :data-clipboard-text="text"
+ :title="title"
+ >
+ <slot>
+ <icon name="duplicate" />
+ </slot>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index fa502b9beb9..8104d919bf6 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -34,7 +34,7 @@ export default {
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
- setDefaultDate: !!this.selectedDate,
+ setDefaultDate: Boolean(this.selectedDate),
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 8e0b08032f7..9cce9a4e542 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -121,7 +121,7 @@ export default {
this.change(1);
break;
default:
- this.change(+text);
+ this.change(Number(text));
break;
}
},
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 93377b8dd91..7f6384f4eea 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -22,7 +22,9 @@ body,
.form-control,
.search form {
// Override default font size used in non-csslab UI
- font-size: 14px;
+ // Use rem to keep default font-size at 14px on body so 1rem still
+ // fits 8px grid, but also allow users to change browser font size
+ font-size: .875rem;
}
legend {
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index 658e0ff638e..8c32b6c8985 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -17,7 +17,7 @@ body {
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
- font-size: 14px;
+ font-size: .875rem;
}
h1 {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index afa85f0e4ae..e3dd127366d 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -6,6 +6,19 @@
position: relative;
z-index: 1;
+ .flash-notice,
+ .flash-alert,
+ .flash-success,
+ .flash-warning {
+ border-radius: $border-radius-default;
+ color: $white-light;
+
+ .container-fluid,
+ .container-fluid.container-limited {
+ background: transparent;
+ }
+ }
+
.flash-notice {
@extend .alert;
background-color: $blue-500;
@@ -28,7 +41,8 @@
.flash-warning {
@extend .alert;
- background-color: $orange-500;
+ background-color: $orange-100;
+ color: $orange-900;
margin: 0;
}
@@ -60,19 +74,6 @@
margin: 0;
}
- .flash-notice,
- .flash-alert,
- .flash-success,
- .flash-warning {
- border-radius: $border-radius-default;
- color: $white-light;
-
- .container-fluid,
- .container-fluid.container-limited {
- background: transparent;
- }
- }
-
&.flash-container-page {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 298610a0631..555a3fe0dc7 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -177,14 +177,6 @@ ul.content-list {
}
}
- .member-controls {
- float: none;
-
- @include media-breakpoint-up(sm) {
- float: right;
- }
- }
-
// When dragging a list item
&.ui-sortable-helper {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 28768bdf88f..0a3bd9bf4d1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -381,6 +381,7 @@ $breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
+$gl-line-height-20: 20px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 0c1067bfacc..f08fa80495d 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -719,7 +719,7 @@ $ide-commit-header-height: 48px;
border: 1px solid $white-dark;
}
-.ide-commit-radios {
+.ide-commit-options {
label {
font-weight: normal;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index e34628002ac..500f5816d38 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -8,7 +8,7 @@
border-bottom: 1px solid $white-normal;
color: $gl-text-color-secondary;
position: relative;
- line-height: $gl-line-height;
+ line-height: $gl-line-height-20;
.system-note-image {
position: absolute;
@@ -48,7 +48,7 @@
}
.event-user-info {
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding-4;
.author_name {
a {
@@ -67,7 +67,7 @@
}
.event-body {
- margin-top: $gl-padding-8;
+ margin-top: $gl-padding-4;
margin-right: 174px;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 297f642681b..d8aabecc036 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -73,7 +73,8 @@
.login-body {
font-size: 13px;
- input + p {
+ input + p,
+ input ~ p.field-validation {
margin-top: 5px;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index f8e273a2735..68af01f9ccc 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -20,17 +20,6 @@
}
}
- .list-item-name {
- @include media-breakpoint-up(sm) {
- float: left;
- width: 50%;
- }
-
- strong {
- font-weight: $gl-font-weight-bold;
- }
- }
-
.controls {
@include media-breakpoint-up(sm) {
display: flex;
@@ -43,10 +32,11 @@
.form-group {
margin-bottom: 0;
+ }
- @include media-breakpoint-down(sm) {
- display: block;
- margin-left: 5px;
+ .member-controls {
+ .fa {
+ line-height: inherit;
}
}
@@ -66,23 +56,12 @@
}
.member-form-control {
- @include media-breakpoint-down(sm) {
- width: $dropdown-member-form-control-width;
- margin-left: 0;
- padding-bottom: 5px;
- }
-
@include media-breakpoint-down(xs) {
margin-right: 0;
width: auto;
}
}
-.member-access-text {
- margin-left: auto;
- line-height: 43px;
-}
-
.member-search-form {
position: relative;
@@ -221,9 +200,6 @@
}
.content-list.members-list li {
- display: flex;
- justify-content: space-between;
-
.list-item-name {
float: none;
display: flex;
@@ -252,33 +228,24 @@
align-self: flex-start;
}
+ @include media-breakpoint-down(sm) {
+ .member-access-text {
+ margin: 0 0 $gl-padding-4 ($grid-size * 6);
+ }
+ }
+
@include media-breakpoint-down(xs) {
display: block;
- .controls > .btn {
- margin-left: 0;
- margin-right: 0;
+ .controls > .btn,
+ .controls .member-form-control {
+ margin: 0 0 $gl-padding-8;
display: block;
}
- .controls > .btn:last-child {
- margin-left: 5px;
- margin-right: 5px;
- width: auto;
- }
-
.form-control {
width: 100%;
}
-
- .member-access-text {
- line-height: 0;
- margin-left: 50px;
- }
-
- .member-controls {
- margin-top: 5px;
- }
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ab5a9e170f0..77b40fe2d30 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -185,46 +185,6 @@
}
}
}
-
- .accept-control {
- display: inline-block;
- float: left;
- margin: 0;
- margin-left: 20px;
- padding: 5px;
- padding-top: 8px;
- line-height: 20px;
-
- &.right {
- float: right;
- padding-right: 0;
- }
-
- .modify-merge-commit-link {
- padding: 0;
- background-color: transparent;
- border: 0;
- color: $gl-text-color;
-
- &:hover,
- &:focus {
- text-decoration: underline;
- }
- }
-
- .merge-param-checkbox {
- margin: 0;
- }
-
- a .fa-question-circle {
- color: $gl-text-color-secondary;
-
- &:hover,
- &:focus {
- color: $link-hover-color;
- }
- }
- }
}
.ci-widget {
@@ -407,12 +367,6 @@
width: 100%;
text-align: center;
}
-
- .accept-control {
- width: 100%;
- text-align: center;
- margin: 0;
- }
}
.commit-message-editor {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 8c7b124dd33..267fdfa5d86 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -103,6 +103,11 @@
margin: auto;
align-items: center;
+ a {
+ color: $orange-600;
+ text-decoration: underline;
+ }
+
.icon {
margin-right: $issuable-warning-icon-margin;
vertical-align: text-bottom;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 32477c20db6..52677b7df60 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -762,7 +762,7 @@ $note-form-margin-left: 72px;
background-color: $white-light;
}
- a {
+ a:not(.learn-more) {
color: $blue-600;
}
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 6e98d66d712..7321f719deb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -440,6 +440,8 @@ class ApplicationController < ActionController::Base
end
def set_session_storage(&block)
+ return yield if sessionless_user?
+
Gitlab::Session.with_session(session, &block)
end
diff --git a/app/controllers/concerns/import_url_params.rb b/app/controllers/concerns/import_url_params.rb
new file mode 100644
index 00000000000..e51e4157f50
--- /dev/null
+++ b/app/controllers/concerns/import_url_params.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ImportUrlParams
+ def import_url_params
+ return {} unless params.dig(:project, :import_url).present?
+
+ { import_url: import_params_to_full_url(params[:project]) }
+ end
+
+ def import_params_to_full_url(params)
+ Gitlab::UrlSanitizer.new(
+ params[:import_url],
+ credentials: {
+ user: params[:import_url_user],
+ password: params[:import_url_password]
+ }
+ ).full_url
+ end
+end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index cfff154c3dd..8b8b7db72f8 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -26,16 +26,22 @@ module MilestoneActions
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
+ milestone_labels = @milestone.issue_labels_visible_by_user(current_user)
+
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels.map { |label| label.present(issuable_subject: @milestone.parent) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ labels: milestone_labels.map do |label|
+ label.present(issuable_subject: @milestone.parent)
+ end
})
end
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 70811f5ea59..65d14781d92 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -6,18 +6,14 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
+ before_action :projects, only: [:index]
before_action :default_sorting
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true))
-
respond_to do |format|
format.html do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
+ render_projects
end
format.atom do
load_events
@@ -51,6 +47,17 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
+ def projects
+ @projects ||= load_projects(params.merge(non_public: true))
+ end
+
+ def render_projects
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
+ end
+
def default_sorting
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7a80da53025..956093b972b 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -15,6 +15,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
+ skip_around_action :set_session_storage
skip_before_action :verify_authenticity_token
skip_before_action :repository
before_action :authenticate_user
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 4640be015de..afbf9fd7720 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -2,6 +2,7 @@
class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
+ include ImportUrlParams
# Authorize
before_action :authorize_admin_project!
@@ -67,10 +68,12 @@ class Projects::ImportsController < Projects::ApplicationController
end
def import_params_attributes
- [:import_url]
+ []
end
def import_params
- params.require(:project).permit(import_params_attributes)
+ params.require(:project)
+ .permit(import_params_attributes)
+ .merge(import_url_params)
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a3ceb76845e..135117926be 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
close_merge_request_if_no_source_project
- mark_merge_request_mergeable
+ @merge_request.check_mergeability
respond_to do |format|
format.html do
@@ -251,10 +251,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
- def mark_merge_request_mergeable
- @merge_request.check_if_can_be_merged
- end
-
def merge!
# Disable the CI check if auto_merge_strategy is specified since we have
# to wait until CI completes to know
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index 58b1bc54181..89f21d8dadb 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -65,11 +65,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController
private
def create_params
- params.require(:pages_domain).permit(:key, :certificate, :domain)
+ params.require(:pages_domain).permit(:key, :certificate, :domain, :auto_ssl_enabled)
end
def update_params
- params.require(:pages_domain).permit(:key, :certificate)
+ params.require(:pages_domain).permit(:key, :certificate, :auto_ssl_enabled)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 79030da64d3..4b0d001fca6 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -10,15 +10,13 @@ module Projects
format.json do
functions = finder.execute
- if functions.any?
- render json: serialize_function(functions)
- else
- head :no_content
- end
+ render json: {
+ knative_installed: finder.knative_installed,
+ functions: serialize_function(functions)
+ }.to_json
end
format.html do
- @installed = finder.installed?
render
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e88c46144ef..12db493978b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown
include SendFileUpload
include RecordUserLastActivity
+ include ImportUrlParams
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@@ -333,6 +334,7 @@ class ProjectsController < Projects::ApplicationController
def project_params(attributes: [])
params.require(:project)
.permit(project_params_attributes + attributes)
+ .merge(import_url_params)
end
def project_params_attributes
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index a80ab3bcd28..cb25548c83f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -25,6 +25,7 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_objects = search_service.search_objects
+ @display_options = search_service.display_options
render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users'
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 6fea61cf45d..a841859621e 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -18,6 +18,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
+ prepend_before_action :ensure_password_authentication_enabled!, if: :password_based_login?, only: [:create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -138,6 +139,14 @@ class SessionsController < Devise::SessionsController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def ensure_password_authentication_enabled!
+ render_403 unless Gitlab::CurrentSettings.password_authentication_enabled_for_web?
+ end
+
+ def password_based_login?
+ user_params[:login].present? || user_params[:password].present?
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end
diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb
new file mode 100644
index 00000000000..7d3b53ef663
--- /dev/null
+++ b/app/finders/clusters/knative_services_finder.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+module Clusters
+ class KnativeServicesFinder
+ include ReactiveCaching
+ include Gitlab::Utils::StrongMemoize
+
+ KNATIVE_STATES = {
+ 'checking' => 'checking',
+ 'installed' => 'installed',
+ 'not_found' => 'not_found'
+ }.freeze
+
+ self.reactive_cache_key = ->(finder) { finder.model_name }
+ self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
+
+ attr_reader :cluster, :project
+
+ def initialize(cluster, project)
+ @cluster = cluster
+ @project = project
+ end
+
+ def with_reactive_cache_memoized(*cache_args, &block)
+ strong_memoize(:reactive_cache) do
+ with_reactive_cache(*cache_args, &block)
+ end
+ end
+
+ def clear_cache!
+ clear_reactive_cache!(*cache_args)
+ end
+
+ def self.from_cache(cluster_id, project_id)
+ cluster = Clusters::Cluster.find(cluster_id)
+ project = ::Project.find(project_id)
+
+ new(cluster, project)
+ end
+
+ def calculate_reactive_cache(*)
+ # read_services calls knative_client.discover implicitily. If we stop
+ # detecting services but still want to detect knative, we'll need to
+ # explicitily call: knative_client.discover
+ #
+ # We didn't create it separately to avoid 2 cluster requests.
+ ksvc = read_services
+ pods = knative_client.discovered ? read_pods : []
+ { services: ksvc, pods: pods, knative_detected: knative_client.discovered }
+ end
+
+ def services
+ return [] unless search_namespace
+
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:services, [])
+ end
+
+ def cache_args
+ [cluster.id, project.id]
+ end
+
+ def service_pod_details(service)
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:pods, []).select do |pod|
+ filter_pods(pod, service)
+ end
+ end
+
+ def knative_detected
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+
+ knative_state = cached_data.to_h[:knative_detected]
+
+ return KNATIVE_STATES['checking'] if knative_state.nil?
+ return KNATIVE_STATES['installed'] if knative_state
+
+ KNATIVE_STATES['uninstalled']
+ end
+
+ def model_name
+ self.class.name.underscore.tr('/', '_')
+ end
+
+ private
+
+ def search_namespace
+ @search_namespace ||= cluster.kubernetes_namespace_for(project)
+ end
+
+ def knative_client
+ cluster.kubeclient.knative_client
+ end
+
+ def filter_pods(pod, service)
+ pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+ end
+
+ def read_services
+ knative_client.get_services(namespace: search_namespace).as_json
+ rescue Kubeclient::ResourceNotFoundError
+ []
+ end
+
+ def read_pods
+ cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json
+ end
+
+ def id
+ nil
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index e5bffccabfe..ebe50806ca1 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -14,8 +14,16 @@ module Projects
knative_services.flatten.compact
end
- def installed?
- clusters_with_knative_installed.exists?
+ # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
+ def knative_installed
+ states = @clusters.map do |cluster|
+ cluster.application_knative
+ cluster.knative_services_finder(project).knative_detected.tap do |state|
+ return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
+ end
+ end
+
+ states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
end
def service(environment_scope, name)
@@ -25,7 +33,7 @@ module Projects
def invocation_metrics(environment_scope, name)
return unless prometheus_adapter&.can_query?
- cluster = clusters_with_knative_installed.preload_knative.find do |c|
+ cluster = @clusters.find do |c|
environment_scope == c.environment_scope
end
@@ -34,7 +42,7 @@ module Projects
end
def has_prometheus?(environment_scope)
- clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+ @clusters.any? do |cluster|
environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
end
end
@@ -42,10 +50,12 @@ module Projects
private
def knative_service(environment_scope, name)
- clusters_with_knative_installed.preload_knative.map do |cluster|
+ @clusters.map do |cluster|
next if environment_scope != cluster.environment_scope
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ services = cluster
+ .knative_services_finder(project)
+ .services
.select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil?
@@ -53,8 +63,11 @@ module Projects
end
def knative_services
- clusters_with_knative_installed.preload_knative.map do |cluster|
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ @clusters.map do |cluster|
+ services = cluster
+ .knative_services_finder(project)
+ .services
+
add_metadata(cluster, services) unless services.nil?
end
end
@@ -65,17 +78,14 @@ module Projects
s["cluster_id"] = cluster.id
if services.length == 1
- s["podcount"] = cluster.application_knative.service_pod_details(
- cluster.kubernetes_namespace_for(project),
- s["metadata"]["name"]).length
+ s["podcount"] = cluster
+ .knative_services_finder(project)
+ .service_pod_details(s["metadata"]["name"])
+ .length
end
end
end
- def clusters_with_knative_installed
- @clusters.with_knative_installed
- end
-
# rubocop: disable CodeReuse/ServiceClass
def prometheus_adapter
@prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index f8ad6bee21b..2e5bdbd79c8 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -45,6 +45,31 @@ class GitlabSchema < GraphQL::Schema
super(query_str, **kwargs)
end
+ def id_from_object(object)
+ unless object.respond_to?(:to_global_id)
+ # This is an error in our schema and needs to be solved. So raise a
+ # more meaningfull error message
+ raise "#{object} does not implement `to_global_id`. "\
+ "Include `GlobalID::Identification` into `#{object.class}"
+ end
+
+ object.to_global_id
+ end
+
+ def object_from_id(global_id)
+ gid = GlobalID.parse(global_id)
+
+ unless gid
+ raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab id."
+ end
+
+ if gid.model_class < ApplicationRecord
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
+ else
+ gid.find
+ end
+ end
+
private
def max_query_complexity(ctx)
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
index 7d0cb777ad1..e85d16fc2c5 100644
--- a/app/graphql/mutations/merge_requests/base.rb
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -10,7 +10,7 @@ module Mutations
required: true,
description: "The project the merge request to mutate is in"
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the merge request to mutate"
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 3ee3849f483..6988b451ec3 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class IssuesResolver < BaseResolver
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'The IID of the issue, e.g., "1"'
- argument :iids, [GraphQL::ID_TYPE],
+ argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'The list of IIDs of issues, e.g., [1, 2]'
argument :state, Types::IssuableStateEnum,
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 90795c797ac..b84e60066e1 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class MergeRequestsResolver < BaseResolver
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'The IID of the merge request, e.g., "1"'
- argument :iids, [GraphQL::ID_TYPE],
+ argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'The list of IIDs of issues, e.g., [1, 2]'
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index a374851e835..dd0d9105df6 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -29,15 +29,18 @@ module Types
# proc because we set complexity depending on arguments and number of
# items which can be loaded.
proc do |ctx, args, child_complexity|
- page_size = @max_page_size || ctx.schema.default_max_page_size
- limit_value = [args[:first], args[:last], page_size].compact.min
-
# Resolvers may add extra complexity depending on used arguments
complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
- # Resolvers may add extra complexity depending on number of items being loaded.
- multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
- complexity += complexity * limit_value * multiplier
+ field_defn = to_graphql
+
+ if field_defn.connection?
+ # Resolvers may add extra complexity depending on number of items being loaded.
+ page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size
+ limit_value = [args[:first], args[:last], page_size].compact.min
+ multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
+ complexity += complexity * limit_value * multiplier
+ end
complexity.to_i
end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index 82b78abd573..e40059c46bb 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -6,5 +6,10 @@ module Types
prepend Gitlab::Graphql::ExposePermissions
field_class Types::BaseField
+
+ # All graphql fields exposing an id, should expose a global id.
+ def id
+ GitlabSchema.id_from_object(object)
+ end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index de7d6570a3e..cff81e5670b 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -10,7 +10,7 @@ module Types
expose_permissions Types::PermissionTypes::Ci::Pipeline
field :id, GraphQL::ID_TYPE, null: false
- field :iid, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::STRING_TYPE, null: false
field :sha, GraphQL::STRING_TYPE, null: false
field :before_sha, GraphQL::STRING_TYPE, null: true
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 120ffe0dfde..85ac3102442 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -11,7 +11,7 @@ module Types
present_using MergeRequestPresenter
field :id, GraphQL::ID_TYPE, null: false
- field :iid, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::STRING_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :state, MergeRequestStateEnum, null: false
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index 35ae23c21fc..62537361918 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -11,5 +11,6 @@ module Types
field :lfs_objects_size, GraphQL::INT_TYPE, null: false
field :build_artifacts_size, GraphQL::INT_TYPE, null: false
field :packages_size, GraphQL::INT_TYPE, null: false
+ field :wiki_size, GraphQL::INT_TYPE, null: true
end
end
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 230624201b0..f2b7d5df2b2 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -4,7 +4,11 @@ module Types
class BlobType < BaseObject
implements Types::Tree::EntryType
+ present_using BlobPresenter
+
graphql_name 'Blob'
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
end
end
end
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index d5cfb898aea..23ec2ef0ec2 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -4,8 +4,12 @@ module Types
class TreeEntryType < BaseObject
implements Types::Tree::EntryType
+ present_using TreeEntryPresenter
+
graphql_name 'TreeEntry'
description 'Represents a directory'
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
end
end
end
diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index 1eb6c43972e..1ee93ed9542 100644
--- a/app/graphql/types/tree/tree_type.rb
+++ b/app/graphql/types/tree/tree_type.rb
@@ -4,9 +4,15 @@ module Types
class TreeType < BaseObject
graphql_name 'Tree'
- field :trees, Types::Tree::TreeEntryType.connection_type, null: false
+ field :trees, Types::Tree::TreeEntryType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+ Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository)
+ end
+
field :submodules, Types::Tree::SubmoduleType.connection_type, null: false
- field :blobs, Types::Tree::BlobType.connection_type, null: false
+
+ field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+ Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
+ end
end
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 971d1052824..4469118f065 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -160,6 +160,7 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable,
:authorized_keys_enabled,
:auto_devops_enabled,
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index 5bfdeb9e33c..e313015c937 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -17,7 +17,7 @@ module CiVariablesHelper
if variable && !only_key_value
variable.masked
else
- true
+ false
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index dc0e5511fcf..2beb081ab77 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -98,16 +98,17 @@ module EmailsHelper
case format
when :html
- " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}"
+ merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
+ _("via merge request %{link}").html_safe % { link: merge_request_link }
else
# If it's not HTML nor text then assume it's text to be safe
- " via merge request #{merge_request.to_reference} (#{merge_request.web_url})"
+ _("via merge request %{link}") % { link: "#{merge_request.to_reference} (#{merge_request.web_url})" }
end
when String
# Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob
- " via #{closed_via}"
+ _("via %{closed_via}") % { closed_via: closed_via }
else
""
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index a7ce7667916..11b9cf22142 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -100,4 +100,8 @@ module NotificationsHelper
css_class: "icon notifications-icon js-notifications-icon"
)
end
+
+ def show_unsubscribe_title?(noteable)
+ can?(current_user, "read_#{noteable.to_ability_name}".to_sym, noteable)
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e587cf4045d..8dee842a22d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -241,6 +241,7 @@ module ProjectsHelper
# TODO: Remove this method when removing the feature flag
# https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234863
+ # make sure to remove from the EE specific controller as well: ee/app/controllers/ee/dashboard/projects_controller.rb
def show_projects?(projects, params)
Feature.enabled?(:project_list_filter_bar) || !!(params[:personal] || params[:name] || any_projects?(projects))
end
diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb
new file mode 100644
index 00000000000..51ea79d1ddd
--- /dev/null
+++ b/app/helpers/tracking_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module TrackingHelper
+ def tracking_attrs(label, event, property)
+ {} # CE has no tracking features
+ end
+end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 9deb783d289..b318b27992a 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -165,8 +165,46 @@ module VisibilityLevelHelper
!form_model.visibility_level_allowed?(level)
end
+ # Visibility level can be restricted in two ways:
+ #
+ # 1. The group permissions (e.g. a subgroup is private, which requires
+ # all projects to be private)
+ # 2. The global allowed visibility settings, set by the admin
+ def selected_visibility_level(form_model, requested_level)
+ requested_level =
+ if requested_level.present?
+ requested_level.to_i
+ else
+ default_project_visibility
+ end
+
+ [requested_level, max_allowed_visibility_level(form_model)].min
+ end
+
private
+ def max_allowed_visibility_level(form_model)
+ # First obtain the maximum visibility for the project or group
+ current_level = max_allowed_visibility_level_by_model(form_model)
+
+ # Now limit this by the global setting
+ Gitlab::VisibilityLevel.closest_allowed_level(current_level)
+ end
+
+ def max_allowed_visibility_level_by_model(form_model)
+ current_level = Gitlab::VisibilityLevel::PRIVATE
+
+ Gitlab::VisibilityLevel.values.sort.each do |value|
+ if disallowed_visibility_level?(form_model, value)
+ break
+ else
+ current_level = value
+ end
+ end
+
+ current_level
+ end
+
def visibility_level_errors_for_group(group, level_name)
group_name = link_to group.name, group_path(group)
change_visiblity = link_to 'change the visibility', edit_group_path(group)
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e51619b0f9c..904d650ef96 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -21,6 +21,7 @@ module ApplicationSettingImplementation
after_sign_up_text: nil,
akismet_enabled: false,
allow_local_requests_from_hooks_and_services: false,
+ dns_rebinding_protection_enabled: true,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f743fca423a..89cc082d0bc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,6 @@ module Ci
include Gitlab::Utils::StrongMemoize
include Deployable
include HasRef
- include UpdateProjectStatistics
BuildArchivedError = Class.new(StandardError)
@@ -344,7 +343,7 @@ module Ci
end
def retryable?
- !archived? && (success? || failed?)
+ !archived? && (success? || failed? || canceled?)
end
def retries_count
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0dbeab30498..f80e98e5bca 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -59,7 +59,7 @@ module Ci
validate :valid_file_format?, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
- update_project_statistics stat: :build_artifacts_size
+ update_project_statistics project_statistics_name: :build_artifacts_size
after_save :update_file_store, if: :saved_change_to_file?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 80401ca0a1e..3727a9861aa 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -166,6 +166,16 @@ module Ci
end
end
+ after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
+ pipeline.run_after_commit do
+ pipeline.all_merge_requests.each do |merge_request|
+ next unless merge_request.auto_merge_enabled?
+
+ AutoMergeProcessWorker.perform_async(merge_request.id)
+ end
+ end
+ end
+
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index c0a0ca9acf6..c40ad39be61 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -27,9 +27,13 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
+ scope :runnable_schedules, -> { active.where("next_run_at < ?", Time.now) }
+ scope :preloaded, -> { preload(:owner, :project) }
accepts_nested_attributes_for :variables, allow_destroy: true
+ alias_attribute :real_next_run, :next_run_at
+
def owned_by?(current_user)
owner == current_user
end
@@ -46,8 +50,14 @@ module Ci
update_attribute(:active, false)
end
+ ##
+ # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`.
+ # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval
+ # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer.
def set_next_run_at
- self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ self.next_run_at = Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'],
+ Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
def schedule_next_run!
@@ -56,15 +66,14 @@ module Ci
update_attribute(:next_run_at, nil) # update without validation
end
- def real_next_run(
- worker_cron: Settings.cron_jobs['pipeline_schedule_worker']['cron'],
- worker_time_zone: Time.zone.name)
- Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
- .next_time_from(next_run_at)
- end
-
def job_variables
variables&.map(&:to_runner_variable) || []
end
+
+ private
+
+ def ideal_next_run_at
+ Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ end
end
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index bd9c453e2a4..4aaa1f941e5 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -61,6 +61,10 @@ module Clusters
"http://#{hostname}/hub/oauth_callback"
end
+ def oauth_scopes
+ 'api read_repository write_repository'
+ end
+
private
def specification
@@ -94,7 +98,8 @@ module Clusters
},
"singleuser" => {
"extraEnv" => {
- "GITLAB_CLUSTER_ID" => cluster.id.to_s
+ "GITLAB_CLUSTER_ID" => cluster.id.to_s,
+ "GITLAB_HOST" => gitlab_host
}
}
}
@@ -112,6 +117,10 @@ module Clusters
Gitlab.config.gitlab.url
end
+ def gitlab_host
+ Gitlab.config.gitlab.host
+ end
+
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 9fbf5d8af04..d5a3bd62e3d 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,9 +15,6 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
- include ReactiveCaching
-
- self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
def set_initial_status
return unless not_installable?
@@ -41,8 +38,6 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
- after_save :clear_reactive_cache!
-
def chart
'knative/knative'
end
@@ -77,55 +72,12 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
- def client
- cluster.kubeclient.knative_client
- end
-
- def services
- with_reactive_cache do |data|
- data[:services]
- end
- end
-
- def calculate_reactive_cache
- { services: read_services, pods: read_pods }
- end
-
def ingress_service
cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
end
- def services_for(ns: namespace)
- return [] unless services
- return [] unless ns
-
- services.select do |service|
- service.dig('metadata', 'namespace') == ns
- end
- end
-
- def service_pod_details(ns, service)
- with_reactive_cache do |data|
- data[:pods].select { |pod| filter_pods(pod, ns, service) }
- end
- end
-
private
- def read_pods
- cluster.kubeclient.core_client.get_pods.as_json
- end
-
- def filter_pods(pod, namespace, service)
- pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
- end
-
- def read_services
- client.get_services.as_json
- rescue Kubeclient::ResourceNotFoundError
- []
- end
-
def install_knative_metrics
["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 0dff91c3fe2..db7fd8524c2 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.5.1'.freeze
+ VERSION = '0.5.2'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 57a1e461b2d..e1d6b2a802b 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -223,6 +223,10 @@ module Clusters
end
end
+ def knative_services_finder(project)
+ @knative_services_finder ||= KnativeServicesFinder.new(self, project)
+ end
+
private
def instance_domain
diff --git a/app/models/commit.rb b/app/models/commit.rb
index f412d252e5c..fa0bf36ba49 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -13,6 +13,7 @@ class Commit
include StaticModel
include Presentable
include ::Gitlab::Utils::StrongMemoize
+ include CacheMarkdownField
attr_mentionable :safe_message, pipeline: :single_line
@@ -37,13 +38,9 @@ class Commit
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
- def banzai_render_context(field)
- pipeline = field == :description ? :commit_description : :single_line
- context = { pipeline: pipeline, project: self.project }
- context[:author] = self.author if self.author
-
- context
- end
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :full_title, pipeline: :single_line
+ cache_markdown_field :description, pipeline: :commit_description
class << self
def decorate(commits, project)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index f90cd1ea690..42203a5f214 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -13,43 +13,9 @@
module CacheMarkdownField
extend ActiveSupport::Concern
- # Increment this number every time the renderer changes its output
- CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 16
-
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
- # Knows about the relationship between markdown and html field names, and
- # stores the rendering contexts for the latter
- class FieldData
- def initialize
- @data = {}
- end
-
- delegate :[], :[]=, to: :@data
-
- def markdown_fields
- @data.keys
- end
-
- def html_field(markdown_field)
- "#{markdown_field}_html"
- end
-
- def html_fields
- markdown_fields.map { |field| html_field(field) }
- end
-
- def html_fields_whitelisted
- markdown_fields.each_with_object([]) do |field, fields|
- if @data[field].fetch(:whitelisted, false)
- fields << html_field(field)
- end
- end
- end
- end
-
def skip_project_check?
false
end
@@ -85,24 +51,22 @@ module CacheMarkdownField
end.to_h
updates['cached_markdown_version'] = latest_cached_markdown_version
- updates.each {|html_field, data| write_attribute(html_field, data) }
+ updates.each { |field, data| write_markdown_field(field, data) }
end
def refresh_markdown_cache!
updates = refresh_markdown_cache
- return unless persisted? && Gitlab::Database.read_write?
-
- update_columns(updates)
+ save_markdown(updates)
end
def cached_html_up_to_date?(markdown_field)
- html_field = cached_markdown_fields.html_field(markdown_field)
+ return false if cached_html_for(markdown_field).nil? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
- return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend
+ html_field = cached_markdown_fields.html_field(markdown_field)
- markdown_changed = attribute_changed?(markdown_field) || false
- html_changed = attribute_changed?(html_field) || false
+ markdown_changed = markdown_field_changed?(markdown_field)
+ html_changed = markdown_field_changed?(html_field)
latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
@@ -117,21 +81,21 @@ module CacheMarkdownField
end
def cached_html_for(markdown_field)
- raise ArgumentError.new("Unknown field: #{field}") unless
+ raise ArgumentError.new("Unknown field: #{markdown_field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
def latest_cached_markdown_version
- @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version
+ @latest_cached_markdown_version ||= (Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) | local_version
end
def local_version
# because local_markdown_version is stored in application_settings which
# uses cached_markdown_version too, we check explicitly to avoid
# endless loop
- return local_markdown_version if has_attribute?(:local_markdown_version)
+ return local_markdown_version if respond_to?(:has_attribute?) && has_attribute?(:local_markdown_version)
settings = Gitlab::CurrentSettings.current_application_settings
@@ -150,32 +114,14 @@ module CacheMarkdownField
included do
cattr_reader :cached_markdown_fields do
- FieldData.new
+ Gitlab::MarkdownCache::FieldData.new
end
- # Always exclude _html fields from attributes (including serialization).
- # They contain unredacted HTML, which would be a security issue
- alias_method :attributes_before_markdown_cache, :attributes
- def attributes
- attrs = attributes_before_markdown_cache
- html_fields = cached_markdown_fields.html_fields
- whitelisted = cached_markdown_fields.html_fields_whitelisted
- exclude_fields = html_fields - whitelisted
-
- exclude_fields.each do |field|
- attrs.delete(field)
- end
-
- if whitelisted.empty?
- attrs.delete('cached_markdown_version')
- end
-
- attrs
+ if self < ActiveRecord::Base
+ include Gitlab::MarkdownCache::ActiveRecord::Extension
+ else
+ prepend Gitlab::MarkdownCache::Redis::Extension
end
-
- # Using before_update here conflicts with elasticsearch-model somehow
- before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
- before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end
class_methods do
@@ -193,10 +139,8 @@ module CacheMarkdownField
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
- changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
- invalidations.delete(markdown_field.to_s) if changed_fields.include?("#{markdown_field}_html")
-
+ invalidations = changed_markdown_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ invalidations.delete(markdown_field.to_s) if changed_markdown_fields.include?("#{markdown_field}_html")
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e65bbb8ca07..3deb86da6cf 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,28 +1,20 @@
# frozen_string_literal: true
module Milestoneish
- def closed_items_count(user)
- memoize_per_user(user, :closed_items_count) do
- (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
- end
- end
-
- def total_items_count(user)
- memoize_per_user(user, :total_items_count) do
- total_issues_count(user) + merge_requests.size
- end
- end
-
def total_issues_count(user)
count_issues_by_state(user).values.sum
end
+ def closed_issues_count(user)
+ count_issues_by_state(user)['closed'].to_i
+ end
+
def complete?(user)
- total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
+ total_issues_count(user) > 0 && total_issues_count(user) == closed_issues_count(user)
end
def percent_complete(user)
- ((closed_items_count(user) * 100) / total_items_count(user)).abs
+ closed_issues_count(user) * 100 / total_issues_count(user)
rescue ZeroDivisionError
0
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index bfd0c36942b..4b428b0af83 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,14 +3,16 @@
module Noteable
extend ActiveSupport::Concern
- # `Noteable` class names that support resolvable notes.
- RESOLVABLE_TYPES = %w(MergeRequest).freeze
-
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
%w(Issue MergeRequest)
end
+
+ # `Noteable` class names that support resolvable notes.
+ def resolvable_types
+ %w(MergeRequest)
+ end
end
# The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
@@ -36,7 +38,7 @@ module Noteable
end
def supports_resolvable_notes?
- RESOLVABLE_TYPES.include?(base_class_name)
+ self.class.resolvable_types.include?(base_class_name)
end
def supports_discussions?
@@ -131,3 +133,5 @@ module Noteable
)
end
end
+
+Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 16ea330701d..2d2d5fb7168 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -12,7 +12,7 @@ module ResolvableNote
validates :resolved_by, presence: true, if: :resolved?
# Keep this scope in sync with `#potentially_resolvable?`
- scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) }
+ scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable.resolvable_types) }
# Keep this scope in sync with `#resolvable?`
scope :resolvable, -> { potentially_resolvable.user }
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 67e1f0ec930..1f881249322 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -5,43 +5,47 @@
# It deals with `ProjectStatistics.increment_statistic` making sure not to update statistics on a cascade delete from the
# project, and keeping track of value deltas on each save. It updates the DB only when a change is needed.
#
-# How to use
-# - Invoke `update_project_statistics stat: :a_project_statistics_column, attribute: :an_attr_to_track` in a model class body.
+# Example:
#
-# Expectation
-# - `attribute` must be an ActiveRecord attribute
+# module Ci
+# class JobArtifact < ApplicationRecord
+# include UpdateProjectStatistics
+#
+# update_project_statistics project_statistics_name: :build_artifacts_size
+# end
+# end
+#
+# Expectation:
+#
+# - `statistic_attribute` must be an ActiveRecord attribute
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
+#
module UpdateProjectStatistics
extend ActiveSupport::Concern
class_methods do
- attr_reader :statistic_name, :statistic_attribute
+ attr_reader :project_statistics_name, :statistic_attribute
- # Configure the model to update +stat+ on ProjectStatistics when +attribute+ changes
+ # Configure the model to update `project_statistics_name` on ProjectStatistics,
+ # when `statistic_attribute` changes
+ #
+ # - project_statistics_name: A column of `ProjectStatistics` to update
+ # - statistic_attribute: An attribute of the current model, default to `size`
#
- # +stat+:: a column of ProjectStatistics to update
- # +attribute+:: an attribute of the current model, default to +:size+
- def update_project_statistics(stat:, attribute: :size)
- @statistic_name = stat
- @statistic_attribute = attribute
+ def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
+ @project_statistics_name = project_statistics_name
+ @statistic_attribute = statistic_attribute
after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?)
after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?)
end
+
private :update_project_statistics
end
included do
private
- def project_destroyed?
- project.pending_delete?
- end
-
- def update_project_statistics_attribute_changed?
- saved_change_to_attribute?(self.class.statistic_attribute)
- end
-
def update_project_statistics_after_save
attr = self.class.statistic_attribute
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
@@ -49,12 +53,20 @@ module UpdateProjectStatistics
update_project_statistics(delta)
end
+ def update_project_statistics_attribute_changed?
+ saved_change_to_attribute?(self.class.statistic_attribute)
+ end
+
def update_project_statistics_after_destroy
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
end
+ def project_destroyed?
+ project.pending_delete?
+ end
+
def update_project_statistics(delta)
- ProjectStatistics.increment_statistic(project_id, self.class.statistic_name, delta)
+ ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
end
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index feabea9b8ba..8221b7de2b6 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -15,7 +15,9 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
- validates :noteable_type, inclusion: { in: noteable_types }
+ # We need to evaluate the `noteable` types when running the validation since
+ # EE might have added a type when the module was prepended
+ validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
@@ -44,7 +46,7 @@ class DiffNote < Note
# Returns the diff file from `position`
def latest_diff_file
strong_memoize(:latest_diff_file) do
- position.diff_file(project.repository)
+ position.diff_file(repository)
end
end
@@ -75,7 +77,7 @@ class DiffNote < Note
end
def supports_suggestion?
- return false unless noteable.supports_suggestion? && on_text?
+ return false unless noteable&.supports_suggestion? && on_text?
# We don't want to trigger side-effects of `diff_file` call.
return false unless file = latest_diff_file
return false unless line = file.line_for_position(self.position)
@@ -111,7 +113,7 @@ class DiffNote < Note
if note_diff_file
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
- repository: project.repository,
+ repository: repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
@@ -122,7 +124,7 @@ class DiffNote < Note
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(original_position.diff_options).diff_files.first
else
- original_position.diff_file(self.project.repository)
+ original_position.diff_file(repository)
end
# Since persisted diff files already have its content "unfolded"
@@ -137,7 +139,7 @@ class DiffNote < Note
end
def set_line_code
- self.line_code = self.position.line_code(self.project.repository)
+ self.line_code = self.position.line_code(repository)
end
def verify_supported
@@ -171,6 +173,10 @@ class DiffNote < Note
shas << self.position.head_sha
end
- project.repository.keep_around(*shas)
+ repository.keep_around(*shas)
+ end
+
+ def repository
+ noteable.respond_to?(:repository) ? noteable.repository : project.repository
end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index b097be8cc89..8aa25924c28 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -59,6 +59,11 @@ class Key < ApplicationRecord
"key-#{id}"
end
+ # EE overrides this
+ def can_delete?
+ true
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def update_last_used_at
Keys::LastUsedService.new(self).execute
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2602738901b..59416fb4b51 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -589,6 +589,8 @@ class MergeRequest < ApplicationRecord
return
end
+ [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }
+
if opened?
similar_mrs = target_project
.merge_requests
@@ -609,6 +611,16 @@ class MergeRequest < ApplicationRecord
end
end
+ def validate_branch_name(attr)
+ return unless changes_include?(attr)
+
+ branch = read_attribute(attr)
+
+ return unless branch
+
+ errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
+ end
+
def validate_target_project
return true if target_project.merge_requests_enabled?
@@ -713,19 +725,16 @@ class MergeRequest < ApplicationRecord
MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
- # rubocop: enable CodeReuse/ServiceClass
- def check_if_can_be_merged
- return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
-
- can_be_merged =
- !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
+ def check_mergeability
+ MergeRequests::MergeabilityCheckService.new(self).execute
+ end
+ # rubocop: enable CodeReuse/ServiceClass
- if can_be_merged
- mark_as_mergeable
- else
- mark_as_unmergeable
- end
+ # Returns boolean indicating the merge_status should be rechecked in order to
+ # switch to either can_be_merged or cannot_be_merged.
+ def recheck_merge_status?
+ self.class.state_machines[:merge_status].check_state?(merge_status)
end
def merge_event
@@ -751,7 +760,7 @@ class MergeRequest < ApplicationRecord
def mergeable?(skip_ci_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
- check_if_can_be_merged
+ check_mergeability
can_be_merged? && !should_be_rebased?
end
@@ -766,15 +775,6 @@ class MergeRequest < ApplicationRecord
true
end
- def mergeable_to_ref?
- return false unless mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
-
- # Given the `merge_ref_path` will have the same
- # state the `target_branch` would have. Ideally
- # we need to check if it can be merged to it.
- project.repository.can_be_merged?(diff_head_sha, target_branch)
- end
-
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -1102,6 +1102,12 @@ class MergeRequest < ApplicationRecord
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
+ # Returns the current merge-ref HEAD commit.
+ #
+ def merge_ref_head
+ project.repository.commit(merge_ref_path)
+ end
+
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 6f057f79ef6..9b2bbb7eba5 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -50,7 +50,7 @@ class NotificationRecipient
when :mention
@type == :mention
when :participating
- !excluded_participating_action? && %i[participating mention watch].include?(@type)
+ @custom_action == :failed_pipeline || %i[participating mention].include?(@type)
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch
@@ -106,12 +106,6 @@ class NotificationRecipient
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
- def excluded_participating_action?
- return false unless @custom_action
-
- NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
- end
-
private
def read_ability
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 61af5c09ae4..8306b11a7b6 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -54,14 +54,11 @@ class NotificationSetting < ApplicationRecord
self.class.email_events(source)
end
- EXCLUDED_PARTICIPATING_EVENTS = [
- :success_pipeline
- ].freeze
-
EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request,
- :issue_due
- ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
+ :issue_due,
+ :success_pipeline
+ ].freeze
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
diff --git a/app/models/project.rb b/app/models/project.rb
index 20895923d3b..78d54571d94 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -407,6 +407,7 @@ class Project < ApplicationRecord
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
+ scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
@@ -597,6 +598,17 @@ class Project < ApplicationRecord
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
+
+ # Returns ids of projects with milestones available for given user
+ #
+ # Used on queries to find milestones which user can see
+ # For example: Milestone.where(project_id: ids_with_milestone_available_for(user))
+ def ids_with_milestone_available_for(user)
+ with_issues_enabled = with_issues_available_for_user(user).select(:id)
+ with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
+
+ from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
+ end
end
def all_pipelines
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 0542581c6e0..6bcb051bff6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -72,6 +72,8 @@ class ProjectFeature < ApplicationRecord
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
+ scope :for_project_id, -> (project) { where(project: project) }
+
def feature_available?(feature, user)
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d43f991bb3e..e05d3dd58ac 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1077,7 +1077,7 @@ class Repository
end
def rebase(user, merge_request)
- if Feature.disabled?(:two_step_rebase, default_enabled: false)
+ if Feature.disabled?(:two_step_rebase, default_enabled: true)
return rebase_deprecated(user, merge_request)
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 5dcc3e9945a..f1fc5e599eb 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -38,7 +38,9 @@ class Todo < ApplicationRecord
self
end
}, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+
belongs_to :user
+ belongs_to :issue, -> { where("target_type = 'Issue'") }, foreign_key: :target_id
delegate :name, :email, to: :author, prefix: true, allow_nil: true
@@ -59,6 +61,7 @@ class Todo < ApplicationRecord
scope :for_target, -> (id) { where(target_id: id) }
scope :for_commit, -> (id) { where(commit_id: id) }
scope :with_api_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
+ scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
state_machine :state, initial: :pending do
event :done do
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 6323c1b3389..c5675ef3ea3 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -13,4 +13,8 @@ class BlobPresenter < Gitlab::View::Presenter::Simple
plain: plain
)
end
+
+ def web_url
+ Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
+ end
end
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index ed3daf6585b..6d46e0bf18a 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -4,7 +4,6 @@ module Ci
class BuildRunnerPresenter < SimpleDelegator
include Gitlab::Utils::StrongMemoize
- DEFAULT_GIT_DEPTH_MERGE_REQUEST = 10
RUNNER_REMOTE_TAG_PREFIX = 'refs/tags/'.freeze
RUNNER_REMOTE_BRANCH_PREFIX = 'refs/remotes/origin/'.freeze
@@ -28,7 +27,6 @@ module Ci
def git_depth
strong_memoize(:git_depth) do
git_depth = variables&.find { |variable| variable[:key] == 'GIT_DEPTH' }&.dig(:value)
- git_depth ||= DEFAULT_GIT_DEPTH_MERGE_REQUEST if merge_request_ref?
git_depth.to_i
end
end
@@ -39,12 +37,13 @@ module Ci
if git_depth > 0
specs << refspec_for_branch(ref) if branch? || legacy_detached_merge_request_pipeline?
specs << refspec_for_tag(ref) if tag?
- specs << refspec_for_merge_request_ref if merge_request_ref?
else
specs << refspec_for_branch
specs << refspec_for_tag
end
+ specs << refspec_for_merge_request_ref if merge_request_ref?
+
specs
end
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 9e9b6973b8e..2561c3f0244 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -32,6 +32,11 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
request? && can_update?
end
+ # This functionality is only available in EE.
+ def can_override?
+ false
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
new file mode 100644
index 00000000000..7bb10cd1455
--- /dev/null
+++ b/app/presenters/tree_entry_presenter.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
+ presents :tree
+
+ def web_url
+ Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
+ end
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 914ad628a99..36e601f45c5 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -44,4 +44,12 @@ class IssueEntity < IssuableEntity
expose :preview_note_path do |issue|
preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid)
end
+
+ expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue|
+ help_page_path('user/project/issues/confidential_issues.md')
+ end
+
+ expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
+ help_page_path('user/discussions/index.md', anchor: 'lock-discussions')
+ end
end
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
new file mode 100644
index 00000000000..387d0351490
--- /dev/null
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineScheduleService < BaseService
+ def execute(schedule)
+ # Ensure `next_run_at` is set properly before creating a pipeline.
+ # Otherwise, multiple pipelines could be created in a short interval.
+ schedule.schedule_next_run!
+
+ RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner.id)
+ end
+ end
+end
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
index 14a45437287..a9feb60be6e 100644
--- a/app/services/clusters/applications/base_service.rb
+++ b/app/services/clusters/applications/base_service.rb
@@ -81,7 +81,7 @@ module Clusters
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
- scopes: 'api read_user openid',
+ scopes: application.oauth_scopes,
owner: current_user
}
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 2a19e57a94f..805721212ba 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -29,7 +29,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
- closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit)
+ closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit)
notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index bb9062e9b40..2cfed62ce49 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -60,31 +60,7 @@ module MergeRequests
end
def create_pipeline_for(merge_request, user)
- return unless can_create_pipeline_for?(merge_request)
-
- create_detached_merge_request_pipeline(merge_request, user)
- end
-
- def create_detached_merge_request_pipeline(merge_request, user)
- if can_use_merge_request_ref?(merge_request)
- Ci::CreatePipelineService.new(merge_request.source_project, user,
- ref: merge_request.ref_path)
- .execute(:merge_request_event, merge_request: merge_request)
- else
- Ci::CreatePipelineService.new(merge_request.source_project, user,
- ref: merge_request.source_branch)
- .execute(:merge_request_event, merge_request: merge_request)
- end
- end
-
- def can_create_pipeline_for?(merge_request)
- ##
- # UpdateMergeRequestsWorker could be retried by an exception.
- # pipelines for merge request should not be recreated in such case.
- return false if merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
- return false if merge_request.has_no_commits?
-
- true
+ MergeRequests::CreatePipelineService.new(project, user).execute(merge_request)
end
def can_use_merge_request_ref?(merge_request)
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index e77051bb1c9..b0f6166ea1c 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -18,6 +18,7 @@ module MergeRequests
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
cleanup_environments(merge_request)
+ cancel_auto_merge(merge_request)
end
merge_request
@@ -33,5 +34,9 @@ module MergeRequests
merge_request_metrics_service(merge_request).close(close_event)
end
end
+
+ def cancel_auto_merge(merge_request)
+ AutoMergeService.new(project, current_user).cancel(merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
new file mode 100644
index 00000000000..03246cc1920
--- /dev/null
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class CreatePipelineService < MergeRequests::BaseService
+ def execute(merge_request)
+ return unless can_create_pipeline_for?(merge_request)
+
+ create_detached_merge_request_pipeline(merge_request)
+ end
+
+ def create_detached_merge_request_pipeline(merge_request)
+ if can_use_merge_request_ref?(merge_request)
+ Ci::CreatePipelineService.new(merge_request.source_project, current_user,
+ ref: merge_request.ref_path)
+ .execute(:merge_request_event, merge_request: merge_request)
+ else
+ Ci::CreatePipelineService.new(merge_request.source_project, current_user,
+ ref: merge_request.source_branch)
+ .execute(:merge_request_event, merge_request: merge_request)
+ end
+ end
+
+ def can_create_pipeline_for?(merge_request)
+ ##
+ # UpdateMergeRequestsWorker could be retried by an exception.
+ # pipelines for merge request should not be recreated in such case.
+ return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
+ return false if merge_request.has_no_commits?
+
+ true
+ end
+
+ def allow_duplicate
+ params[:allow_duplicate]
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index 87147d90c32..8670b9ccf3d 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -20,20 +20,14 @@ module MergeRequests
raise_error('Conflicts detected during merge') unless commit_id
- commit = project.commit(commit_id)
- target_id, source_id = commit.parent_ids
-
- success(commit_id: commit.id,
- target_id: target_id,
- source_id: source_id)
- rescue MergeError => error
+ success(commit_id: commit_id)
+ rescue MergeError, ArgumentError => error
error(error.message)
end
private
def validate!
- authorization_check!
error_check!
end
@@ -43,21 +37,13 @@ module MergeRequests
error =
if !hooks_validation_pass?(merge_request)
hooks_validation_error(merge_request)
- elsif !@merge_request.mergeable_to_ref?
- "Merge request is not mergeable to #{target_ref}"
- elsif !source
+ elsif source.blank?
'No source for merge'
end
raise_error(error) if error
end
- def authorization_check!
- unless Ability.allowed?(current_user, :admin_merge_request, project)
- raise_error("You are not allowed to merge to this ref")
- end
- end
-
def target_ref
merge_request.merge_ref_path
end
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
new file mode 100644
index 00000000000..ef833774e65
--- /dev/null
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class MergeabilityCheckService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :project, to: :@merge_request
+ delegate :repository, to: :project
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ # Updates the MR merge_status. Whenever it switches to a can_be_merged state,
+ # the merge-ref is refreshed.
+ #
+ # Returns a ServiceResponse indicating merge_status is/became can_be_merged
+ # and the merge-ref is synced. Success in case of being/becoming mergeable,
+ # error otherwise.
+ def execute
+ return ServiceResponse.error(message: 'Invalid argument') unless merge_request
+ return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only?
+
+ update_merge_status
+
+ unless merge_request.can_be_merged?
+ return ServiceResponse.error(message: 'Merge request is not mergeable')
+ end
+
+ unless payload.fetch(:merge_ref_head)
+ return ServiceResponse.error(message: 'Merge ref was not found')
+ end
+
+ ServiceResponse.success(payload: payload)
+ end
+
+ private
+
+ attr_reader :merge_request
+
+ def payload
+ strong_memoize(:payload) do
+ {
+ merge_ref_head: merge_ref_head_payload
+ }
+ end
+ end
+
+ def merge_ref_head_payload
+ commit = merge_request.merge_ref_head
+
+ return unless commit
+
+ target_id, source_id = commit.parent_ids
+
+ {
+ commit_id: commit.id,
+ source_id: source_id,
+ target_id: target_id
+ }
+ end
+
+ def update_merge_status
+ return unless merge_request.recheck_merge_status?
+
+ if can_git_merge?
+ merge_to_ref && merge_request.mark_as_mergeable
+ else
+ merge_request.mark_as_unmergeable
+ end
+ end
+
+ def can_git_merge?
+ !merge_request.broken? && repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
+ end
+
+ def merge_to_ref
+ result = MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ result[:status] == :success
+ end
+ end
+end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index dfa7bd20254..2bc04470342 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -64,6 +64,7 @@ module Projects
if project.previous_changes.include?(:visibility_level) && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
elsif (project_changed_feature_keys & todos_features_changes).present?
TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index e0cbfac2420..302510341ac 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -52,6 +52,10 @@ class SearchService
@search_objects ||= search_results.objects(scope, params[:page])
end
+ def display_options
+ @display_options ||= search_results.display_options(scope)
+ end
+
private
def search_service
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 1de30e68d87..f3437ba16de 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -1,19 +1,20 @@
# frozen_string_literal: true
class ServiceResponse
- def self.success(message: nil)
- new(status: :success, message: message)
+ def self.success(message: nil, payload: {})
+ new(status: :success, message: message, payload: payload)
end
- def self.error(message:, http_status: nil)
- new(status: :error, message: message, http_status: http_status)
+ def self.error(message:, payload: {}, http_status: nil)
+ new(status: :error, message: message, payload: payload, http_status: http_status)
end
- attr_reader :status, :message, :http_status
+ attr_reader :status, :message, :http_status, :payload
- def initialize(status:, message: nil, http_status: nil)
+ def initialize(status:, message: nil, payload: {}, http_status: nil)
self.status = status
self.message = message
+ self.payload = payload
self.http_status = http_status
end
@@ -27,5 +28,5 @@ class ServiceResponse
private
- attr_writer :status, :message, :http_status
+ attr_writer :status, :message, :http_status, :payload
end
diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb
index f3f1dbb5698..7378f10e7c4 100644
--- a/app/services/todos/destroy/base_service.rb
+++ b/app/services/todos/destroy/base_service.rb
@@ -13,7 +13,7 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord
def without_authorized(items)
- items.where('user_id NOT IN (?)', authorized_users)
+ items.where('todos.user_id NOT IN (?)', authorized_users)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
index 6276e332448..6cdd8c16894 100644
--- a/app/services/todos/destroy/confidential_issue_service.rb
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -2,36 +2,55 @@
module Todos
module Destroy
+ # Service class for deleting todos that belongs to confidential issues.
+ # It deletes todos for users that are not at least reporters, issue author or assignee.
+ #
+ # Accepts issue_id or project_id as argument.
+ # When issue_id is passed it deletes matching todos for one confidential issue.
+ # When project_id is passed it deletes matching todos for all confidential issues of the project.
class ConfidentialIssueService < ::Todos::Destroy::BaseService
extend ::Gitlab::Utils::Override
- attr_reader :issue
+ attr_reader :issues
# rubocop: disable CodeReuse/ActiveRecord
- def initialize(issue_id)
- @issue = Issue.find_by(id: issue_id)
+ def initialize(issue_id: nil, project_id: nil)
+ @issues =
+ if issue_id
+ Issue.where(id: issue_id)
+ elsif project_id
+ project_confidential_issues(project_id)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
private
+ def project_confidential_issues(project_id)
+ project = Project.find(project_id)
+
+ project.issues.confidential_only
+ end
+
override :todos
# rubocop: disable CodeReuse/ActiveRecord
def todos
- Todo.where(target: issue)
- .where('user_id != ?', issue.author_id)
- .where('user_id NOT IN (?)', issue.assignees.select(:id))
+ Todo.joins_issue_and_assignees
+ .where(target: issues)
+ .where('issues.confidential = ?', true)
+ .where('todos.user_id != issues.author_id')
+ .where('todos.user_id != issue_assignees.user_id')
end
# rubocop: enable CodeReuse/ActiveRecord
override :todos_to_remove?
def todos_to_remove?
- issue&.confidential?
+ issues&.any?(&:confidential?)
end
override :project_ids
def project_ids
- issue.project_id
+ issues&.distinct&.select(:project_id)
end
override :authorized_users
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index a161fbd064e..c6781e91cfd 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,10 +1,10 @@
-- page_title _("Report abuse to GitLab")
+- page_title _("Report abuse to admin")
%h3.page-title
- = _("Report abuse to GitLab")
+ = _("Report abuse to admin")
%p
- = _("Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately.")
+ = _("Please use this form to report to the admin users who create spam issues, comments or behave inappropriately.")
%p
- = _("A member of GitLab's abuse team will review your report as soon as possible.")
+ = _("A member of the abuse team will review your report as soon as possible.")
%hr
= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index f4bfb5af385..dd56bb99a06 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -8,4 +8,12 @@
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
Allow requests to the local network from hooks and services
+ .form-group
+ .form-check
+ = f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
+ = f.label :dns_rebinding_protection_enabled, class: 'form-check-label' do
+ = _('Enforce DNS rebinding attack protection')
+ %span.form-text.text-muted
+ = _('Resolves IP addresses once and uses them to submit requests')
+
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index d07cbe4589c..0b5c1a806b2 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,3 +1,3 @@
-= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
+= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index ca2521e9bc6..ed4bd5ae19e 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -8,7 +8,7 @@
- value = variable&.value
- is_protected_default = ci_variable_protected_by_default?
- is_protected = ci_variable_protected?(variable, only_key_value)
-- is_masked_default = true
+- is_masked_default = false
- is_masked = ci_variable_masked?(variable, only_key_value)
- id_input_name = "#{form_field}[variables_attributes][][id]"
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 383fd5130ce..5eba819172b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,3 +1,5 @@
+- max_name_length = 128
+- max_username_length = 255
#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
@@ -5,13 +7,13 @@
= render "devise/shared/error_messages", resource: resource
.name.form-group
= f.label :name, _('Full name'), class: 'label-bold'
- = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji", required: true, title: _("This field is required.")
+ = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.")
.username.form-group
= f.label :username, class: 'label-bold'
- = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
- %p.validation-error.hide= _('Username is already taken.')
- %p.validation-success.hide= _('Username is available.')
- %p.validation-pending.hide= _('Checking username availability...')
+ = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
+ %p.validation-error.field-validation.hide= _('Username is already taken.')
+ %p.validation-success.field-validation.hide= _('Username is available.')
+ %p.validation-pending.field-validation.hide= _('Checking username availability...')
.form-group
= f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.")
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 043cca6ad38..c38f96f302a 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -10,4 +10,6 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= footer_message
+ = render_if_exists "shared/onboarding_guide"
+
= yield :scripts_body
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index f21cf1ad34b..d3733ab3a09 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+ = _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index 5567adc9165..ff2548a4b42 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+= _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 47494fc3f06..b9d73d89334 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -18,6 +18,7 @@
.float-right
%span.key-created-at
= s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
- = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do
- %span.sr-only= _('Remove')
- = icon('trash')
+ - if key.can_delete?
+ = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do
+ %span.sr-only= _('Remove')
+ = icon('trash')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index dcdb7fc63b1..0ef01dec493 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -24,4 +24,5 @@
= @key.key
.col-md-12
.float-right
- = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
+ - if @key.can_delete?
+ = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index b5678b56ca6..28d4f8eb201 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -8,59 +8,58 @@
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', **tracking_attrs(track_label, 'click_button', 'gitlab_export') do
= icon('gitlab', text: 'GitLab export')
- if github_import_enabled?
%div
- = link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do
+ = link_to new_import_github_path, class: 'btn js-import-github', **tracking_attrs(track_label, 'click_button', 'github') do
= icon('github', text: 'GitHub')
- if bitbucket_import_enabled?
%div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do
+ **tracking_attrs(track_label, 'click_button', 'bitbucket_cloud') do
= icon('bitbucket', text: 'Bitbucket Cloud')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled?
%div
- = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
+ = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket", **tracking_attrs(track_label, 'click_button', 'bitbucket_server') do
= icon('bitbucket-square', text: 'Bitbucket Server')
%div
- if gitlab_import_enabled?
%div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do
+ **tracking_attrs(track_label, 'click_button', 'gitlab_com') do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
- if google_code_import_enabled?
%div
- = link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do
+ = link_to new_import_google_code_path, class: 'btn import_google_code', **tracking_attrs(track_label, 'click_button', 'google_code') do
= icon('google', text: 'Google Code')
- if fogbugz_import_enabled?
%div
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', **tracking_attrs(track_label, 'click_button', 'fogbugz') do
= icon('bug', text: 'Fogbugz')
- if gitea_import_enabled?
%div
- = link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do
+ = link_to new_import_gitea_path, class: 'btn import_gitea', **tracking_attrs(track_label, 'click_button', 'gitea') do
= custom_icon('gitea_logo')
Gitea
- if git_import_enabled?
%div
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
= icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
%div
- = link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do
+ = link_to new_import_manifest_path, class: 'btn import_manifest', **tracking_attrs(track_label, 'click_button', 'manifest_file') do
= icon('file-text-o', text: 'Manifest file')
- if phabricator_import_enabled?
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
index 1ab467a3710..c21d333f21a 100644
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -3,7 +3,7 @@
.form-group
%b= s_('ProjectSettings|Merge checks')
%p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged')
- .form-check.mb-2.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
+ .form-check.mb-2.builds-feature
= form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
= form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
= s_('ProjectSettings|Pipelines must succeed')
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 1c1c7d832bd..e423631ec99 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,4 +1,4 @@
-- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+- visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level))
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
- hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false)
- track_label = local_assigns.fetch(:track_label, 'blank_project')
@@ -54,7 +54,7 @@
.form-group.row.initialize-with-readme-setting
%div{ :class => "col-sm-12" }
.form-check
- = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
+ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
%strong Initialize repository with a README
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 78b416edd5c..1cee8be604a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -59,7 +59,7 @@
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
- - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
+ - if can?(current_user, :read_issue, @project) && @milestone.total_issues_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span= _('Assign some issues to this milestone.')
- elsif @milestone.complete?(current_user) && @milestone.active?
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 8de84f82e9f..8a6e5fde99b 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -11,7 +11,7 @@
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
- = _('Report abuse to GitLab')
+ = _('Report abuse to admin')
- if note_editable
%li
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index 1e50a101c1e..33f2166480b 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -1,29 +1,80 @@
- if @domain.errors.any?
- #error_explanation
- .alert.alert-danger
- - @domain.errors.full_messages.each do |msg|
- %p= msg
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ = msg
.form-group.row
.col-sm-2.col-form-label
= f.label :domain, _("Domain")
.col-sm-10
- = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
+ = f.text_field :domain, required: true, autocomplete: "off", class: "form-control", disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :certificate, _("Certificate (PEM)")
- .col-sm-10
- = f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline= _("Upload a certificate for your domain with all intermediates")
-
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :key, _("Key (PEM)")
- .col-sm-10
- = f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline= _("Upload a private key for your certificate")
+
+ - auto_ssl_available = Feature.enabled?(:pages_auto_ssl)
+ - auto_ssl_enabled = @domain.auto_ssl_enabled?
+ - auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled
+
+ - if auto_ssl_available
+ .form-group.row
+ .col-sm-2.col-form-label
+ %label{ for: "pages_domain_auto_ssl_enabled_button" }
+ - lets_encrypt_link_url = "https://letsencrypt.org/"
+ - lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url }
+ - lets_encrypt_link_end = "</a>".html_safe
+ = _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end }
+
+ .col-sm-10.js-auto-ssl-toggle-container
+ %button{ type: "button", id: "pages_domain_auto_ssl_enabled_button",
+ class: "js-project-feature-toggle project-feature-toggle mt-2 #{"is-checked" if auto_ssl_available_and_enabled}",
+ "aria-label": _("Automatic certificate management using Let's Encrypt") }
+ = f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
+ %span.toggle-icon
+ = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked")
+ = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked")
+ %p.text-secondary.mt-3
+ - docs_link_url = help_page_path("user/project/pages/lets_encrypt_for_gitlab_pages.md", anchor: "lets-encrypt-for-gitlab-pages")
+ - docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
+ - docs_link_end = "</a>".html_safe
+ = _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+
+ .js-shown-if-auto-ssl{ class: ("d-none" unless auto_ssl_available_and_enabled) }
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :certificate, _("Certificate (PEM)")
+ .col-sm-10
+ - if auto_ssl_available_and_enabled && !@domain.certificate.empty?
+ = f.text_area :certificate,
+ rows: 5,
+ class: "form-control",
+ disabled: true
+ %span.help-inline.text-muted= _("This certificate is automatically managed by Let's Encrypt")
+ - else
+ %p.text-secondary.form-control-plaintext= _("The certificate will be shown here once it has been obtained from Let's Encrypt. This process may take up to an hour to complete.")
+
+ .js-shown-unless-auto-ssl{ class: ("d-none" if auto_ssl_available_and_enabled) }
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :certificate, _("Certificate (PEM)")
+ .col-sm-10
+ = f.text_area :certificate,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ value: (@domain.certificate unless auto_ssl_available_and_enabled),
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a certificate for your domain with all intermediates")
+
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :key, _("Key (PEM)")
+ .col-sm-10
+ = f.text_area :key,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ value: (@domain.key unless auto_ssl_available_and_enabled),
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a private key for your certificate")
+
- else
.nothing-here-block
= _("Support for custom certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pages_domains/_helper_text.html.haml b/app/views/projects/pages_domains/_helper_text.html.haml
new file mode 100644
index 00000000000..5a79fefabfc
--- /dev/null
+++ b/app/views/projects/pages_domains/_helper_text.html.haml
@@ -0,0 +1,9 @@
+- docs_link_url = help_page_path("user/project/pages/getting_started_part_three.md", anchor: "adding-certificates-to-your-project")
+- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
+- docs_link_end = "</a>".html_safe
+
+-# Hiding behind a feature flag to avoid any changes to this feature's implemention
+-# when the :pages_auto_ssl feature flag is disabled. This check should be removed
+-# once the :pages_auto_ssl feature flag is removed.
+- if Feature.enabled?(:pages_auto_ssl)
+ %p= _("Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index e11387ae742..7c0777e5496 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -3,6 +3,7 @@
- page_title @domain.domain
%h3.page-title
= @domain.domain
+= render 'projects/pages_domains/helper_text'
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index c7cefa87c76..e23ccb5d4c6 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -2,6 +2,7 @@
- page_title _('New Pages Domain')
%h3.page-title
= _("New Pages Domain")
+= render 'projects/pages_domains/helper_text'
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
index f049c35b38d..a124283921d 100644
--- a/app/views/projects/settings/operations/_external_dashboard.html.haml
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -1,2 +1,3 @@
-.js-operation-settings{ data: { external_dashboard: { path: metrics_external_dashboard_url,
+.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
+ external_dashboard: { url: metrics_external_dashboard_url,
help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index e37fd7624be..065fef606d5 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,3 @@
+- full_title = markdown_field(commit, :full_title)
%span.str-truncated
- = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link'
+ = link_to_html full_title, project_commit_path(@project, commit.id), title: full_title, class: 'tree-commit-link'
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index d5327a2b4cc..dfcd1c6b19f 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -6,3 +6,5 @@
%p
= _("You are receiving this message because you are a GitLab administrator for %{url}.") % { url: Gitlab.config.gitlab.url }
+
+= render_if_exists 'repository_check_mailer/email_additional_text'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index cb8a8a24be8..12eb8d7fa81 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -21,7 +21,7 @@
.search-results
- if @scope == 'projects'
.term
- = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
+ = render 'shared/projects/list', { projects: @search_objects, pipeline_status: false }.merge(@display_options)
- else
- locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 796782035f2..1f055cdfa31 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,7 +1,7 @@
.search-result-row
%h4
= confidential_icon(issue)
- = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
+ = link_to namespace_project_issue_path(issue.project.namespace.becomes(Namespace), issue.project, issue) do
%span.term.str-truncated= issue.title
- if issue.closed?
%span.badge.badge-danger.prepend-left-5= _("Closed")
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index f0e0af11f27..074bb9bce8d 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
+ = link_to namespace_project_merge_request_path(merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request) do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
%span.badge.badge-primary.prepend-left-5= _("Merged")
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index 2daa96e34d1..3201f1a7815 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to [milestone.project.namespace.becomes(Namespace), milestone.project, milestone] do
+ = link_to namespace_project_milestone_path(milestone.project.namespace.becomes(Namespace), milestone.project, milestone) do
%span.term.str-truncated= milestone.title
- if milestone.description.present?
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index ca392e1adfc..22fcfcda297 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,6 +1,6 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
-- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+- noteable_text = show_unsubscribe_title?(noteable) ? %(#{noteable.title} (#{noteable.to_reference})) : %(#{noteable.to_reference})
- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 3ee713cf499..d0f9374e832 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,11 +1,26 @@
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+- import_url = Gitlab::UrlSanitizer.new(f.object.import_url)
-.form-group.import-url-data
- = f.label :import_url, class: 'label-bold' do
- %span
- = _('Git repository URL')
+.import-url-data
+ .form-group
+ = f.label :import_url, class: 'label-bold' do
+ %span
+ = _('Git repository URL')
+ = f.text_field :import_url, value: import_url.sanitized_url,
+ autocomplete: 'off', class: 'form-control', placeholder: 'https://gitlab.company.com/group/project.git', required: true
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
+ .row
+ .form-group.col-md-6
+ = f.label :import_url_user, class: 'label-bold' do
+ %span
+ = _('Username (optional)')
+ = f.text_field :import_url_user, value: import_url.user, class: 'form-control', required: false, autocomplete: 'new-password'
+
+ .form-group.col-md-6
+ = f.label :import_url_password, class: 'label-bold' do
+ %span
+ = _('Password (optional)')
+ = f.password_field :import_url_password, class: 'form-control', required: false, autocomplete: 'new-password'
.info-well.prepend-top-20
.well-segment
@@ -13,7 +28,7 @@
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
+ = _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
= import_will_timeout_message(ci_cd_only)
%li
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 2c46efdc124..3a5adb34ad1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -158,7 +158,7 @@
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
data: { toggle: 'dropdown', display: 'static' } }
= _('Move issue')
- .dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
= dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
= dropdown_content
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 9ec76d82d18..e83ca5eaab8 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -2,9 +2,12 @@
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
- dom_id = "group_member_#{group_link.id}"
-%li.member.group_member{ id: dom_id }
- %span.list-item-name
- = group_icon(group, class: "avatar s40", alt: '')
+
+-# Note this is just for groups. For individual members please see shared/members/_member
+
+%li.member.group_member.py-2.px-3.d-flex.flex-column.flex-md-row{ id: dom_id }
+ %span.list-item-name.mb-2.m-md-0
+ = group_icon(group, class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '')
.user-info
= link_to group.full_name, group_path(group), class: 'member'
.cgray
@@ -13,10 +16,10 @@
ยท
%span{ class: ('text-warning' if group_link.expires_soon?) }
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) }
- .controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
+ .controls.member-controls.align-items-center
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
- .member-form-control.dropdown.append-right-5
+ .member-form-control.dropdown.mr-sm-2.d-sm-inline-block
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: !can_admin_member,
data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
@@ -32,14 +35,14 @@
= link_to role, "javascript:void(0)",
class: ("is-active" if group_link.group_access == role_id),
data: { id: role_id, el_id: dom_id }
- .prepend-left-5.clearable-input.member-form-control
+ .clearable-input.member-form-control.d-sm-inline-block
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to project_group_link_path(@project, group_link),
method: :delete,
data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } },
- class: 'btn btn-remove prepend-left-10' do
+ class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
%span.d-block.d-sm-none
= _("Delete")
= icon('trash', class: 'd-none d-sm-block')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2db1f67a793..331283f7eec 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -4,11 +4,14 @@
- member = local_assigns.fetch(:member)
- user = local_assigns.fetch(:user, member.user)
- source = member.source
+- override = member.try(:override)
-%li.member{ class: dom_class(member), id: dom_id(member) }
- %span.list-item-name
+-# Note this is just for individual members. For groups please see shared/members/_group
+
+%li.member.py-2.px-3.d-flex.flex-column{ class: [dom_class(member), ("is-overridden" if override), ("flex-md-row" unless force_mobile_view)], id: dom_id(member) }
+ %span.list-item-name.mb-2.m-md-0
- if user
- = image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
+ = image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
= link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
= user_status(user)
@@ -42,7 +45,7 @@
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) }
- else
- = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40", alt: ''
+ = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
.member= member.invite_email
.cgray
@@ -53,20 +56,22 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
- .controls.member-controls
+ .controls.member-controls.align-items-center
+ = render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override?
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
method: :post,
- class: 'btn btn-default prepend-left-10 d-none d-sm-block',
+ class: 'btn btn-default align-self-center mr-sm-2',
title: _('Resend invite')
- if user != current_user && member.can_update?
- = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f|
+ = form_for member, remote: true, html: { class: "js-edit-member-form form-group #{'d-sm-flex' unless force_mobile_view}" } do |f|
= f.hidden_field :access_level
- .member-form-control.dropdown.append-right-5
+ .member-form-control.dropdown{ class: [("mr-sm-2 d-sm-inline-block" unless force_mobile_view)] }
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: member.can_override? && !override,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text
= member.human_access
@@ -80,20 +85,25 @@
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
- .prepend-left-5.clearable-input.member-form-control
+ = render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
+ group: @group,
+ member: member,
+ can_override: member.can_override?
+ .clearable-input.member-form-control{ class: [("d-sm-inline-block" unless force_mobile_view)] }
= f.text_field :expires_at,
+ disabled: member.can_override? && !override,
class: 'form-control js-access-expiration-date js-member-update-control',
placeholder: _('Expiration date'),
id: "member_expires_at_#{member.id}",
data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
- %span.member-access-text= member.human_access
+ %span.member-access-text.user-access-role= member.human_access
- if member.can_approve?
= link_to polymorphic_path([:approve_access_request, member]),
method: :post,
- class: 'btn btn-success prepend-left-10',
+ class: "btn btn-success align-self-center m-0 mb-2 #{'mb-sm-0 ml-sm-2' unless force_mobile_view}",
title: _('Grant access') do
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _('Grant access')
@@ -105,16 +115,19 @@
= link_to icon('sign-out', text: _('Leave')), polymorphic_path([:leave, member.source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
- class: 'btn btn-remove prepend-left-10'
+ class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
- else
= link_to member,
method: :delete,
data: { confirm: remove_member_message(member) },
- class: 'btn btn-remove prepend-left-10',
+ class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
title: remove_member_title(member) do
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _("Delete")
- unless force_mobile_view
= icon('trash', class: 'd-none d-sm-block')
+ = render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
- %span.member-access-text= member.human_access
+ %span.member-access-text.user-access-role= member.human_access
+
+= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e4e85de93da..fd0cc5fb24e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1,6 +1,8 @@
---
- auto_devops:auto_devops_disable
+- auto_merge:auto_merge_process
+
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
new file mode 100644
index 00000000000..cd81cdbc60c
--- /dev/null
+++ b/app/workers/auto_merge_process_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AutoMergeProcessWorker
+ include ApplicationWorker
+
+ queue_namespace :auto_merge
+
+ def perform(merge_request_id)
+ MergeRequest.find_by_id(merge_request_id).try do |merge_request|
+ AutoMergeService.new(merge_request.project, merge_request.merge_user)
+ .process(merge_request)
+ end
+ end
+end
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index 92d62a15aee..60703c83e9e 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -5,6 +5,8 @@ class PagesDomainVerificationCronWorker
include CronjobQueue
def perform
+ return if Gitlab::Database.read_only?
+
PagesDomain.needs_verification.find_each do |domain|
PagesDomainVerificationWorker.perform_async(domain.id)
end
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index b3319ff5a13..7817b2ee5fc 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -5,6 +5,8 @@ class PagesDomainVerificationWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(domain_id)
+ return if Gitlab::Database.read_only?
+
domain = PagesDomain.find_by(id: domain_id)
return unless domain
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 8a9ee7808e4..9410fd1a786 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,47 +3,12 @@
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
- include ::Gitlab::ExclusiveLeaseHelpers
- EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock'
- LOCK_TIMEOUT = 50.minutes
-
- # rubocop: disable CodeReuse/ActiveRecord
def perform
- in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
- Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
- .preload(:owner, :project).find_each do |schedule|
-
- schedule.schedule_next_run!
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
- rescue => e
- error(schedule, e)
+ Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
+ schedules.each do |schedule|
+ Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def error(schedule, error)
- failed_creation_counter.increment
-
- Rails.logger.error "Failed to create a scheduled pipeline. " \
- "schedule_id: #{schedule.id} message: #{error.message}"
-
- Gitlab::Sentry
- .track_exception(error,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: schedule.id })
- end
-
- def failed_creation_counter
- @failed_creation_counter ||=
- Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
- "Counter of failed attempts of pipeline schedule creation")
- end
end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index ce6c88c85c1..666331e6cd4 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -6,14 +6,7 @@ class PipelineSuccessWorker
queue_namespace :pipeline_processing
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
- pipeline.all_merge_requests.preload(:merge_user).each do |merge_request|
- AutoMergeService.new(pipeline.project, merge_request.merge_user)
- .process(merge_request)
- end
- end
+ # no-op
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index f72331c003a..43e0b9db22f 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -21,6 +21,30 @@ class RunPipelineScheduleWorker
Ci::CreatePipelineService.new(schedule.project,
user,
ref: schedule.ref)
- .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ rescue Ci::CreatePipelineService::CreateError
+ # no-op. This is a user operation error such as corrupted .gitlab-ci.yml.
+ rescue => e
+ error(schedule, e)
+ end
+
+ private
+
+ def error(schedule, error)
+ failed_creation_counter.increment
+
+ Rails.logger.error "Failed to create a scheduled pipeline. " \
+ "schedule_id: #{schedule.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: schedule.id })
+ end
+
+ def failed_creation_counter
+ @failed_creation_counter ||=
+ Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
+ "Counter of failed attempts of pipeline schedule creation")
end
end
diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb
index 481fde8c83d..240a5f98ad5 100644
--- a/app/workers/todos_destroyer/confidential_issue_worker.rb
+++ b/app/workers/todos_destroyer/confidential_issue_worker.rb
@@ -5,8 +5,8 @@ module TodosDestroyer
include ApplicationWorker
include TodosDestroyerQueue
- def perform(issue_id)
- ::Todos::Destroy::ConfidentialIssueService.new(issue_id).execute
+ def perform(issue_id = nil, project_id = nil)
+ ::Todos::Destroy::ConfidentialIssueService.new(issue_id: issue_id, project_id: project_id).execute
end
end
end