Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/boards/stores/actions.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/contextual_sidebar.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue59
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue10
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js16
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js7
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js16
-rw-r--r--app/assets/javascripts/filtered_search/constants.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js2
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js11
-rw-r--r--app/assets/javascripts/ide/components/ide.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue16
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue23
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue52
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue53
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue11
-rw-r--r--app/assets/javascripts/lib/utils/autosave.js1
-rw-r--r--app/assets/javascripts/lib/utils/invalid_url.js6
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue53
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js94
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js53
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js16
-rw-r--r--app/assets/javascripts/new_branch_form.js2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue9
-rw-r--r--app/assets/javascripts/operation_settings/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js15
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue2
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue3
-rw-r--r--app/assets/javascripts/repository/graphql.js2
-rw-r--r--app/assets/javascripts/repository/utils/title.js1
-rw-r--r--app/assets/javascripts/search_autocomplete.js2
-rw-r--r--app/assets/javascripts/users_select.js1
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js132
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/constants.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js23
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js52
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js27
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js42
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js82
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js15
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/events.js36
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/index.js5
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js77
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/utils.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue)16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue47
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue (renamed from app/assets/javascripts/vue_shared/components/table_pagination.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue29
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss5
-rw-r--r--app/assets/stylesheets/components/popover.scss95
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss7
-rw-r--r--app/assets/stylesheets/framework/forms.scss16
-rw-r--r--app/assets/stylesheets/framework/highlight.scss6
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss15
-rw-r--r--app/assets/stylesheets/pages/builds.scss4
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss3
-rw-r--r--app/assets/stylesheets/pages/milestone.scss21
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss1
-rw-r--r--app/controllers/admin/application_settings_controller.rb6
-rw-r--r--app/controllers/concerns/boards_actions.rb8
-rw-r--r--app/controllers/concerns/boards_responses.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/registry/tags_controller.rb2
-rw-r--r--app/controllers/projects/settings/operations_controller.rb4
-rw-r--r--app/controllers/search_controller.rb1
-rw-r--r--app/graphql/gitlab_schema.rb4
-rw-r--r--app/graphql/types/issue_type.rb4
-rw-r--r--app/graphql/types/merge_request_type.rb4
-rw-r--r--app/graphql/types/notes/diff_position_type.rb46
-rw-r--r--app/graphql/types/notes/discussion_type.rb15
-rw-r--r--app/graphql/types/notes/note_type.rb46
-rw-r--r--app/graphql/types/notes/noteable_type.rb25
-rw-r--r--app/graphql/types/notes/position_type_enum.rb13
-rw-r--r--app/graphql/types/permission_types/note.rb11
-rw-r--r--app/graphql/types/project_statistics_type.rb2
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/graphql/types/task_completion_status.rb11
-rw-r--r--app/graphql/types/tree/blob_type.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/helpers/markup_helper.rb5
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/mailers/repository_check_mailer.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/diff_positionable_note.rb9
-rw-r--r--app/models/discussion.rb8
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/lfs_objects_project.rb8
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_recipient.rb5
-rw-r--r--app/models/project.rb22
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_services/data_fields.rb10
-rw-r--r--app/models/project_services/issue_tracker_data.rb25
-rw-r--r--app/models/project_services/jira_tracker_data.rb30
-rw-r--r--app/models/service.rb5
-rw-r--r--app/models/user_callout_enums.rb6
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/global_policy.rb4
-rw-r--r--app/policies/project_policy.rb5
-rw-r--r--app/policies/project_statistics_policy.rb5
-rw-r--r--app/presenters/ci/build_runner_presenter.rb2
-rw-r--r--app/serializers/board_serializer.rb5
-rw-r--r--app/serializers/board_simple_entity.rb5
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/services/auto_merge/base_service.rb8
-rw-r--r--app/services/auto_merge_service.rb6
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/files/multi_service.rb2
-rw-r--r--app/services/git/branch_hooks_service.rb6
-rw-r--r--app/services/lfs/file_transformer.rb16
-rw-r--r--app/services/projects/fork_service.rb6
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml1
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml1
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml4
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_deprecation_message.html.haml1
177 files changed, 1846 insertions, 531 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 743f11625bc..aaab217964c 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this */
+/* eslint-disable class-methods-use-this, @gitlab/i18n/no-non-i18n-strings */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index da82b52330a..d4f4df3ad75 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,4 +1,5 @@
const notImplemented = () => {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
throw new Error('Not implemented!');
};
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 77ba68be07e..09eb8bb9b98 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,6 +1,7 @@
import * as mutationTypes from './mutation_types';
const notImplemented = () => {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
throw new Error('Not implemented!');
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 3e01841d563..4890f99e9d1 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -2,7 +2,7 @@
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
-import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index b62ec8a651b..9263e9b27e4 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -78,7 +78,7 @@ export default class ContextualSidebar {
const dbp = ContextualSidebar.isDesktopBreakpoint();
if (this.$sidebar.length) {
- this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
+ this.$sidebar.toggleClass(`sidebar-collapsed-desktop ${SIDEBAR_COLLAPSED_CLASS}`, collapsed);
this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
this.$page.toggleClass(
'page-with-icon-sidebar',
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue
deleted file mode 100644
index 6c256fa6736..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import iconCommit from '../svg/icon_commit.svg';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- userAvatarImage,
- totalTime,
- limitWarning,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- },
- stage: {
- type: Object,
- default: () => ({}),
- },
- },
- computed: {
- iconCommit() {
- return iconCommit;
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(commit, i) in items" :key="i" class="stage-event-item">
- <div class="item-details item-conmmit-component">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="commit.author.avatarUrl" />
- <h5 class="item-title commit-title">
- <a :href="commit.commitUrl"> {{ commit.title }} </a>
- </h5>
- <span>
- {{ s__('FirstPushedBy|First') }} <span class="commit-icon" v-html="iconCommit"> </span>
- <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{
- commit.shortSha
- }}</a>
- {{ s__('FirstPushedBy|pushed by') }}
- <a :href="commit.author.webUrl" class="commit-author-link">
- {{ commit.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time"><total-time :time="commit.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 3f0a9f2602c..b56e08175cc 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -5,7 +5,6 @@ import Flash from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
-import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
@@ -26,7 +25,7 @@ export default () => {
components: {
banner,
'stage-issue-component': stageComponent,
- 'stage-plan-component': stagePlanComponent,
+ 'stage-plan-component': stageComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 11d6672cacf..81da0754752 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -69,6 +69,16 @@ export default {
required: false,
default: false,
},
+ dismissEndpoint: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const treeWidth =
@@ -141,7 +151,12 @@ export default {
showTreeList: 'adjustView',
},
mounted() {
- this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
+ this.setBaseConfig({
+ endpoint: this.endpoint,
+ projectPath: this.projectPath,
+ dismissEndpoint: this.dismissEndpoint,
+ showSuggestPopover: this.showSuggestPopover,
+ });
if (this.shouldShow) {
this.fetchData();
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index c209b857652..da0cdbe467b 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -42,6 +42,7 @@ export default {
noteableData: state => state.notes.noteableData,
diffViewType: state => state.diffs.diffViewType,
}),
+ ...mapState('diffs', ['showSuggestPopover']),
...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters([
'isLoggedIn',
@@ -80,7 +81,12 @@ export default {
}
},
methods: {
- ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']),
+ ...mapActions('diffs', [
+ 'cancelCommentForm',
+ 'assignDiscussionsToDiff',
+ 'saveDiffDiscussion',
+ 'setSuggestPopoverDismissed',
+ ]),
handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
@@ -125,11 +131,13 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:diff-file="diffFile"
+ :show-suggest-popover="showSuggestPopover"
save-button-title="Comment"
class="diff-comment-form"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
+ @handleSuggestDismissed="setSuggestPopoverDismissed"
/>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 1d897bca1dd..1e57e9b8a30 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -72,6 +72,8 @@ export default function initDiffsApp(store) {
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
isFluidLayout: parseBoolean(dataset.isFluidLayout),
+ dismissEndpoint: dataset.dismissEndpoint,
+ showSuggestPopover: parseBoolean(dataset.showSuggestPopover),
};
},
computed: {
@@ -99,6 +101,8 @@ export default function initDiffsApp(store) {
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
isFluidLayout: this.isFluidLayout,
+ dismissEndpoint: this.dismissEndpoint,
+ showSuggestPopover: this.showSuggestPopover,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 479afc50113..88d7b4bba63 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -36,8 +36,8 @@ import {
import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => {
- const { endpoint, projectPath } = options;
- commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
+ const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
+ commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
};
export const fetchDiffFiles = ({ state, commit }) => {
@@ -455,5 +455,17 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
+export const setSuggestPopoverDismissed = ({ commit, state }) =>
+ axios
+ .post(state.dismissEndpoint, {
+ feature_name: 'suggest_popover_dismissed',
+ })
+ .then(() => {
+ commit(types.SET_SHOW_SUGGEST_POPOVER);
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.'));
+ });
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index cf4dd93dbfb..6821c8445ea 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -28,4 +28,6 @@ export default () => ({
renderTreeList: true,
showWhitespace: true,
fileFinderVisible: false,
+ dismissEndpoint: '',
+ showSuggestPopover: true,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6bb24c97139..8d6111da500 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -33,3 +33,5 @@ export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES'
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
+
+export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 67bc1724738..00181a63c43 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -11,8 +11,8 @@ import * as types from './mutation_types';
export default {
[types.SET_BASE_CONFIG](state, options) {
- const { endpoint, projectPath } = options;
- Object.assign(state, { endpoint, projectPath });
+ const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
+ Object.assign(state, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
},
[types.SET_LOADING](state, isLoading) {
@@ -302,4 +302,7 @@ export default {
file.renderingLines = !file.renderingLines;
},
+ [types.SET_SHOW_SUGGEST_POPOVER](state) {
+ state.showSuggestPopover = false;
+ },
};
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index be80661223c..f8a637138ad 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import TablePagination from '~/vue_shared/components/table_pagination.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import containerMixin from 'ee_else_ce/environments/mixins/container_mixin';
import EnvironmentTable from '../components/environments_table.vue';
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index a5812b173dc..31347d95a25 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -11,7 +11,7 @@ import Flash from '../../flash';
import eventHub from '../event_hub';
import EnvironmentsService from '../services/environments_service';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import container from '../components/container.vue';
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index be867a3838d..891086b4142 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -8,9 +8,19 @@ import DropdownUtils from './dropdown_utils';
import { mergeUrlParams } from '../lib/utils/url_utility';
export default class AvailableDropdownMappings {
- constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) {
+ constructor(
+ container,
+ baseEndpoint,
+ labelsEndpoint,
+ milestonesEndpoint,
+ groupsOnly,
+ includeAncestorGroups,
+ includeDescendantGroups,
+ ) {
this.container = container;
this.baseEndpoint = baseEndpoint;
+ this.labelsEndpoint = labelsEndpoint;
+ this.milestonesEndpoint = milestonesEndpoint;
this.groupsOnly = groupsOnly;
this.includeAncestorGroups = includeAncestorGroups;
this.includeDescendantGroups = includeDescendantGroups;
@@ -117,11 +127,11 @@ export default class AvailableDropdownMappings {
}
getMilestoneEndpoint() {
- return `${this.baseEndpoint}/milestones.json`;
+ return `${this.milestonesEndpoint}.json`;
}
getLabelsEndpoint() {
- let endpoint = `${this.baseEndpoint}/labels.json?`;
+ let endpoint = `${this.labelsEndpoint}.json?`;
if (this.groupsOnly) {
endpoint = `${endpoint}only_group_labels=true&`;
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
new file mode 100644
index 00000000000..8ca0e94cfeb
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -0,0 +1,2 @@
+/* eslint-disable import/prefer-default-export */
+export const TOKEN_TYPES = ['author', 'assignee'];
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cb0a84b490b..1cbfd7f9bb9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -9,6 +9,8 @@ import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
constructor({
baseEndpoint = '',
+ labelsEndpoint = '',
+ milestonesEndpoint = '',
tokenizer,
page,
isGroup,
@@ -18,6 +20,8 @@ export default class FilteredSearchDropdownManager {
}) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.labelsEndpoint = labelsEndpoint.replace(/\/$/, '');
+ this.milestonesEndpoint = milestonesEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
@@ -48,6 +52,8 @@ export default class FilteredSearchDropdownManager {
const availableMappings = new AvailableDropdownMappings(
this.container,
this.baseEndpoint,
+ this.labelsEndpoint,
+ this.milestonesEndpoint,
this.groupsOnly,
this.includeAncestorGroups,
this.includeDescendantGroups,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 78fbb3696cc..450e0725f2e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -86,6 +86,8 @@ export default class FilteredSearchManager {
this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager({
baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ labelsEndpoint: this.filteredSearchInput.getAttribute('data-labels-endpoint') || '',
+ milestonesEndpoint: this.filteredSearchInput.getAttribute('data-milestones-endpoint') || '',
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 315cd6f64da..7f6457242ef 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,4 +1,4 @@
-import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value';
+import VisualTokenValue from './visual_token_value';
import { objectToQueryString } from '~/lib/utils/common_utils';
import FilteredSearchContainer from './container';
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 38327472cb3..018207541b3 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -6,6 +6,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
+import { TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants';
export default class VisualTokenValue {
constructor(tokenValue, tokenType) {
@@ -22,7 +23,7 @@ export default class VisualTokenValue {
if (tokenType === 'label') {
this.updateLabelTokenColor(tokenValueContainer);
- } else if (tokenType === 'author' || tokenType === 'assignee') {
+ } else if (TOKEN_TYPES.includes(tokenType)) {
this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement);
} else if (tokenType === 'my-reaction') {
this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement);
@@ -56,13 +57,13 @@ export default class VisualTokenValue {
updateLabelTokenColor(tokenValueContainer) {
const { tokenValue } = this;
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const { baseEndpoint } = filteredSearchInput.dataset;
- const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
- `${baseEndpoint}/labels.json`,
+ const { labelsEndpoint } = filteredSearchInput.dataset;
+ const labelsEndpointWithParams = FilteredSearchVisualTokens.getEndpointWithQueryParams(
+ `${labelsEndpoint}.json`,
filteredSearchInput.dataset.endpointQueryParams,
);
- return AjaxCache.retrieve(labelsEndpoint)
+ return AjaxCache.retrieve(labelsEndpointWithParams)
.then(labels => {
const matchingLabel = (labels || []).find(
label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index e41b1530226..363a8f43033 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -146,7 +146,7 @@ export default {
</div>
<component :is="rightPaneComponent" v-if="currentProjectId" />
</div>
- <ide-status-bar :file="activeFile" />
+ <ide-status-bar />
<new-modal />
</article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index ce577ae85b0..206b8341aad 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import IdeStatusList from 'ee_else_ce/ide/components/ide_status_list.vue';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
@@ -12,18 +13,12 @@ export default {
icon,
userAvatarImage,
CiIcon,
+ IdeStatusList,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
- props: {
- file: {
- type: Object,
- required: false,
- default: null,
- },
- },
data() {
return {
lastCommitFormatedAge: null,
@@ -125,11 +120,6 @@ export default {
>{{ lastCommitFormatedAge }}</time
>
</div>
- <div v-if="file" class="ide-status-file">{{ file.name }}</div>
- <div v-if="file" class="ide-status-file">{{ file.eol }}</div>
- <div v-if="file && !file.binary" class="ide-status-file">
- {{ file.editorRow }}:{{ file.editorColumn }}
- </div>
- <div v-if="file" class="ide-status-file">{{ file.fileLanguage }}</div>
+ <ide-status-list class="ml-auto" />
</footer>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
new file mode 100644
index 00000000000..364e3f081a1
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -0,0 +1,23 @@
+<script>
+import { mapGetters } from 'vuex';
+
+export default {
+ computed: {
+ ...mapGetters(['activeFile']),
+ },
+};
+</script>
+
+<template>
+ <div class="ide-status-list d-flex">
+ <template v-if="activeFile">
+ <div class="ide-status-file">{{ activeFile.name }}</div>
+ <div class="ide-status-file">{{ activeFile.eol }}</div>
+ <div v-if="!activeFile.binary" class="ide-status-file">
+ {{ activeFile.editorRow }}:{{ activeFile.editorColumn }}
+ </div>
+ <div class="ide-status-file">{{ activeFile.fileLanguage }}</div>
+ </template>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e88ca4747c5..de2a9664cde 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -11,6 +11,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
+import PinnedLinks from './pinned_links.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
@@ -19,6 +20,7 @@ export default {
titleComponent,
editedComponent,
formComponent,
+ PinnedLinks,
},
mixins: [recaptchaModalImplementor],
props: {
@@ -340,6 +342,7 @@ export default {
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
+ <pinned-links :description-html="state.descriptionHtml" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
new file mode 100644
index 00000000000..7a54b26bc2b
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlLink,
+ },
+ props: {
+ descriptionHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ linksInDescription() {
+ const el = document.createElement('div');
+ el.innerHTML = this.descriptionHtml;
+ return [...el.querySelectorAll('a')].map(a => a.href);
+ },
+ // Detect links matching the following formats:
+ // Zoom Start links: https://zoom.us/s/<meeting-id>
+ // Zoom Join links: https://zoom.us/j/<meeting-id>
+ // Personal Zoom links: https://zoom.us/my/<meeting-id>
+ // Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my)
+ zoomHref() {
+ const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/;
+ return this.linksInDescription.reduce((acc, currentLink) => {
+ let lastLink = acc;
+ if (zoomRegex.test(currentLink)) {
+ lastLink = currentLink;
+ }
+ return lastLink;
+ }, '');
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="zoomHref" class="border-bottom mb-3 mt-n2">
+ <gl-link
+ :href="zoomHref"
+ target="_blank"
+ class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
+ >
+ <icon name="brand-zoom" :size="14" />
+ <strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index 92e20e92d66..d611b370ab9 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -17,10 +17,19 @@ export default {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
- this.$nextTick(() => this.handleScrollDown());
+ this.$nextTick(() => {
+ this.handleScrollDown();
+ this.handleCollapsibleRows();
+ });
},
mounted() {
- this.$nextTick(() => this.handleScrollDown());
+ this.$nextTick(() => {
+ this.handleScrollDown();
+ this.handleCollapsibleRows();
+ });
+ },
+ destroyed() {
+ this.removeEventListener();
},
methods: {
...mapActions(['scrollBottom']),
@@ -38,21 +47,45 @@ export default {
}, 0);
}
},
+ removeEventListener() {
+ this.$el
+ .querySelectorAll('.js-section-start')
+ .forEach(el => el.removeEventListener('click', this.handleSectionClick));
+ },
+ /**
+ * The collapsible rows are sent in HTML from the backend
+ * We need tos add a onclick handler for the divs that match `.js-section-start`
+ *
+ */
+ handleCollapsibleRows() {
+ this.$el
+ .querySelectorAll('.js-section-start')
+ .forEach(el => el.addEventListener('click', this.handleSectionClick));
+ },
+ /**
+ * On click, we toggle the hidden class of
+ * all the rows that match the `data-section` selector
+ */
+ handleSectionClick(evt) {
+ const clickedArrow = evt.currentTarget;
+ // toggle the arrow class
+ clickedArrow.classList.toggle('fa-caret-right');
+ clickedArrow.classList.toggle('fa-caret-down');
+
+ const { section } = clickedArrow.dataset;
+ const sibilings = this.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`);
+
+ sibilings.forEach(row => row.classList.toggle('hidden'));
+ },
},
};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
- <code
- class="bash"
- v-html="trace"
- >
+ <code class="bash" v-html="trace">
</code>
- <div
- v-if="!isComplete"
- class="js-log-animation build-loader-animation"
- >
+ <div v-if="!isComplete" class="js-log-animation build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index cb073a9b04d..6e92b599b0a 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -2,7 +2,6 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -10,7 +9,6 @@ export default {
CiIcon,
Icon,
GlLink,
- PipelineLink,
},
props: {
pipeline: {
@@ -50,12 +48,9 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span>
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="js-pipeline-path link-commit qa-pipeline-path"
- />
+ <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
+ >#{{ pipeline.id }}</gl-link
+ >
<template v-if="hasRef">
{{ s__('Job|for') }}
diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js
index 023c336db02..37896626053 100644
--- a/app/assets/javascripts/lib/utils/autosave.js
+++ b/app/assets/javascripts/lib/utils/autosave.js
@@ -29,4 +29,5 @@ export const updateDraft = (autosaveKey, text) => {
};
export const getDiscussionReplyKey = (noteableType, discussionId) =>
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/');
diff --git a/app/assets/javascripts/lib/utils/invalid_url.js b/app/assets/javascripts/lib/utils/invalid_url.js
new file mode 100644
index 00000000000..481bd059fc9
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/invalid_url.js
@@ -0,0 +1,6 @@
+/**
+ * Invalid URL that ensures we don't make a network request
+ * Can be used as a default value for URLs. Using an empty
+ * string can still result in request being made to the current page
+ */
+export default 'https://invalid';
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index d93873e0214..e7f6255e5f1 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -12,6 +12,7 @@ function notificationGranted(message, opts, onclick) {
}
function notifyPermissions() {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
if ('Notification' in window) {
return Notification.requestPermission();
}
@@ -24,6 +25,7 @@ function notifyMe(message, body, icon, onclick) {
icon: icon,
};
// Let's check if the browser supports notifications
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
if (!('Notification' in window)) {
// do nothing
} else if (Notification.permission === 'granted') {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e5cf43e8289..b6868e63716 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -147,14 +147,14 @@ export default class MergeRequestTabs {
e.stopImmediatePropagation();
e.preventDefault();
- const { action } = e.currentTarget.dataset;
+ const { action } = e.currentTarget.dataset || {};
- if (action) {
- const href = e.currentTarget.getAttribute('href');
- this.tabShown(action, href);
- } else if (isMetaClick(e)) {
+ if (isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
window.open(targetLink, '_blank');
+ } else if (action) {
+ const href = e.currentTarget.getAttribute('href');
+ this.tabShown(action, href);
}
}
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index c43791f2426..9de4e96e4da 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,7 +1,7 @@
<script>
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
-import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, graphTypes, lineTypes } from '../../constants';
@@ -111,7 +111,7 @@ export default {
yAxis: {
name: this.yAxisLabel,
axisLabel: {
- formatter: value => value.toFixed(3),
+ formatter: num => roundOffFloat(num, 3).toString(),
},
},
series: this.scatterSeries,
@@ -227,6 +227,7 @@ export default {
[this.primaryColor] = chart.getOption().color;
},
onResize() {
+ if (!this.$refs.areaChart) return;
const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
this.width = width;
},
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2314f7b80cf..0a652329dfe 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,18 +1,12 @@
<script>
-import {
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlModalDirective,
- GlLink,
-} from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee';
import { getParameterValues } from '~/lib/utils/url_utility';
+import invalidUrl from '~/lib/utils/invalid_url';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
@@ -31,7 +25,6 @@ export default {
GlButton,
GlDropdown,
GlDropdownItem,
- GlLink,
GlModal,
},
directives: {
@@ -77,7 +70,7 @@ export default {
type: String,
required: true,
},
- deploymentEndpoint: {
+ deploymentsEndpoint: {
type: String,
required: false,
default: null,
@@ -119,6 +112,11 @@ export default {
type: String,
required: true,
},
+ dashboardEndpoint: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
},
data() {
return {
@@ -139,13 +137,19 @@ export default {
'showEmptyState',
'environments',
'deploymentData',
+ 'metricsWithData',
+ 'useDashboardEndpoint',
]),
+ groupsWithData() {
+ return this.groups.filter(group => this.chartsWithData(group.metrics).length > 0);
+ },
},
created() {
this.setEndpoints({
metricsEndpoint: this.metricsEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
- deploymentsEndpoint: this.deploymentEndpoint,
+ deploymentsEndpoint: this.deploymentsEndpoint,
+ dashboardEndpoint: this.dashboardEndpoint,
});
this.timeWindows = timeWindows;
@@ -183,7 +187,16 @@ export default {
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
+ 'setDashboardEnabled',
]),
+ chartsWithData(charts) {
+ if (!this.useDashboardEndpoint) {
+ return charts;
+ }
+ return charts.filter(chart =>
+ chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
+ );
+ },
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
@@ -255,7 +268,9 @@ export default {
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
- ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
+ :href="setTimeWindowParameter(key)"
+ active-class="active"
+ >{{ value }}</gl-dropdown-item
>
</gl-dropdown>
</div>
@@ -265,9 +280,8 @@ export default {
<gl-button
v-gl-modal-directive="$options.addMetric.modalId"
class="js-add-metric-button text-success border-success"
+ >{{ $options.addMetric.title }}</gl-button
>
- {{ $options.addMetric.title }}
- </gl-button>
<gl-modal
ref="addMetricModal"
:modal-id="$options.addMetric.modalId"
@@ -281,16 +295,13 @@ export default {
/>
</form>
<div slot="modal-footer">
- <gl-button @click="hideAddMetricModal">
- {{ __('Cancel') }}
- </gl-button>
+ <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
<gl-button
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
+ >{{ __('Save changes') }}</gl-button
>
- {{ __('Save changes') }}
- </gl-button>
</div>
</gl-modal>
</div>
@@ -307,13 +318,13 @@ export default {
</div>
</div>
<graph-group
- v-for="(groupData, index) in groups"
+ v-for="(groupData, index) in groupsWithData"
:key="index"
:name="groupData.group"
:show-panels="showPanels"
>
<monitor-area-chart
- v-for="(graphData, graphIndex) in groupData.metrics"
+ v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
:deployment-data="deploymentData"
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 62c0f44c1e6..1d33537b3b2 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -7,6 +7,11 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
+ store.dispatch(
+ 'monitoringDashboard/setDashboardEnabled',
+ gon.features.environmentMetricsUsePrometheusEndpoint,
+ );
+
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 63c23e8449d..f41e215cb5d 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -35,6 +35,21 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
+export const setDashboardEnabled = ({ commit }, enabled) => {
+ commit(types.SET_DASHBOARD_ENABLED, enabled);
+};
+
+export const requestMetricsDashboard = ({ commit }) => {
+ commit(types.REQUEST_METRICS_DATA);
+};
+export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
+ commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
+ dispatch('fetchPrometheusMetrics', params);
+};
+export const receiveMetricsDashboardFailure = ({ commit }, error) => {
+ commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
+};
+
export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA);
export const receiveMetricsDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_METRICS_DATA_SUCCESS, data);
@@ -56,6 +71,10 @@ export const fetchData = ({ dispatch }, params) => {
};
export const fetchMetricsData = ({ state, dispatch }, params) => {
+ if (state.useDashboardEndpoint) {
+ return dispatch('fetchDashboard', params);
+ }
+
dispatch('requestMetricsData');
return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
@@ -73,11 +92,82 @@ export const fetchMetricsData = ({ state, dispatch }, params) => {
});
};
+export const fetchDashboard = ({ state, dispatch }, params) => {
+ dispatch('requestMetricsDashboard');
+
+ return axios
+ .get(state.dashboardEndpoint, { params })
+ .then(resp => resp.data)
+ .then(response => {
+ dispatch('receiveMetricsDashboardSuccess', { response, params });
+ })
+ .catch(error => {
+ dispatch('receiveMetricsDashboardFailure', error);
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ });
+};
+
+function fetchPrometheusResult(prometheusEndpoint, params) {
+ return backOffRequest(() => axios.get(prometheusEndpoint, { params }))
+ .then(res => res.data)
+ .then(response => {
+ if (response.status === 'error') {
+ throw new Error(response.error);
+ }
+
+ return response.data.result;
+ });
+}
+
+/**
+ * Returns list of metrics in data.result
+ * {"status":"success", "data":{"resultType":"matrix","result":[]}}
+ *
+ * @param {metric} metric
+ */
+export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
+ const { start, end } = params;
+ const timeDiff = end - start;
+
+ const minStep = 60;
+ const queryDataPoints = 600;
+ const step = Math.max(minStep, Math.ceil(timeDiff / queryDataPoints));
+
+ const queryParams = {
+ start,
+ end,
+ step,
+ };
+
+ return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams).then(result => {
+ commit(types.SET_QUERY_RESULT, { metricId: metric.metric_id, result });
+ });
+};
+
+export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
+ commit(types.REQUEST_METRICS_DATA);
+
+ const promises = [];
+ state.groups.forEach(group => {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ promises.push(dispatch('fetchPrometheusMetric', { metric, params }));
+ });
+ });
+ });
+
+ return Promise.all(promises).then(() => {
+ if (state.metricsWithData.length === 0) {
+ commit(types.SET_NO_DATA_EMPTY_STATE);
+ }
+ });
+};
+
export const fetchDeploymentsData = ({ state, dispatch }) => {
- if (!state.deploymentEndpoint) {
+ if (!state.deploymentsEndpoint) {
return Promise.resolve([]);
}
- return backOffRequest(() => axios.get(state.deploymentEndpoint))
+ return backOffRequest(() => axios.get(state.deploymentsEndpoint))
.then(resp => resp.data)
.then(response => {
if (!response || !response.deployments) {
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 3fd9e07fa8b..63894e83362 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -7,6 +7,9 @@ export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILUR
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_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
+export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
+export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index c1779333d75..d4b816e2717 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,5 +1,6 @@
+import Vue from 'vue';
import * as types from './mutation_types';
-import { normalizeMetrics, sortMetrics } from './utils';
+import { normalizeMetrics, sortMetrics, normalizeQueryResult } from './utils';
export default {
[types.REQUEST_METRICS_DATA](state) {
@@ -7,10 +8,24 @@ export default {
state.showEmptyState = true;
},
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
- state.groups = groupData.map(group => ({
- ...group,
- metrics: normalizeMetrics(sortMetrics(group.metrics)),
- }));
+ state.groups = groupData.map(group => {
+ let { metrics } = group;
+
+ // for backwards compatibility, and to limit Vue template changes:
+ // for each group alias panels to metrics
+ // for each panel alias metrics to queries
+ if (state.useDashboardEndpoint) {
+ metrics = group.panels.map(panel => ({
+ ...panel,
+ queries: panel.metrics,
+ }));
+ }
+
+ return {
+ ...group,
+ metrics: normalizeMetrics(sortMetrics(metrics)),
+ };
+ });
if (!state.groups.length) {
state.emptyState = 'noData';
@@ -34,12 +49,40 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environments = [];
},
+ [types.SET_QUERY_RESULT](state, { metricId, result }) {
+ if (!metricId || !result || result.length === 0) {
+ return;
+ }
+
+ state.showEmptyState = false;
+
+ state.groups.forEach(group => {
+ group.metrics.forEach(metric => {
+ metric.queries.forEach(query => {
+ if (query.metric_id === metricId) {
+ state.metricsWithData.push(metricId);
+ // ensure dates/numbers are correctly formatted for charts
+ const normalizedResults = result.map(normalizeQueryResult);
+ Vue.set(query, 'result', Object.freeze(normalizedResults));
+ }
+ });
+ });
+ });
+ },
[types.SET_ENDPOINTS](state, endpoints) {
state.metricsEndpoint = endpoints.metricsEndpoint;
state.environmentsEndpoint = endpoints.environmentsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
+ state.dashboardEndpoint = endpoints.dashboardEndpoint;
+ },
+ [types.SET_DASHBOARD_ENABLED](state, enabled) {
+ state.useDashboardEndpoint = enabled;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
},
+ [types.SET_NO_DATA_EMPTY_STATE](state) {
+ state.showEmptyState = true;
+ state.emptyState = 'noData';
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 5103122612a..c33529cd588 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,12 +1,17 @@
+import invalidUrl from '~/lib/utils/invalid_url';
+
export default () => ({
hasMetrics: false,
showPanels: true,
metricsEndpoint: null,
environmentsEndpoint: null,
deploymentsEndpoint: null,
+ dashboardEndpoint: invalidUrl,
+ useDashboardEndpoint: false,
emptyState: 'gettingStarted',
showEmptyState: true,
groups: [],
deploymentData: [],
environments: [],
+ metricsWithData: [],
});
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 9216554ecbf..84e1f1c4c20 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -58,6 +58,14 @@ export const sortMetrics = metrics =>
.sortBy('weight')
.value();
+export const normalizeQueryResult = timeSeries => ({
+ ...timeSeries,
+ values: timeSeries.values.map(([timestamp, value]) => [
+ new Date(timestamp * 1000).toISOString(),
+ Number(value),
+ ]),
+});
+
export const normalizeMetrics = metrics => {
const groupedMetrics = groupQueriesByChartInfo(metrics);
@@ -66,13 +74,7 @@ export const normalizeMetrics = metrics => {
...query,
// custom metrics do not require a label, so we should ensure this attribute is defined
label: query.label || metric.y_label,
- result: query.result.map(result => ({
- ...result,
- values: result.values.map(([timestamp, value]) => [
- new Date(timestamp * 1000).toISOString(),
- Number(value),
- ]),
- })),
+ result: (query.result || []).map(normalizeQueryResult),
}));
return {
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index f338dbbb0a6..98522c67696 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, one-var, consistent-return, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return */
+/* eslint-disable func-names, no-var, one-var, consistent-return, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, @gitlab/i18n/no-non-i18n-strings */
import $ from 'jquery';
import RefSelectDropdown from './ref_select_dropdown';
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 09ecb695214..042ed196933 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -77,6 +77,11 @@ export default {
required: false,
default: '',
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
let updatedNoteBody = this.noteBody;
@@ -247,6 +252,8 @@ export default {
:can-suggest="canSuggest"
:add-spacing-classes="false"
:help-page-path="helpPagePath"
+ :show-suggest-popover="showSuggestPopover"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<textarea
id="note_note"
@@ -303,7 +310,7 @@ export default {
{{ __('Add comment now') }}
</button>
<button
- class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ class="btn note-edit-cancel js-close-discussion-note-form"
type="button"
@click="cancelHandler()"
>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index 6946578e6d2..f075291ce98 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -3,14 +3,6 @@ import store from './store';
import ExternalDashboardForm from './components/external_dashboard.vue';
export default () => {
- /**
- * This check can be removed when we remove
- * the :grafana_dashboard_link feature flag
- */
- if (!gon.features.grafanaDashboardLink) {
- return null;
- }
-
const el = document.querySelector('.js-operation-settings');
return new Vue({
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index 377dce6c746..506e6075d16 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -124,11 +124,14 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg
- .append('g')
- .attr('class', 'x axis')
- .attr('transform', 'translate(0, ' + this.height + ')')
- .call(this.x_axis);
+ return (
+ this.svg
+ .append('g')
+ .attr('class', 'x axis')
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ .attr('transform', 'translate(0, ' + this.height + ')')
+ .call(this.x_axis)
+ );
};
ContributorsGraph.prototype.draw_y_axis = function() {
@@ -205,6 +208,7 @@ export const ContributorsMasterGraph = (function(superClass) {
.attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr('class', 'tint-box')
.append('g')
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
.attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
@@ -354,6 +358,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
.attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr('class', 'spark')
.append('g')
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
.attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index f3a71ee434c..b2e365e5cde 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -83,8 +83,6 @@ export default {
v-if="shouldRenderContent"
:status="status"
:item-id="pipeline.id"
- :item-iid="pipeline.iid"
- :item-id-tooltip="__('Pipeline ID (IID)')"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 00c02e15562..c41ecab1294 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -2,7 +2,6 @@
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
@@ -20,7 +19,6 @@ export default {
components: {
UserAvatarLink,
GlLink,
- PipelineLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -61,13 +59,10 @@ export default {
};
</script>
<template>
- <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap">
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="js-pipeline-url-link"
- />
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags">
+ <gl-link :href="pipeline.path" class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{ pipeline.id }}</span>
+ </gl-link>
<div class="label-container">
<span
v-if="pipeline.flags.latest"
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 9dcea557b32..d730ef41b1a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -4,7 +4,7 @@ import { __, sprintf, s__ } from '../../locale';
import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
-import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 81fe0a48c06..f535b2ae9f2 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -4,7 +4,7 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '../../locale';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import Icon from '../../vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
@@ -64,7 +64,7 @@ export default {
<th>{{ s__('ContainerRegistry|Tag') }}</th>
<th>{{ s__('ContainerRegistry|Tag ID') }}</th>
<th>{{ s__('ContainerRegistry|Size') }}</th>
- <th>{{ s__('ContainerRegistry|Created') }}</th>
+ <th>{{ s__('ContainerRegistry|Last Updated') }}</th>
<th></th>
</tr>
</thead>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index e24a5e2c447..4519f82fc93 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,5 +1,6 @@
<script>
import { GlBadge } from '@gitlab/ui';
+import { visitUrl } from '~/lib/utils/url_utility';
import { getIconName } from '../../utils/icon';
import getRefMixin from '../../mixins/get_ref';
@@ -63,6 +64,8 @@ export default {
openRow() {
if (this.isFolder) {
this.$router.push(this.routerLinkTo);
+ } else {
+ visitUrl(this.url);
}
},
},
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index c64d16ef02a..ef147ec15cb 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -18,6 +18,7 @@ const defaultClient = createDefaultClient(
cacheConfig: {
fragmentMatcher,
dataIdFromObject: obj => {
+ /* eslint-disable @gitlab/i18n/no-non-i18n-strings */
// eslint-disable-next-line no-underscore-dangle
switch (obj.__typename) {
// We need to create a dynamic ID for each entry
@@ -33,6 +34,7 @@ const defaultClient = createDefaultClient(
// eslint-disable-next-line no-underscore-dangle
return obj.id || obj._id;
}
+ /* eslint-enable @gitlab/i18n/no-non-i18n-strings */
},
},
},
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
index 4e194640e92..87d54c01200 100644
--- a/app/assets/javascripts/repository/utils/title.js
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -5,5 +5,6 @@ export const setTitle = (pathMatch, ref, project) => {
const path = pathMatch.replace(/^\//, '');
const isEmpty = path === '';
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`;
};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 6aca4067ba7..842fb5e5b4f 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -447,9 +447,11 @@ export class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
}
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 7e6f02b10af..33cedf78331 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -427,6 +427,7 @@ function UsersSelect(currentUser, els, options = {}) {
const isActive = $el.hasClass('is-active');
const previouslySelected = $dropdown
.closest('.selectbox')
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
.find("input[name='" + $dropdown.data('fieldName') + "'][value!=0]");
// Enables support for limiting the number of users selected
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js
new file mode 100644
index 00000000000..2fec96d1435
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js
@@ -0,0 +1,132 @@
+import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants';
+import { clearNote, note, postError } from './note';
+import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } from './utils';
+
+const comment = `
+ <div>
+ <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea>
+ ${note}
+ <p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
+ </div>
+ <div class="gitlab-button-wrapper">
+ <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Logout </button>
+ <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
+ </div>
+`;
+
+const resetCommentBox = () => {
+ const commentBox = selectCommentBox();
+ const commentButton = selectCommentButton();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Send feedback';
+ commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
+ commentButton.style.opacity = 1;
+
+ commentBox.style.pointerEvents = 'auto';
+ commentBox.style.color = BLACK;
+};
+
+const resetCommentButton = () => {
+ const commentBox = selectCommentBox();
+ const currentNote = selectNote();
+
+ commentBox.value = '';
+ currentNote.innerText = '';
+};
+
+const resetComment = () => {
+ resetCommentBox();
+ resetCommentButton();
+};
+
+const confirmAndClear = mergeRequestId => {
+ const commentButton = selectCommentButton();
+ const currentNote = selectNote();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Feedback sent';
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`;
+ setTimeout(resetComment, 2000);
+};
+
+const setInProgressState = () => {
+ const commentButton = selectCommentButton();
+ const commentBox = selectCommentBox();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Sending feedback';
+ commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary');
+ commentButton.style.opacity = 0.5;
+ commentBox.style.color = MUTED;
+ commentBox.style.pointerEvents = 'none';
+};
+
+const postComment = ({
+ href,
+ platform,
+ browser,
+ userAgent,
+ innerWidth,
+ innerHeight,
+ projectId,
+ mergeRequestId,
+ mrUrl,
+ token,
+}) => {
+ // Clear any old errors
+ clearNote(COMMENT_BOX);
+
+ setInProgressState();
+
+ const commentText = selectCommentBox().value.trim();
+
+ if (!commentText) {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ postError('Your comment appears to be empty.', COMMENT_BOX);
+ resetCommentBox();
+ return;
+ }
+
+ const detailText = `
+ \n
+<details>
+ <summary>Metadata</summary>
+ Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.
+ <br /><br />
+ <em>User agent: ${userAgent}</em>
+</details>
+ `;
+
+ const url = `
+ ${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
+
+ const body = `${commentText} ${detailText}`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'PRIVATE-TOKEN': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ body }),
+ })
+ .then(response => {
+ if (response.ok) {
+ confirmAndClear(mergeRequestId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`);
+ })
+ .catch(err => {
+ postError(
+ `Your comment could not be sent. Please try again. Error: ${err.message}`,
+ COMMENT_BOX,
+ );
+ resetCommentBox();
+ });
+};
+
+export { comment, postComment };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/components/constants.js
new file mode 100644
index 00000000000..32ed1153515
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/constants.js
@@ -0,0 +1,37 @@
+// component selectors
+const COLLAPSE_BUTTON = 'gitlab-collapse';
+const COMMENT_BOX = 'gitlab-comment';
+const COMMENT_BUTTON = 'gitlab-comment-button';
+const FORM = 'gitlab-form-wrapper';
+const LOGIN = 'gitlab-login';
+const LOGOUT = 'gitlab-logout-button';
+const NOTE = 'gitlab-validation-note';
+const REMEMBER_TOKEN = 'gitlab-remember_token';
+const REVIEW_CONTAINER = 'gitlab-review-container';
+const TOKEN_BOX = 'gitlab-token';
+
+// colors — these are applied programmatically
+// rest of styles belong in ./styles
+const BLACK = 'rgba(46, 46, 46, 1)';
+const CLEAR = 'rgba(255, 255, 255, 0)';
+const MUTED = 'rgba(223, 223, 223, 0.5)';
+const RED = 'rgba(219, 59, 33, 1)';
+const WHITE = 'rgba(255, 255, 255, 1)';
+
+export {
+ COLLAPSE_BUTTON,
+ COMMENT_BOX,
+ COMMENT_BUTTON,
+ FORM,
+ LOGIN,
+ LOGOUT,
+ NOTE,
+ REMEMBER_TOKEN,
+ REVIEW_CONTAINER,
+ TOKEN_BOX,
+ BLACK,
+ CLEAR,
+ MUTED,
+ RED,
+ WHITE,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js
new file mode 100644
index 00000000000..43581818152
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/index.js
@@ -0,0 +1,23 @@
+import { comment, postComment } from './comment';
+import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants';
+import { authorizeUser, login } from './login';
+import { selectContainer } from './utils';
+import { form, logoutUser, toggleForm } from './wrapper';
+import { collapseButton } from './wrapper_icons';
+
+export {
+ authorizeUser,
+ collapseButton,
+ comment,
+ form,
+ login,
+ logoutUser,
+ postComment,
+ selectContainer,
+ toggleForm,
+ COLLAPSE_BUTTON,
+ COMMENT_BUTTON,
+ LOGIN,
+ LOGOUT,
+ REVIEW_CONTAINER,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js
new file mode 100644
index 00000000000..ce713cdc520
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/login.js
@@ -0,0 +1,52 @@
+import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants';
+import { clearNote, note, postError } from './note';
+import { buttonClearStyles, selectRemember, selectToken } from './utils';
+import { addCommentForm } from './wrapper';
+
+const login = `
+ <div>
+ <label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label>
+ <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password">
+ ${note}
+ </div>
+ <div class="gitlab-checkbox-wrapper">
+ <input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember">
+ <label for="${REMEMBER_TOKEN}" class="gitlab-checkbox-label">Remember me</label>
+ </div>
+ <div class="gitlab-button-wrapper">
+ <button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${LOGIN}"> Submit </button>
+ </div>
+`;
+
+const storeToken = (token, state) => {
+ const { localStorage } = window;
+ const rememberMe = selectRemember().checked;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ if (rememberMe) {
+ localStorage.setItem('token', token);
+ }
+ } finally {
+ state.token = token;
+ }
+};
+
+const authorizeUser = state => {
+ // Clear any old errors
+ clearNote(TOKEN_BOX);
+
+ const token = selectToken().value;
+
+ if (!token) {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ postError('Please enter your token.', TOKEN_BOX);
+ return;
+ }
+
+ storeToken(token, state);
+ addCommentForm();
+};
+
+export { authorizeUser, login };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js
new file mode 100644
index 00000000000..dfebf58fd95
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/note.js
@@ -0,0 +1,27 @@
+import { NOTE, RED } from './constants';
+import { selectById, selectNote } from './utils';
+
+const note = `
+ <p id=${NOTE} class='gitlab-message'></p>
+`;
+
+const clearNote = inputId => {
+ const currentNote = selectNote();
+ currentNote.innerText = '';
+ currentNote.style.color = '';
+
+ if (inputId) {
+ const field = document.getElementById(inputId);
+ field.style.borderColor = '';
+ }
+};
+
+const postError = (message, inputId) => {
+ const currentNote = selectNote();
+ const field = selectById(inputId);
+ field.style.borderColor = RED;
+ currentNote.style.color = RED;
+ currentNote.innerText = message;
+};
+
+export { clearNote, note, postError };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js
new file mode 100644
index 00000000000..7bc2e5a905b
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js
@@ -0,0 +1,42 @@
+/* global document */
+
+import {
+ COLLAPSE_BUTTON,
+ COMMENT_BOX,
+ COMMENT_BUTTON,
+ FORM,
+ NOTE,
+ REMEMBER_TOKEN,
+ REVIEW_CONTAINER,
+ TOKEN_BOX,
+} from './constants';
+
+// this style must be applied inline in a handful of components
+/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+const buttonClearStyles = `
+ -webkit-appearance: none;
+`;
+
+// selector functions to abstract out a little
+const selectById = id => document.getElementById(id);
+const selectCollapseButton = () => document.getElementById(COLLAPSE_BUTTON);
+const selectCommentBox = () => document.getElementById(COMMENT_BOX);
+const selectCommentButton = () => document.getElementById(COMMENT_BUTTON);
+const selectContainer = () => document.getElementById(REVIEW_CONTAINER);
+const selectForm = () => document.getElementById(FORM);
+const selectNote = () => document.getElementById(NOTE);
+const selectRemember = () => document.getElementById(REMEMBER_TOKEN);
+const selectToken = () => document.getElementById(TOKEN_BOX);
+
+export {
+ buttonClearStyles,
+ selectById,
+ selectCollapseButton,
+ selectContainer,
+ selectCommentBox,
+ selectCommentButton,
+ selectForm,
+ selectNote,
+ selectRemember,
+ selectToken,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
new file mode 100644
index 00000000000..233b7ec496c
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
@@ -0,0 +1,82 @@
+import { comment } from './comment';
+import { CLEAR, FORM, WHITE } from './constants';
+import { login } from './login';
+import { selectCollapseButton, selectContainer, selectForm } from './utils';
+import { commentIcon, compressIcon } from './wrapper_icons';
+
+const form = content => `
+ <form id=${FORM}>
+ ${content}
+ </form>
+`;
+
+const addCommentForm = () => {
+ const formWrapper = selectForm();
+ formWrapper.innerHTML = comment;
+};
+
+const addLoginForm = () => {
+ const formWrapper = selectForm();
+ formWrapper.innerHTML = login;
+};
+
+function logoutUser() {
+ const { localStorage } = window;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ localStorage.removeItem('token');
+ } catch (err) {
+ return;
+ }
+
+ addLoginForm();
+}
+
+function toggleForm() {
+ const container = selectContainer();
+ const collapseButton = selectCollapseButton();
+ const currentForm = selectForm();
+ const OPEN = 'open';
+ const CLOSED = 'closed';
+
+ /*
+ You may wonder why we spread the arrays before we reverse them.
+ In the immortal words of MDN,
+ Careful: reverse is destructive. It also changes the original array
+ */
+
+ const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open'];
+ const closedButtonClasses = [...openButtonClasses].reverse();
+ const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper'];
+ const closedContainerClasses = [...openContainerClasses].reverse();
+
+ const stateVals = {
+ [OPEN]: {
+ buttonClasses: openButtonClasses,
+ containerClasses: openContainerClasses,
+ icon: compressIcon,
+ display: 'flex',
+ backgroundColor: WHITE,
+ },
+ [CLOSED]: {
+ buttonClasses: closedButtonClasses,
+ containerClasses: closedContainerClasses,
+ icon: commentIcon,
+ display: 'none',
+ backgroundColor: CLEAR,
+ },
+ };
+
+ const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
+ const currentVals = stateVals[nextState];
+
+ container.classList.replace(...currentVals.containerClasses);
+ container.style.backgroundColor = currentVals.backgroundColor;
+ currentForm.style.display = currentVals.display;
+ collapseButton.classList.replace(...currentVals.buttonClasses);
+ collapseButton.innerHTML = currentVals.icon;
+}
+
+export { addCommentForm, addLoginForm, form, logoutUser, toggleForm };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js
new file mode 100644
index 00000000000..b686fd4f5c2
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js
@@ -0,0 +1,15 @@
+import { buttonClearStyles } from './utils';
+
+const commentIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
+`;
+
+const compressIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
+`;
+
+const collapseButton = `
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button>
+`;
+
+export { commentIcon, compressIcon, collapseButton };
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
index 91d0382feac..941d77e25b4 100644
--- a/app/assets/javascripts/visual_review_toolbar/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -1,2 +1,37 @@
import './styles/toolbar.css';
-import 'vendor/visual_review_toolbar';
+
+import { form, selectContainer, REVIEW_CONTAINER } from './components';
+import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store';
+
+/*
+
+ Welcome to the visual review toolbar files. A few useful notes:
+
+ - These files build a static script that is served from our webpack
+ assets folder. (https://gitlab.com/assets/webpack/visual_review_toolbar.js)
+
+ - To compile this file, run `yarn webpack-vrt`.
+
+ - Vue is not used in these files because we do not want to ask users to
+ install another library at this time. It's all pure vanilla javascript.
+
+*/
+
+window.addEventListener('load', () => {
+ initializeState(window, document);
+
+ const { content, toggleButton } = getInitialView(window);
+ const container = document.createElement('div');
+
+ container.setAttribute('id', REVIEW_CONTAINER);
+ container.insertAdjacentHTML('beforeend', toggleButton);
+ container.insertAdjacentHTML('beforeend', form(content));
+
+ document.body.insertBefore(container, document.body.firstChild);
+
+ selectContainer().addEventListener('click', event => {
+ eventLookup(event)();
+ });
+
+ window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200));
+});
diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js
new file mode 100644
index 00000000000..93996be8473
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/events.js
@@ -0,0 +1,36 @@
+import {
+ authorizeUser,
+ logoutUser,
+ postComment,
+ toggleForm,
+ COLLAPSE_BUTTON,
+ COMMENT_BUTTON,
+ LOGIN,
+ LOGOUT,
+} from '../components';
+
+import { state } from './state';
+
+const noop = () => {};
+
+const eventLookup = ({ target: { id } }) => {
+ switch (id) {
+ case COLLAPSE_BUTTON:
+ return toggleForm;
+ case COMMENT_BUTTON:
+ return postComment.bind(null, state);
+ case LOGIN:
+ return authorizeUser.bind(null, state);
+ case LOGOUT:
+ return logoutUser;
+ default:
+ return noop;
+ }
+};
+
+const updateWindowSize = wind => {
+ state.innerWidth = wind.innerWidth;
+ state.innerHeight = wind.innerHeight;
+};
+
+export { eventLookup, updateWindowSize };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js
new file mode 100644
index 00000000000..7143588c0bf
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/index.js
@@ -0,0 +1,5 @@
+import { eventLookup, updateWindowSize } from './events';
+import { getInitialView, initializeState } from './state';
+import debounce from './utils';
+
+export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js
new file mode 100644
index 00000000000..f5ede6e85b2
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/state.js
@@ -0,0 +1,77 @@
+import { comment, login, collapseButton } from '../components';
+
+const state = {
+ browser: '',
+ href: '',
+ innerWidth: '',
+ innerHeight: '',
+ mergeRequestId: '',
+ mrUrl: '',
+ platform: '',
+ projectId: '',
+ userAgent: '',
+ token: '',
+};
+
+// adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator#Example_2_Browser_detect_and_return_an_index
+const getBrowserId = sUsrAg => {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ const aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'];
+ let nIdx = aKeys.length - 1;
+
+ for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx -= 1);
+ return aKeys[nIdx];
+};
+
+const initializeState = (wind, doc) => {
+ const {
+ innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: { platform, userAgent },
+ } = wind;
+
+ const browser = getBrowserId(userAgent);
+
+ const scriptEl = doc.getElementById('review-app-toolbar-script');
+ const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
+
+ // This mutates our default state object above. It's weird but it makes the linter happy.
+ Object.assign(state, {
+ browser,
+ href,
+ innerWidth,
+ innerHeight,
+ mergeRequestId,
+ mrUrl,
+ platform,
+ projectId,
+ userAgent,
+ });
+};
+
+function getInitialView({ localStorage }) {
+ const loginView = {
+ content: login,
+ toggleButton: collapseButton,
+ };
+
+ const commentView = {
+ content: comment,
+ toggleButton: collapseButton,
+ };
+
+ try {
+ const token = localStorage.getItem('token');
+
+ if (token) {
+ state.token = token;
+ return commentView;
+ }
+ return loginView;
+ } catch (err) {
+ return loginView;
+ }
+}
+
+export { initializeState, getInitialView, state };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js
new file mode 100644
index 00000000000..5cf145351b3
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/utils.js
@@ -0,0 +1,15 @@
+const debounce = (fn, time) => {
+ let current;
+
+ const debounced = () => {
+ if (current) {
+ clearTimeout(current);
+ }
+
+ current = setTimeout(fn, time);
+ };
+
+ return debounced;
+};
+
+export default debounce;
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 abe5bdd2901..34cdb70ce14 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -49,6 +49,7 @@ export default {
required: false,
default: () => ({
sourceProjectId: '',
+ sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
@@ -184,11 +185,6 @@ export default {
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
- <visual-review-app-link
- v-if="showVisualReviewApp"
- :link="deploymentExternalUrl"
- :app-metadata="visualReviewAppMeta"
- />
</template>
<template slot="result" slot-scope="slotProps">
@@ -213,12 +209,12 @@ export default {
:link="deploymentExternalUrl"
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
/>
- <visual-review-app-link
- v-if="showVisualReviewApp"
- :link="deploymentExternalUrl"
- :app-metadata="visualReviewAppMeta"
- />
</template>
+ <visual-review-app-link
+ v-if="showVisualReviewApp"
+ :link="deploymentExternalUrl"
+ :app-metadata="visualReviewAppMeta"
+ />
</template>
<span
v-if="deployment.stop_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index c377c16fb13..f5fa68308bc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -5,7 +5,6 @@ import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
@@ -17,7 +16,6 @@ export default {
Icon,
TooltipOnTruncate,
GlLink,
- PipelineLink,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -114,12 +112,9 @@ export default {
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
{{ s__('Pipeline|Pipeline') }}
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="pipeline-id pipeline-iid font-weight-normal"
- />
+ <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</gl-link
+ >
{{ pipeline.details.status.label }}
<template v-if="hasCommitInfo">
{{ s__('Pipeline|for') }}
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 03a15ba81ed..17ac8ada32d 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
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import Deployment from './deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
@@ -17,6 +18,8 @@ export default {
Deployment,
MrWidgetContainer,
MrWidgetPipeline,
+ MergeTrainInfo: () =>
+ import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'),
},
props: {
mr: {
@@ -50,6 +53,7 @@ export default {
appUrl: this.mr.appUrl,
mergeRequestId: this.mr.iid,
sourceProjectId: this.mr.sourceProjectId,
+ sourceProjectPath: this.mr.sourceProjectFullPath,
};
},
pipeline() {
@@ -58,6 +62,9 @@ export default {
showVisualReviewAppLink() {
return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
},
+ showMergeTrainInfo() {
+ return _.isNumber(this.mr.mergeTrainIndex);
+ },
},
};
</script>
@@ -79,10 +86,15 @@ export default {
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
- :show-visual-review-app="true"
+ :show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
+ <merge-train-info
+ v-if="showMergeTrainInfo"
+ class="mr-widget-extension"
+ :merge-train-index="mr.mergeTrainIndex"
+ />
</template>
</mr-widget-container>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 88e1ccbaf35..5958c2cf87e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -1,15 +1,19 @@
<script>
+import _ from 'underscore';
+import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
+import { AUTO_MERGE_STRATEGIES } from '../../constants';
export default {
- name: 'MRWidgetMergeWhenPipelineSucceeds',
+ name: 'MRWidgetAutoMergeEnabled',
components: {
MrWidgetAuthor,
statusIcon,
},
+ mixins: [autoMergeMixin],
props: {
mr: {
type: Object,
@@ -57,7 +61,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ auto_merge_strategy: this.mr.autoMergeStrategy,
should_remove_source_branch: true,
};
@@ -66,7 +70,7 @@ export default {
.merge(options)
.then(res => res.data)
.then(data => {
- if (data.status === 'merge_when_pipeline_succeeds') {
+ if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
@@ -84,9 +88,9 @@ export default {
<div class="media-body">
<h4 class="d-flex align-items-start">
<span class="append-right-10">
- {{ s__('mrWidget|Set by') }}
+ <span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" />
- {{ s__('mrWidget|to be merged automatically when the pipeline succeeds') }}
+ <span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
@@ -97,7 +101,7 @@ export default {
@click.prevent="cancelAutomaticMerge"
>
<i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- {{ s__('mrWidget|Cancel automatic merge') }}
+ {{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index f6f445c1cef..3df4a777aca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -26,7 +26,7 @@ export default {
);
},
showResolveButton() {
- return this.mr.conflictResolutionPath && this.mr.canMerge;
+ return this.mr.conflictResolutionPath && this.mr.canPushToSourceBranch;
},
showPopover() {
return this.showResolveButton && this.mr.sourceBranchProtected;
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 615d59a7b8e..ca1b4a57717 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
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
@@ -12,6 +13,7 @@ import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
+import { AUTO_MERGE_STRATEGIES } from '../../constants';
export default {
name: 'ReadyToMerge',
@@ -30,8 +32,6 @@ export default {
data() {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
- mergeWhenBuildSucceeds: false,
- autoMergeStrategy: undefined,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
@@ -42,18 +42,18 @@ export default {
};
},
computed: {
- shouldShowAutoMergeText() {
- return this.mr.isPipelineActive;
+ isAutoMergeAvailable() {
+ return !_.isEmpty(this.mr.availableAutoMergeStrategies);
},
status() {
- const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
+ const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
return 'failed';
+ } else if (this.isAutoMergeAvailable) {
+ return 'pending';
} else if (!pipeline) {
return 'success';
- } else if (isPipelineActive) {
- return 'pending';
} else if (isPipelineFailed) {
return 'failed';
}
@@ -87,14 +87,14 @@ export default {
mergeButtonText() {
if (this.isMergingImmediately) {
return __('Merge in progress');
- } else if (this.shouldShowAutoMergeText) {
- return __('Merge when pipeline succeeds');
+ } else if (this.isAutoMergeAvailable) {
+ return this.autoMergeText;
}
- return 'Merge';
+ return __('Merge');
},
shouldShowMergeOptionsDropdown() {
- return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
+ return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds;
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
@@ -104,7 +104,7 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
shouldShowMergeControls() {
- return this.mr.isMergeAllowed || this.shouldShowAutoMergeText;
+ return this.mr.isMergeAllowed || this.isAutoMergeAvailable;
},
shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
@@ -118,20 +118,15 @@ export default {
const { commitMessageWithDescription, commitMessage } = this.mr;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
- handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
- // TODO: Remove no-param-reassign
- if (mergeWhenBuildSucceeds === undefined) {
- mergeWhenBuildSucceeds = this.mr.isPipelineActive; // eslint-disable-line no-param-reassign
- } else if (mergeImmediately) {
+ handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
+ if (mergeImmediately) {
this.isMergingImmediately = true;
}
- this.autoMergeStrategy = mergeWhenBuildSucceeds ? 'merge_when_pipeline_succeeds' : undefined;
-
const options = {
sha: this.mr.sha,
commit_message: this.commitMessage,
- auto_merge_strategy: this.autoMergeStrategy,
+ auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
squash_commit_message: this.squashCommitMessage,
@@ -144,7 +139,7 @@ export default {
.then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
- if (data.status === 'merge_when_pipeline_succeeds') {
+ if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) {
eventHub.$emit('MRWidgetUpdateRequested');
} else if (data.status === 'success') {
this.initiateMergePolling();
@@ -242,13 +237,13 @@ export default {
:class="mergeButtonClass"
type="button"
class="qa-merge-button"
- @click="handleMergeButtonClick()"
+ @click="handleMergeButtonClick(isAutoMergeAvailable)"
>
<i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
{{ mergeButtonText }}
</button>
<button
- v-if="shouldShowMergeOptionsDropdown"
+ v-if="isAutoMergeAvailable"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
@@ -264,15 +259,13 @@ export default {
>
<li>
<a
- class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
+ class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
- <span class="media-body merge-opt-title">{{
- __('Merge when pipeline succeeds')
- }}</span>
+ <span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 0a29d55fbd6..3e65bdf0cb0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -3,3 +3,13 @@ export const DANGER = 'danger';
export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message';
+
+export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
+export const ATMTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
+export const MT_MERGE_STRATEGY = 'merge_train';
+
+export const AUTO_MERGE_STRATEGIES = [
+ MWPS_MERGE_STRATEGY,
+ ATMTWPS_MERGE_STRATEGY,
+ MT_MERGE_STRATEGY,
+];
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js
new file mode 100644
index 00000000000..23e140623cc
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js
@@ -0,0 +1,15 @@
+import { s__ } from '~/locale';
+
+export default {
+ computed: {
+ statusTextBeforeAuthor() {
+ return s__('mrWidget|Set by');
+ },
+ statusTextAfterAuthor() {
+ return s__('mrWidget|to be merged automatically when the pipeline succeeds');
+ },
+ cancelButtonText() {
+ return s__('mrWidget|Cancel automatic merge');
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index b2e64506472..116d537c463 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default {
computed: {
isMergeButtonDisabled() {
@@ -9,5 +11,9 @@ export default {
this.mr.preventMerge,
);
},
+ autoMergeText() {
+ // MWPS is currently the only auto merge strategy available in CE
+ return __('Merge when pipeline succeeds');
+ },
},
};
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 d02bb2f341d..41386178a1e 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
@@ -29,7 +29,7 @@ import UnresolvedDiscussionsState from './components/states/unresolved_discussio
import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue';
import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
-import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
+import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
import eventHub from './event_hub';
@@ -64,7 +64,7 @@ export default {
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState,
'mr-widget-pipeline-failed': PipelineFailedState,
- 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+ MrWidgetAutoMergeEnabled,
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus,
@@ -117,14 +117,6 @@ export default {
this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId,
);
},
- showTargetBranchAdvancedError() {
- return Boolean(
- this.mr.isOpen &&
- this.mr.pipeline &&
- this.mr.pipeline.target_sha &&
- this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
- );
- },
mergeError() {
return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), {
mergeError: this.mr.mergeError,
@@ -363,18 +355,6 @@ export default {
}}
</mr-widget-alert-message>
- <mr-widget-alert-message
- v-if="showTargetBranchAdvancedError"
- type="danger"
- :help-path="mr.mergeRequestPipelinesHelpPath"
- >
- {{
- s__(
- 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging',
- )
- }}
- </mr-widget-alert-message>
-
<mr-widget-alert-message v-if="mr.mergeError" type="danger">
{{ mergeError }}
</mr-widget-alert-message>
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 32badb0fb08..bfa3e7f4a59 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
@@ -1,7 +1,9 @@
import Timeago from 'timeago.js';
+import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
+import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
export default class MergeRequestStore {
constructor(data) {
@@ -77,6 +79,10 @@ export default class MergeRequestStore {
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.autoMergeStrategy = data.auto_merge_strategy;
+ this.availableAutoMergeStrategies = data.available_auto_merge_strategies;
+ this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
+ this.availableAutoMergeStrategies,
+ );
this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = Boolean(data.should_be_rebased);
@@ -104,7 +110,9 @@ export default class MergeRequestStore {
this.sourceProjectFullPath = data.source_project_full_path;
this.sourceProjectId = data.source_project_id;
this.targetProjectId = data.target_project_id;
- this.mergePipelinesEnabled = data.merge_pipelines_enabled;
+ this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
+ this.mergeTrainsCount = data.merge_trains_count || 0;
+ this.mergeTrainIndex = data.merge_train_index;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
@@ -204,4 +212,16 @@ export default class MergeRequestStore {
return timeagoInstance.format(date);
}
+
+ static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
+ if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) {
+ return ATMTWPS_MERGE_STRATEGY;
+ } else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) {
+ return MT_MERGE_STRATEGY;
+ } else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) {
+ return MWPS_MERGE_STRATEGY;
+ }
+
+ return undefined;
+ }
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 48bc6a867f4..28507bba3e5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -13,7 +13,7 @@ const stateToComponentMap = {
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
- autoMergeEnabled: 'mr-widget-merge-when-pipeline-succeeds',
+ autoMergeEnabled: 'mr-widget-auto-merge-enabled',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch',
diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
deleted file mode 100644
index eae4c06467c..00000000000
--- a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- components: {
- GlLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- href: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- pipelineIid: {
- type: Number,
- required: true,
- },
- },
-};
-</script>
-<template>
- <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')">
- <span class="pipeline-id">#{{ pipelineId }}</span>
- <span class="pipeline-iid">(#{{ pipelineIid }})</span>
- </gl-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 3ba946e6447..a1168fa0f1e 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,6 +1,7 @@
<script>
import _ from 'underscore';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import Icon from '../../vue_shared/components/icon.vue';
@@ -12,6 +13,7 @@ export default {
UserAvatarLink,
Icon,
GlLink,
+ TooltipOnTruncate,
},
props: {
/**
@@ -165,7 +167,7 @@ export default {
<gl-link :href="commitUrl" class="commit-sha mr-0"> {{ shortSha }} </gl-link>
<div class="commit-title flex-truncate-parent">
- <span v-if="title" class="flex-truncate-child">
+ <tooltip-on-truncate v-if="title" class="flex-truncate-child" :title="title">
<user-avatar-link
v-if="hasAuthor"
:link-href="author.path"
@@ -174,8 +176,10 @@ export default {
:tooltip-text="author.username"
class="avatar-image-container"
/>
- <gl-link :href="commitUrl" class="commit-row-message cgray"> {{ title }} </gl-link>
- </span>
+ <gl-link :href="commitUrl" class="commit-row-message cgray">
+ {{ title }}
+ </gl-link>
+ </tooltip-on-truncate>
<span v-else> Can't find HEAD commit for this branch </span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 0bac63b1062..3f45dc7853b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -37,16 +37,6 @@ export default {
type: Number,
required: true,
},
- itemIid: {
- type: Number,
- required: false,
- default: null,
- },
- itemIdTooltip: {
- type: String,
- required: false,
- default: '',
- },
time: {
type: String,
required: true,
@@ -95,12 +85,7 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong v-gl-tooltip :title="itemIdTooltip">
- {{ itemName }} #{{ itemId }}
- <template v-if="itemIid"
- >(#{{ itemIid }})</template
- >
- </strong>
+ <strong> {{ itemName }} #{{ itemId }} </strong>
<template v-if="shouldRenderTriggeredLabel">
triggered
@@ -111,8 +96,9 @@ export default {
<timeago-tooltip :time="time" />
+ by
+
<template v-if="user">
- by
<gl-link
v-gl-tooltip
:href="user.path"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 0f3b3568414..3bdc0bb8ebd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -67,6 +67,11 @@ export default {
required: false,
default: '',
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -194,8 +199,10 @@ export default {
:preview-markdown="previewMarkdown"
:line-content="lineContent"
:can-suggest="canSuggest"
+ :show-suggest-popover="showSuggestPopover"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index a5a5b2ef415..56a16c9e4d6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
@@ -8,6 +8,8 @@ export default {
components: {
ToolbarButton,
Icon,
+ GlPopover,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -27,6 +29,11 @@ export default {
required: false,
default: true,
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
mdTable() {
@@ -70,6 +77,9 @@ export default {
this.$emit('write-markdown');
},
+ handleSuggestDismissed() {
+ this.$emit('handleSuggestDismissed');
+ },
},
};
</script>
@@ -93,66 +103,92 @@ export default {
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
- <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
- <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
- <toolbar-button
- :prepend="true"
- tag="> "
- :button-title="__('Insert a quote')"
- icon="quote"
- />
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
- <toolbar-button
- tag="[{text}](url)"
- tag-select="url"
- :button-title="__('Add a link')"
- icon="link"
- />
- <toolbar-button
- :prepend="true"
- tag="* "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- />
- <toolbar-button
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- />
- <toolbar-button
- :prepend="true"
- tag="* [ ] "
- :button-title="__('Add a task list')"
- icon="task-done"
- />
- <toolbar-button
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- />
- <toolbar-button
- v-if="canSuggest"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- class="qa-suggestion-btn"
- />
- <button
- v-gl-tooltip
- :aria-label="__('Go full screen')"
- class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- :title="__('Go full screen')"
- type="button"
- >
- <icon name="screen-full" />
- </button>
+ <div class="d-inline-block">
+ <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
+ <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
+ <toolbar-button
+ :prepend="true"
+ tag="> "
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <template v-if="canSuggest">
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ class="qa-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="showSuggestPopover"
+ :target="() => $refs.suggestButton"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="showSuggestPopover"
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{ __('Suggest code changes which are immediately applied. Try it out!') }}
+ </p>
+ <gl-button variant="primary" size="sm" @click="handleSuggestDismissed">
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
+ <toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="__('Add a link')"
+ icon="link"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <toolbar-button
+ :prepend="true"
+ tag="* "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="* [ ] "
+ :button-title="__('Add a task list')"
+ icon="task-done"
+ />
+ <toolbar-button
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <button
+ v-gl-tooltip
+ :aria-label="__('Go full screen')"
+ class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ :title="__('Go full screen')"
+ type="button"
+ >
+ <icon name="screen-full" />
+ </button>
+ </div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 4572caa907b..94f78c0c085 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -66,6 +66,7 @@ export default {
class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
+ @click="() => $emit('click')"
>
<icon :name="icon" />
</button>
diff --git a/app/assets/javascripts/vue_shared/components/pagination/constants.js b/app/assets/javascripts/vue_shared/components/pagination/constants.js
index c24b142ac7e..748ad178c70 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/constants.js
+++ b/app/assets/javascripts/vue_shared/components/pagination/constants.js
@@ -7,3 +7,7 @@ export const PREV = s__('Pagination|Prev');
export const NEXT = s__('Pagination|Next');
export const FIRST = s__('Pagination|« First');
export const LAST = s__('Pagination|Last »');
+export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page');
+export const LABEL_PREV_PAGE = s__('Pagination|Go to previous page');
+export const LABEL_NEXT_PAGE = s__('Pagination|Go to next page');
+export const LABEL_LAST_PAGE = s__('Pagination|Go to last page');
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index 1e2d4ffa7e3..1e2d4ffa7e3 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
index 0b44f8578cb..06097913e91 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_links.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -1,6 +1,13 @@
<script>
import { GlPagination } from '@gitlab/ui';
-import { s__ } from '../../locale';
+import {
+ PREV,
+ NEXT,
+ LABEL_FIRST_PAGE,
+ LABEL_PREV_PAGE,
+ LABEL_NEXT_PAGE,
+ LABEL_LAST_PAGE,
+} from '~/vue_shared/components/pagination/constants';
export default {
components: {
@@ -16,23 +23,27 @@ export default {
required: true,
},
},
- firstText: s__('Pagination|« First'),
- prevText: s__('Pagination|Prev'),
- nextText: s__('Pagination|Next'),
- lastText: s__('Pagination|Last »'),
+ prevText: PREV,
+ nextText: NEXT,
+ labelFirstPage: LABEL_FIRST_PAGE,
+ labelPrevPage: LABEL_PREV_PAGE,
+ labelNextPage: LABEL_NEXT_PAGE,
+ labelLastPage: LABEL_LAST_PAGE,
};
</script>
<template>
<gl-pagination
v-bind="$attrs"
- :change="change"
- :page="pageInfo.page"
+ :value="pageInfo.page"
:per-page="pageInfo.perPage"
:total-items="pageInfo.total"
- :first-text="$options.firstText"
:prev-text="$options.prevText"
:next-text="$options.nextText"
- :last-text="$options.lastText"
+ :label-first-page="$options.labelFirstPage"
+ :label-prev-page="$options.labelPrevPage"
+ :label-next-page="$options.labelNextPage"
+ :label-last-page="$options.labelLastPage"
+ @input="change"
/>
</template>
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 7f6384f4eea..802d58779d0 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -147,11 +147,6 @@ table {
pointer-events: none;
}
-.popover,
-.popover-header {
- font-size: 14px;
-}
-
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
index 774be9ef588..58aaca93160 100644
--- a/app/assets/stylesheets/components/popover.scss
+++ b/app/assets/stylesheets/components/popover.scss
@@ -1,37 +1,98 @@
.popover {
- min-width: 300px;
-
- .popover-body .user-popover {
- padding: $gl-padding-8;
- font-size: $gl-font-size-small;
- line-height: $gl-line-height;
-
- .category-icon {
- color: $gray-600;
- }
- }
+ max-width: $popover-max-width;
+ border: 1px solid $gray-200;
+ box-shadow: 0 2px 3px 1px $gray-200;
+ font-size: $gl-font-size-small;
+ /**
+ * Blue popover variation
+ */
&.blue {
background-color: $blue-600;
+ border-color: $blue-600;
.popover-body {
color: $white-light;
}
&.bs-popover-bottom {
+ .arrow::before,
.arrow::after {
border-bottom-color: $blue-600;
}
}
&.bs-popover-top {
+ .arrow::before,
.arrow::after {
border-top-color: $blue-600;
}
}
+
+ &.bs-popover-right {
+ .arrow::after,
+ .arrow::before {
+ border-right-color: $blue-600;
+ }
+ }
+
+ &.bs-popover-left {
+ .arrow::before,
+ .arrow::after {
+ border-left-color: $blue-600;
+ }
+ }
+ }
+}
+
+.bs-popover-top {
+ /* When popover position is top, the arrow is translated 1 pixel
+ * due to the box-shadow include in our custom styles.
+ */
+ > .arrow::before {
+ border-top-color: $gray-200;
+ bottom: 1px;
+ }
+
+ > .arrow::after {
+ bottom: 2px;
+ }
+}
+
+.bs-popover-bottom {
+ > .arrow::before {
+ border-bottom-color: $gray-200;
+ }
+
+ > .popover-header::before {
+ border-color: $white-light;
+ }
+}
+
+.bs-popover-right > .arrow::before {
+ border-right-color: $gray-200;
+}
+
+.bs-popover-left > .arrow::before {
+ border-left-color: $gray-200;
+}
+
+.popover-header {
+ background-color: $white-light;
+ font-size: $gl-font-size-small;
+}
+
+.popover-body {
+ padding: $gl-padding $gl-padding-12;
+
+ > .popover-hr {
+ margin: $gl-padding 0;
}
}
+/**
+* mr_popover component
+*/
.mr-popover {
.text-secondary {
font-size: 12px;
@@ -58,6 +119,18 @@
}
}
+/**
+* user_popover component
+*/
+.user-popover {
+ padding: $gl-padding-8;
+ line-height: $gl-line-height;
+
+ .category-icon {
+ color: $gray-600;
+ }
+}
+
.onboarding-welcome-page {
.popover {
min-width: auto;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index db09118ba15..1bd5043ed10 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -416,6 +416,7 @@ img.emoji {
.center { text-align: center; }
.block { display: block; }
.flex { display: flex; }
+.vertical-align-top { vertical-align: top; }
.vertical-align-middle { vertical-align: middle; }
.vertical-align-sub { vertical-align: sub; }
.flex-align-self-center { align-self: center; }
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
index 85cabf43e9e..f9b167669a6 100644
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -39,7 +39,7 @@
display: none;
hr {
- margin: $gl-padding * 0.5 0;
+ margin: $gl-padding 0;
}
.btn-link {
@@ -71,9 +71,6 @@
.feature-highlight-popover {
width: 240px;
- padding: 0;
- border: 1px solid $border-color;
- box-shadow: 0 2px 4px $dropdown-shadow-color;
&.right > .arrow {
border-right-color: $border-color;
@@ -85,7 +82,7 @@
}
.feature-highlight-popover-sub-content {
- padding: 9px 14px;
+ padding: $gl-padding $gl-padding-12;
}
@include keyframes(pulse-highlight) {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2a601afff53..821e6691fe4 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -248,14 +248,24 @@ label {
.gl-form-checkbox {
align-items: baseline;
+ margin-right: 1rem;
+ margin-bottom: 0.25rem;
+
+ .form-check-input {
+ margin-right: 0;
+ }
+
+ .form-check-label {
+ padding-left: $gl-padding-8;
+ }
&.form-check-inline .form-check-input {
align-self: flex-start;
- margin-right: $gl-padding-8;
height: 1.5 * $gl-font-size;
}
- .help-text {
- margin-bottom: 0;
+ .form-check-input:disabled,
+ .form-check-input:disabled ~ .form-check-label {
+ cursor: not-allowed;
}
}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 741f92110c3..983bd032da4 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -11,7 +11,7 @@
border-radius: 0 0 $border-radius-default $border-radius-default;
font-family: $monospace-font;
font-size: $code-font-size;
- line-height: 19px;
+ line-height: 1.5;
margin: 0;
overflow: auto;
overflow-y: hidden;
@@ -30,7 +30,7 @@
.line {
display: block;
width: 100%;
- min-height: 19px;
+ min-height: 1.5em;
padding-left: 10px;
padding-right: 10px;
white-space: pre;
@@ -48,7 +48,7 @@
font-family: $monospace-font;
display: block;
font-size: $code-font-size !important;
- min-height: 19px;
+ min-height: 1.5em;
white-space: nowrap;
i {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index bfd96a4bc05..0bf911eec0a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -154,11 +154,9 @@
}
.toolbar-fullscreen-btn {
- margin-left: $gl-padding;
margin-right: -5px;
@include media-breakpoint-down(xs) {
- margin-left: 0;
margin-right: 0;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index dc451a97e17..b6a24247d40 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -336,6 +336,7 @@ $tooltip-font-size: 12px;
*/
$gl-padding-4: 4px;
$gl-padding-8: 8px;
+$gl-padding-12: 12px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
@@ -810,6 +811,11 @@ $modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px;
/*
+Popovers
+*/
+$popover-max-width: 384px;
+
+/*
Issues Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
@@ -817,7 +823,7 @@ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
/*
Merge Requests
*/
-$mr-tabs-height: 51px;
+$mr-tabs-height: 48px;
$mr-version-controls-height: 56px;
/*
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index f08fa80495d..cbcd8a474f1 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -396,10 +396,6 @@ $ide-commit-header-height: 48px;
font-size: inherit;
}
- > div + div {
- padding-left: $gl-padding;
- }
-
svg {
vertical-align: sub;
}
@@ -410,13 +406,14 @@ $ide-commit-header-height: 48px;
}
}
+.ide-status-list {
+ > div + div {
+ padding-left: $gl-padding;
+ }
+}
+
.ide-status-file {
text-align: right;
-
- .ide-status-branch + &,
- &:first-child {
- margin-left: auto;
- }
}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6fc742871e7..6e98908eeed 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -124,6 +124,10 @@
float: left;
padding-left: $gl-padding-8;
}
+
+ .section-header ~ .section.line {
+ margin-left: $gl-padding;
+ }
}
.build-header {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index c386493231c..62fc7311d94 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -9,7 +9,6 @@
color: $gl-text-color;
}
- .issue_created_ago,
.author-link {
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 4ebf1019456..d2d35d91e0b 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,7 +14,7 @@
position: -webkit-sticky;
position: sticky;
top: $mr-file-header-top;
- z-index: 102;
+ z-index: 220;
&::before {
content: '';
@@ -1122,3 +1122,15 @@ table.code {
outline: 0;
}
}
+
+.diff-suggest-popover {
+ &.popover {
+ width: 250px;
+ min-width: 250px;
+ z-index: 210;
+ }
+
+ .popover-header {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 8cb3fab74e0..3917937f4af 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -904,7 +904,8 @@
margin-right: -5px;
}
-.deploy-heading {
+.deploy-heading,
+.merge-train-info {
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 49608a3964f..00d84df1650 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -230,27 +230,6 @@ $status-box-line-height: 26px;
background-color: $white-light;
}
-.milestone-deprecation-message {
- .popover {
- padding: 0;
- }
-
- .popover-body,
- .popover-content {
- padding: 0;
- }
-}
-
-.milestone-popover-body {
- padding: $gl-padding-8;
- background-color: $gray-light;
-}
-
-.milestone-popover-footer {
- padding: $gl-padding-8 $gl-padding;
- border-top: 1px solid $white-dark;
-}
-
.milestone-popover-instructions-list {
padding-left: 2em;
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index c03554b287f..2d600e3aef6 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -136,7 +136,6 @@
> .popover-header,
> .popover-body {
padding: 8px;
- font-size: 12px;
white-space: nowrap;
position: relative;
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index d5bc723aa8c..57b976b9121 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -2,7 +2,9 @@
class Admin::ApplicationSettingsController < Admin::ApplicationController
include InternalRedirect
+
before_action :set_application_setting
+ before_action :whitelist_query_limiting, only: [:usage_data]
def show
end
@@ -102,6 +104,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
@application_setting = Gitlab::CurrentSettings.current_application_settings
end
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/63107')
+ end
+
def application_setting_params
params[:application_setting] ||= {}
diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb
index ed7ea2f0e04..e4123d87137 100644
--- a/app/controllers/concerns/boards_actions.rb
+++ b/app/controllers/concerns/boards_actions.rb
@@ -35,4 +35,12 @@ module BoardsActions
boards.find(params[:id])
end
end
+
+ def serializer
+ BoardSerializer.new(current_user: current_user)
+ end
+
+ def serialize_as_json(resource)
+ serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
+ end
end
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index 8b191c86397..7625600e452 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -69,7 +69,7 @@ module BoardsResponses
end
def serialize_as_json(resource)
- resource.as_json(only: [:id])
+ serializer.represent(resource).as_json
end
def respond_with(resource)
@@ -80,4 +80,8 @@ module BoardsResponses
end
end
end
+
+ def serializer
+ BoardSerializer.new
+ end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e002a4d349b..ae46a234aa6 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -13,7 +13,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
- push_frontend_feature_flag(:grafana_dashboard_link)
push_frontend_feature_flag(:prometheus_computed_alerts)
end
@@ -165,7 +164,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
result = dashboard_finder.find(project, current_user, environment, params[:dashboard])
- result[:all_dashboards] = project.repository.metrics_dashboard_paths
+ result[:all_dashboards] = dashboard_finder.find_all_paths(project)
else
result = dashboard_finder.find(project, current_user, environment)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a3ceb76845e..9e7e3ed5afb 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -273,9 +273,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
if auto_merge_requested?
- AutoMergeService.new(project, current_user, merge_params)
- .execute(merge_request,
- params[:auto_merge_strategy] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ if merge_request.auto_merge_enabled?
+ # TODO: We should have a dedicated endpoint for updating merge params.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63130.
+ AutoMergeService.new(project, current_user, merge_params).update(merge_request)
+ else
+ AutoMergeService.new(project, current_user, merge_params)
+ .execute(merge_request,
+ params[:auto_merge_strategy] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ end
else
@merge_request.merge_async(current_user.id, merge_params)
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index 567d750caae..bf1d8d8b5fc 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -3,7 +3,7 @@
module Projects
module Registry
class TagsController < ::Projects::Registry::ApplicationController
- before_action :authorize_update_container_image!, only: [:destroy]
+ before_action :authorize_destroy_container_image!, only: [:destroy]
def index
respond_to do |format|
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index b5c77e5bbf4..5cfb0ac307d 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -5,10 +5,6 @@ module Projects
class OperationsController < Projects::ApplicationController
before_action :authorize_update_environment!
- before_action do
- push_frontend_feature_flag(:grafana_dashboard_link)
- end
-
helper_method :error_tracking_setting
def show
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index cb25548c83f..a80ab3bcd28 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -25,7 +25,6 @@ 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/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 2e5bdbd79c8..5615909c4ec 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -7,8 +7,8 @@ class GitlabSchema < GraphQL::Schema
AUTHENTICATED_COMPLEXITY = 250
ADMIN_COMPLEXITY = 300
- DEFAULT_MAX_DEPTH = 10
- AUTHENTICATED_MAX_DEPTH = 15
+ DEFAULT_MAX_DEPTH = 15
+ AUTHENTICATED_MAX_DEPTH = 20
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index dd5133189dc..f2365499eee 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -4,6 +4,8 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
+ implements(Types::Notes::NoteableType)
+
authorize :read_issue
expose_permissions Types::PermissionTypes::Issue
@@ -49,5 +51,7 @@ module Types
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
+
+ field :task_completion_status, Types::TaskCompletionStatus, null: false
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 85ac3102442..dac4c24cf10 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -4,6 +4,8 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
+ implements(Types::Notes::NoteableType)
+
authorize :read_merge_request
expose_permissions Types::PermissionTypes::MergeRequest
@@ -53,5 +55,7 @@ module Types
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
+
+ field :task_completion_status, Types::TaskCompletionStatus, null: false
end
end
diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb
new file mode 100644
index 00000000000..104ccb79bbb
--- /dev/null
+++ b/app/graphql/types/notes/diff_position_type.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class DiffPositionType < BaseObject
+ graphql_name 'DiffPosition'
+
+ field :head_sha, GraphQL::STRING_TYPE, null: false,
+ description: "The sha of the head at the time the comment was made"
+ field :base_sha, GraphQL::STRING_TYPE, null: true,
+ description: "The merge base of the branch the comment was made on"
+ field :start_sha, GraphQL::STRING_TYPE, null: false,
+ description: "The sha of the branch being compared against"
+
+ field :file_path, GraphQL::STRING_TYPE, null: false,
+ description: "The path of the file that was changed"
+ field :old_path, GraphQL::STRING_TYPE, null: true,
+ description: "The path of the file on the start sha."
+ field :new_path, GraphQL::STRING_TYPE, null: true,
+ description: "The path of the file on the head sha."
+ field :position_type, Types::Notes::PositionTypeEnum, null: false
+
+ # Fields for text positions
+ field :old_line, GraphQL::INT_TYPE, null: true,
+ description: "The line on start sha that was changed",
+ resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? }
+ field :new_line, GraphQL::INT_TYPE, null: true,
+ description: "The line on head sha that was changed",
+ resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? }
+
+ # Fields for image positions
+ field :x, GraphQL::INT_TYPE, null: true,
+ description: "The X postion on which the comment was made",
+ resolve: -> (position, _args, _ctx) { position.x if position.on_image? }
+ field :y, GraphQL::INT_TYPE, null: true,
+ description: "The Y position on which the comment was made",
+ resolve: -> (position, _args, _ctx) { position.y if position.on_image? }
+ field :width, GraphQL::INT_TYPE, null: true,
+ description: "The total width of the image",
+ resolve: -> (position, _args, _ctx) { position.width if position.on_image? }
+ field :height, GraphQL::INT_TYPE, null: true,
+ description: "The total height of the image",
+ resolve: -> (position, _args, _ctx) { position.height if position.on_image? }
+ end
+ end
+end
diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb
new file mode 100644
index 00000000000..c4691942f2d
--- /dev/null
+++ b/app/graphql/types/notes/discussion_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class DiscussionType < BaseObject
+ graphql_name 'Discussion'
+
+ authorize :read_note
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :created_at, Types::TimeType, null: false
+ field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion"
+ end
+ end
+end
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
new file mode 100644
index 00000000000..85c55d16ac2
--- /dev/null
+++ b/app/graphql/types/notes/note_type.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class NoteType < BaseObject
+ graphql_name 'Note'
+
+ authorize :read_note
+
+ expose_permissions Types::PermissionTypes::Note
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :project, Types::ProjectType,
+ null: true,
+ description: "The project this note is associated to",
+ resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find }
+
+ field :author, Types::UserType,
+ null: false,
+ description: "The user who wrote this note",
+ resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find }
+
+ field :resolved_by, Types::UserType,
+ null: true,
+ description: "The user that resolved the discussion",
+ resolve: -> (note, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.resolved_by_id).find }
+
+ field :system, GraphQL::BOOLEAN_TYPE,
+ null: false,
+ description: "Whether or not this note was created by the system or by a user"
+
+ field :body, GraphQL::STRING_TYPE,
+ null: false,
+ method: :note,
+ description: "The content note itself"
+
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :discussion, Types::Notes::DiscussionType, null: true, description: "The discussion this note is a part of"
+ field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, method: :resolvable?
+ field :resolved_at, Types::TimeType, null: true, description: "The time the discussion was resolved"
+ field :position, Types::Notes::DiffPositionType, null: true, description: "The position of this note on a diff"
+ end
+ end
+end
diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb
new file mode 100644
index 00000000000..9f126d67b0d
--- /dev/null
+++ b/app/graphql/types/notes/noteable_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ module NoteableType
+ include Types::BaseInterface
+
+ field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable"
+ field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable"
+
+ definition_methods do
+ def resolve_type(object, context)
+ case object
+ when Issue
+ Types::IssueType
+ when MergeRequest
+ Types::MergeRequestType
+ else
+ raise "Unknown GraphQL type for #{object}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/notes/position_type_enum.rb b/app/graphql/types/notes/position_type_enum.rb
new file mode 100644
index 00000000000..abdb2cfc804
--- /dev/null
+++ b/app/graphql/types/notes/position_type_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class PositionTypeEnum < BaseEnum
+ graphql_name 'DiffPositionType'
+ description 'Type of file the position refers to'
+
+ value 'text'
+ value 'image'
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/note.rb b/app/graphql/types/permission_types/note.rb
new file mode 100644
index 00000000000..a585d3daaa8
--- /dev/null
+++ b/app/graphql/types/permission_types/note.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Note < BasePermissionType
+ graphql_name 'NotePermissions'
+
+ abilities :read_note, :create_note, :admin_note, :resolve_note, :award_emoji
+ end
+ end
+end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index 62537361918..4000c6db280 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -4,6 +4,8 @@ module Types
class ProjectStatisticsType < BaseObject
graphql_name 'ProjectStatistics'
+ authorize :read_statistics
+
field :commit_count, GraphQL::INT_TYPE, null: false
field :storage_size, GraphQL::INT_TYPE, null: false
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 2236ffa394d..81914b70c7f 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -70,7 +70,7 @@ module Types
field :group, Types::GroupType, null: true
field :statistics, Types::ProjectStatisticsType,
- null: false,
+ null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
field :repository, Types::RepositoryType, null: false
diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb
new file mode 100644
index 00000000000..c289802509d
--- /dev/null
+++ b/app/graphql/types/task_completion_status.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class TaskCompletionStatus < BaseObject
+ graphql_name 'TaskCompletionStatus'
+ description 'Completion status of tasks'
+
+ field :count, GraphQL::INT_TYPE, null: false
+ field :completed_count, GraphQL::INT_TYPE, null: false
+ end
+end
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index ba191b59132..760781f3612 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -10,7 +10,7 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true
field :lfs_oid, GraphQL::STRING_TYPE, null: true, resolve: -> (blob, args, ctx) do
- Gitlab::Graphql::Loaders::BatchCommitLoader.new(blob.repository, blob.id).find
+ Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find
end
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 8d8c62f1291..64c5fae7d96 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -91,7 +91,7 @@ module DropdownsHelper
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
+ filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 8002eb08ada..0f118c235d8 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -26,7 +26,8 @@ module EnvironmentsHelper
"empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
- "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json),
+ "dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
+ "deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
"environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project),
"tags-path" => project_tags_path(project),
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index dce4168ad7b..bf894360a2e 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -263,6 +263,11 @@ module MarkupHelper
end
def asciidoc_unsafe(text, context = {})
+ context.merge!(
+ commit: @commit,
+ ref: @ref,
+ requested_path: @path
+ )
Gitlab::Asciidoc.render(text, context)
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 4594f5a31b9..dfa34ad7020 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -172,11 +172,17 @@ module SearchHelper
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
+ opts[:data]['labels-endpoint'] = project_labels_path(@project)
+ opts[:data]['milestones-endpoint'] = project_milestones_path(@project)
elsif @group.present?
opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group)
+ opts[:data]['labels-endpoint'] = group_labels_path(@group)
+ opts[:data]['milestones-endpoint'] = group_milestones_path(@group)
else
opts[:data]['base-endpoint'] = root_dashboard_path
+ opts[:data]['labels-endpoint'] = dashboard_labels_path
+ opts[:data]['milestones-endpoint'] = dashboard_milestones_path
end
opts
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 5d658d35107..d5e459311f7 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -3,6 +3,7 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -20,6 +21,10 @@ module UserCalloutsHelper
def render_dashboard_gold_trial(user)
end
+ def show_suggest_popover?
+ !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index a24d3476d0e..aa56ba1828b 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -15,7 +15,7 @@ class RepositoryCheckMailer < BaseMailer
end
mail(
- to: User.admins.pluck(:email),
+ to: User.admins.active.pluck(:email),
subject: "GitLab Admin | #{@message}"
)
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index fa0bf36ba49..be37fa2e76f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -94,7 +94,7 @@ class Commit
end
def lazy(project, oid)
- BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
+ BatchLoader.for({ project: project, oid: oid }).batch(replace_methods: false) do |items, loader|
items_by_project = items.group_by { |i| i[:project] }
items_by_project.each do |project, commit_ids|
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index b61bf29e6ad..2d09eff0111 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -3,6 +3,7 @@ module DiffPositionableNote
extend ActiveSupport::Concern
included do
+ delegate :on_text?, :on_image?, to: :position, allow_nil: true
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
@@ -28,14 +29,6 @@ module DiffPositionableNote
end
end
- def on_text?
- position&.position_type == "text"
- end
-
- def on_image?
- position&.position_type == "image"
- end
-
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 32529ebf71d..ae13cdfd85f 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -4,6 +4,7 @@
#
# A discussion of this type can be resolvable.
class Discussion
+ include GlobalID::Identification
include ResolvableDiscussion
attr_reader :notes, :context_noteable
@@ -11,14 +12,19 @@ class Discussion
delegate :created_at,
:project,
:author,
-
:noteable,
:commit_id,
:for_commit?,
:for_merge_request?,
+ :to_ability_name,
+ :editable?,
to: :first_note
+ def declarative_policy_delegate
+ first_note
+ end
+
def project_id
project&.id
end
diff --git a/app/models/group.rb b/app/models/group.rb
index cdb4e6e87f6..dbec211935d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -423,7 +423,7 @@ class Group < Namespace
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
- users.find_each(&:update_two_factor_requirement)
+ members_with_descendants.find_each(&:update_two_factor_requirement)
end
def path_changed_hook
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 5245dbc8d15..79a376ff0fd 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -5,7 +5,7 @@ class LfsObject < ApplicationRecord
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, through: :lfs_objects_projects
+ has_many :projects, -> { distinct }, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) }
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
index f9afb18c1d7..e45c56b6394 100644
--- a/app/models/lfs_objects_project.rb
+++ b/app/models/lfs_objects_project.rb
@@ -5,11 +5,17 @@ class LfsObjectsProject < ApplicationRecord
belongs_to :lfs_object
validates :lfs_object_id, presence: true
- validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
+ validates :lfs_object_id, uniqueness: { scope: [:project_id, :repository_type], message: "already exists in repository" }
validates :project_id, presence: true
after_commit :update_project_statistics, on: [:create, :destroy]
+ enum repository_type: {
+ project: 0,
+ wiki: 1,
+ design: 2 ## EE-specific
+ }
+
private
def update_project_statistics
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7481f1939ad..f07636e8f77 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -668,7 +668,7 @@ class MergeRequest < ApplicationRecord
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
- merge_request_diffs.create
+ merge_request_diffs.create!
reload_merge_request_diff
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 081d6f91230..15271c68a9e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -342,7 +342,7 @@ class Note < ApplicationRecord
end
def to_ability_name
- for_snippet? ? noteable.class.name.underscore : noteable_type.underscore
+ for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore
end
def can_be_discussion_note?
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 9b2bbb7eba5..a7f73c0f29c 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -101,6 +101,7 @@ class NotificationRecipient
end
def excluded_watcher_action?
+ return false unless @type == :watch
return false unless @custom_action
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
@@ -140,7 +141,7 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global?
- group_setting = closest_non_global_group_notification_settting
+ group_setting = closest_non_global_group_notification_setting
return group_setting unless group_setting.nil?
@@ -148,7 +149,7 @@ class NotificationRecipient
end
# Returns the notification_setting of the lowest group in hierarchy with non global level
- def closest_non_global_group_notification_settting
+ def closest_non_global_group_notification_setting
return unless @group
@group
diff --git a/app/models/project.rb b/app/models/project.rb
index e64a4b313aa..351d08eaf63 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -72,7 +72,6 @@ class Project < ApplicationRecord
delegate :no_import?, to: :import_state, allow_nil: true
default_value_for :archived, false
- default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility }
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
@@ -223,7 +222,7 @@ class Project < ApplicationRecord
has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
has_many :invited_groups, through: :project_group_links, source: :group
@@ -311,7 +310,7 @@ class Project < ApplicationRecord
delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
- delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings
+ delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
# Validations
validates :creator, presence: true, on: :create
@@ -613,6 +612,23 @@ class Project < ApplicationRecord
end
end
+ def initialize(attributes = {})
+ # We can't use default_value_for because the database has a default
+ # value of 0 for visibility_level. If someone attempts to create a
+ # private project, default_value_for will assume that the
+ # visibility_level hasn't changed and will use the application
+ # setting default, which could be internal or public. For projects
+ # inside a private group, those levels are invalid.
+ #
+ # To fix the problem, we assign the actual default in the application if
+ # no explicit visibility has been initialized.
+ unless visibility_attribute_present?(attributes)
+ attributes[:visibility_level] = Gitlab::CurrentSettings.default_project_visibility
+ end
+
+ super
+ end
+
def all_pipelines
if builds_enabled?
super
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 6bcb051bff6..0542581c6e0 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -72,8 +72,6 @@ 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/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
new file mode 100644
index 00000000000..438d85098c8
--- /dev/null
+++ b/app/models/project_services/data_fields.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module DataFields
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :issue_tracker_data
+ has_one :jira_tracker_data
+ end
+end
diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb
new file mode 100644
index 00000000000..2c1d28ed421
--- /dev/null
+++ b/app/models/project_services/issue_tracker_data.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class IssueTrackerData < ApplicationRecord
+ belongs_to :service
+
+ delegate :activated?, to: :service, allow_nil: true
+
+ validates :service, presence: true
+ validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
+ validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
+ validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated?
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :project_url, encryption_options
+ attr_encrypted :issues_url, encryption_options
+ attr_encrypted :new_issue_url, encryption_options
+end
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
new file mode 100644
index 00000000000..4f528e3d81b
--- /dev/null
+++ b/app/models/project_services/jira_tracker_data.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class JiraTrackerData < ApplicationRecord
+ belongs_to :service
+
+ delegate :activated?, to: :service, allow_nil: true
+
+ validates :service, presence: true
+ validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated?
+ validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true
+ validates :username, presence: true, if: :activated?
+ validates :password, presence: true, if: :activated?
+ validates :jira_issue_transition_id,
+ format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") },
+ allow_blank: true
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 9896aa12e90..40033003f3b 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -6,6 +6,7 @@ class Service < ApplicationRecord
include Sortable
include Importable
include ProjectServicesLoggable
+ include DataFields
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
@@ -119,7 +120,7 @@ class Service < ApplicationRecord
end
def self.event_names
- self.supported_events.map { |event| "#{event}_events" }
+ self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) }
end
def event_field(event)
@@ -151,7 +152,7 @@ class Service < ApplicationRecord
end
def self.supported_events
- %w(push tag_push issue confidential_issue merge_request wiki_page)
+ %w(commit push tag_push issue confidential_issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
index b9373ae6166..7b68e5076c7 100644
--- a/app/models/user_callout_enums.rb
+++ b/app/models/user_callout_enums.rb
@@ -6,11 +6,15 @@ module UserCalloutEnums
#
# This method is separate from the `UserCallout` model so that it can be
# extended by EE.
+ #
+ # If you are going to add new items to this hash, check that you're not going
+ # to conflict with EE-only values: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/ee/user_callout_enums.rb
def self.feature_names
{
gke_cluster_integration: 1,
gcp_signup_offer: 2,
- cluster_security_warning: 3
+ cluster_security_warning: 3,
+ suggest_popover_dismissed: 9
}
end
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 5dd2279ef99..82bf9bf8bf6 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -7,6 +7,10 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:admin) { @user&.admin? }
+ desc "User is blocked"
+ with_options scope: :user, score: 0
+ condition(:blocked) { @user&.blocked? }
+
desc "User has access to all private groups & projects"
with_options scope: :user, score: 0
condition(:full_private_access) { @user&.full_private_access? }
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index e85397422e6..134de1c9ace 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class GlobalPolicy < BasePolicy
- desc "User is blocked"
- with_options scope: :user, score: 0
- condition(:blocked) { @user&.blocked? }
-
desc "User is an internal user"
with_options scope: :user, score: 0
condition(:internal) { @user&.internal? }
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 728a3040227..b3e29e775fc 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -258,6 +258,7 @@ class ProjectPolicy < BasePolicy
enable :resolve_note
enable :create_container_image
enable :update_container_image
+ enable :destroy_container_image
enable :create_environment
enable :create_deployment
enable :create_release
@@ -446,6 +447,10 @@ class ProjectPolicy < BasePolicy
prevent :owner_access
end
+ rule { blocked }.policy do
+ prevent :create_pipeline
+ end
+
private
def team_member?
diff --git a/app/policies/project_statistics_policy.rb b/app/policies/project_statistics_policy.rb
new file mode 100644
index 00000000000..c0592f1ea13
--- /dev/null
+++ b/app/policies/project_statistics_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProjectStatisticsPolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index c75f62149f8..b928988ed8c 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -28,7 +28,7 @@ module Ci
if git_depth_variable
git_depth_variable[:value]
elsif Feature.enabled?(:ci_project_git_depth, default_enabled: true)
- project.default_git_depth
+ project.ci_default_git_depth
end.to_i
end
diff --git a/app/serializers/board_serializer.rb b/app/serializers/board_serializer.rb
new file mode 100644
index 00000000000..70a4c9ae282
--- /dev/null
+++ b/app/serializers/board_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class BoardSerializer < BaseSerializer
+ entity BoardSimpleEntity
+end
diff --git a/app/serializers/board_simple_entity.rb b/app/serializers/board_simple_entity.rb
new file mode 100644
index 00000000000..f297d993e27
--- /dev/null
+++ b/app/serializers/board_simple_entity.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class BoardSimpleEntity < Grape::Entity
+ expose :id
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index ec2698ecbe3..9ef93b2387f 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,7 +4,6 @@ class PipelineEntity < Grape::Entity
include RequestAwareEntity
expose :id
- expose :iid
expose :user, using: UserEntity
expose :active?, as: :active
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 7f0a41b3dfa..d726085b89a 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -20,6 +20,14 @@ module AutoMerge
strategy.to_sym
end
+ def update(merge_request)
+ merge_request.merge_params.merge!(params)
+
+ return :failed unless merge_request.save
+
+ strategy.to_sym
+ end
+
def cancel(merge_request)
if cancel_auto_merge(merge_request)
yield if block_given?
diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb
index a3a780ff388..926d2f5fc66 100644
--- a/app/services/auto_merge_service.rb
+++ b/app/services/auto_merge_service.rb
@@ -24,6 +24,12 @@ class AutoMergeService < BaseService
service.execute(merge_request)
end
+ def update(merge_request)
+ return :failed unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).update(merge_request)
+ end
+
def process(merge_request)
return unless merge_request.auto_merge_enabled?
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index fd5442a6c28..f2cd51ef4d0 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,7 +3,7 @@
module Files
class CreateService < Files::BaseService
def create_commit!
- transformer = Lfs::FileTransformer.new(project, @branch_name)
+ transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
result = transformer.new_file(@file_path, @file_content)
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index c1bc26c330a..d8c4e5bc5e8 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -5,7 +5,7 @@ module Files
UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze
def create_commit!
- transformer = Lfs::FileTransformer.new(project, @branch_name)
+ transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
actions = actions_after_lfs_transformation(transformer, params[:actions])
actions = transform_move_actions(actions)
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index d21a6bb1b9a..4aee48f22e7 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -20,8 +20,7 @@ module Git
strong_memoize(:commits) do
if creating_default_branch?
# The most recent PROCESS_COMMIT_LIMIT commits in the default branch
- offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max
- project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
+ project.repository.commits(params[:newrev], limit: PROCESS_COMMIT_LIMIT)
elsif creating_branch?
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually
@@ -84,9 +83,6 @@ module Git
# Schedules processing of commit messages
def enqueue_process_commit_messages
- # don't process commits for the initial push to the default branch
- return if creating_default_branch?
-
limited_commits.each do |commit|
next unless commit.matches_cross_reference_regex?
diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb
index 5239fe1b6e3..d1746399908 100644
--- a/app/services/lfs/file_transformer.rb
+++ b/app/services/lfs/file_transformer.rb
@@ -8,17 +8,17 @@ module Lfs
# pointer returned. If the file isn't in LFS the untransformed content
# is returned to save in the commit.
#
- # transformer = Lfs::FileTransformer.new(project, @branch_name)
+ # transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
# content_or_lfs_pointer = transformer.new_file(file_path, content).content
# create_transformed_commit(content_or_lfs_pointer)
#
class FileTransformer
- attr_reader :project, :branch_name
+ attr_reader :project, :repository, :repository_type, :branch_name
- delegate :repository, to: :project
-
- def initialize(project, branch_name)
+ def initialize(project, repository, branch_name)
@project = project
+ @repository = repository
+ @repository_type = repository.repo_type.name
@branch_name = branch_name
end
@@ -64,7 +64,11 @@ module Lfs
# rubocop: enable CodeReuse/ActiveRecord
def link_lfs_object!(lfs_object)
- project.lfs_objects << lfs_object
+ LfsObjectsProject.safe_find_or_create_by!(
+ project: project,
+ lfs_object: lfs_object,
+ repository_type: repository_type
+ )
end
def parse_file_content(file_content, encoding: nil)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 0b4ab7b8e4d..d8fa9d37359 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -43,10 +43,10 @@ module Projects
shared_runners_enabled: @project.shared_runners_enabled,
namespace_id: target_namespace.id,
fork_network: fork_network,
- # We need to set default_git_depth to 0 for the forked project when
- # @project.default_git_depth is nil in order to keep the same behaviour
+ # We need to set ci_default_git_depth to 0 for the forked project when
+ # @project.ci_default_git_depth is nil in order to keep the same behaviour
# and not get ProjectCiCdSetting::DEFAULT_GIT_DEPTH set on create
- ci_cd_settings_attributes: { default_git_depth: @project.default_git_depth || 0 },
+ ci_cd_settings_attributes: { default_git_depth: @project.ci_default_git_depth || 0 },
# We need to assign the fork network membership after the project has
# been instantiated to avoid ActiveRecord trying to create it when
# initializing the project, as that would cause a foreign key constraint
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 302510341ac..e0cbfac2420 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -52,10 +52,6 @@ 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/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 03ef2924617..c07bafbe302 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -8,6 +8,7 @@
.form-group
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
+ = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index db24c9982f7..a2aa1687f80 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -13,6 +13,8 @@
.settings-content
= render 'ci_cd'
+= render_if_exists 'admin/application_settings/required_instance_ci_setting', expanded: expanded_by_default?
+
- if Gitlab.config.registry.enabled
%section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 8745a4e9d3e..4cd03be572f 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -3,4 +3,4 @@
%a.nav-link.qa-sign-in-tab.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
%li.nav-item{ role: 'presentation' }
- %a.nav-link.qa-register-tab{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
+ %a.nav-link.qa-register-tab{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: 'sign_in', track_event: 'click_button', track_value: 'register', toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 0a14830c666..0da1f1ba7f5 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -17,6 +17,7 @@
%br
%span.descr.text-muted= share_with_group_lock_help_text(@group)
+ = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c357207054b..7535aee83a3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -78,3 +78,4 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render_if_exists 'layouts/snowplow'
+ = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test?
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 9b6551552c7..49ff976f8e8 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -9,7 +9,7 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
- = link_to project_path(@project), class: 'shortcuts-project' do
+ = link_to project_path(@project), class: 'shortcuts-project qa-link-project' do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 9f5241344a7..824fe3c791d 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -64,7 +64,7 @@
.home-panel-home-desc.mt-1
- if @project.description.present?
- .home-panel-description
+ .home-panel-description.text-break
.home-panel-description-markdown.read-more-container
= markdown_field(@project, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 1074cd6bf4e..a5eaae2dff4 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -22,7 +22,7 @@
%span.badge.badge-success.prepend-left-5
= s_('Branches|protected')
- = render_if_exists 'projects/branches/diverged_from_upstream'
+ = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch
.block-truncated
- if commit
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index bdf7b933ab8..f4560404c03 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -53,10 +53,9 @@
%span.badge.badge-info= _('manual')
- if pipeline_link
- %td.pipeline-link
- = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do
+ %td
+ = link_to pipeline_path(pipeline) do
%span.pipeline-id ##{pipeline.id}
- %span.pipeline-iid (##{pipeline.iid})
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 77ea2c04b28..a766dd51463 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -81,7 +81,7 @@
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
- = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)')
+ = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index a201fafb949..f593f4e049e 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -80,7 +80,9 @@
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
project_path: project_path(@merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'),
- is_fluid_layout: fluid_layout.to_s } }
+ is_fluid_layout: fluid_layout.to_s,
+ dismiss_endpoint: user_callouts_path,
+ show_suggest_popover: show_suggest_popover?.to_s } }
.mr-loading-status
= spinner
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 12eb8d7fa81..cb8a8a24be8 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 }.merge(@display_options)
+ = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- 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/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index d90a6d43761..d499bc0a253 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,4 +1,4 @@
-%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
+%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
%span.collapse-text= _("Collapse sidebar")
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3a5adb34ad1..e87e560266f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -102,7 +102,7 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
@@ -118,7 +118,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml
index 4a8f90937ea..acd90fa9178 100644
--- a/app/views/shared/milestones/_deprecation_message.html.haml
+++ b/app/views/shared/milestones/_deprecation_message.html.haml
@@ -11,4 +11,5 @@
%ol.milestone-popover-instructions-list.append-bottom-0
%li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
%li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
+ %hr.popover-hr
.milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'