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
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue19
-rw-r--r--app/assets/javascripts/behaviors/index.js22
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js4
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue21
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue27
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue23
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue21
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue8
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js15
-rw-r--r--app/assets/javascripts/header.js77
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue1
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/performance_bar/index.js8
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue27
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/stylesheets/framework/variables.scss26
-rw-r--r--app/controllers/projects/issues_controller.rb15
-rw-r--r--app/graphql/types/current_user_todos.rb24
-rw-r--r--app/graphql/types/design_management/design_type.rb1
-rw-r--r--app/graphql/types/issue_type.rb1
-rw-r--r--app/graphql/types/merge_request_type.rb1
-rw-r--r--app/graphql/types/todo_type.rb2
-rw-r--r--app/models/concerns/has_wiki.rb4
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_services/chat_message/merge_message.rb16
-rw-r--r--app/models/project_wiki.rb17
-rw-r--r--app/models/wiki.rb24
-rw-r--r--app/services/git/wiki_push_service.rb21
-rw-r--r--app/services/git/wiki_push_service/change.rb6
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml6
-rw-r--r--app/workers/post_receive.rb17
-rw-r--r--changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml5
-rw-r--r--changelogs/unreleased/241969-Replace-v-html.yml5
-rw-r--r--changelogs/unreleased/244298-rake-task-to-dump-usage-ping-sql-yaml-json.yml5
-rw-r--r--changelogs/unreleased/app-logger-16.yml5
-rw-r--r--changelogs/unreleased/approval-notification-bug.yml5
-rw-r--r--changelogs/unreleased/ntepluhina-refactor-design-management-to-gitlab-ui-1.yml5
-rw-r--r--config/initializers/direct_upload_support.rb6
-rw-r--r--config/webpack.config.js1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql194
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json466
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/operations/incident_management/index.md10
-rw-r--r--doc/user/application_security/api_fuzzing/index.md142
-rw-r--r--doc/user/application_security/dast/index.md6
-rw-r--r--doc/user/application_security/index.md3
-rw-r--r--doc/user/application_security/sast/index.md8
-rw-r--r--doc/user/application_security/threat_monitoring/index.md2
-rw-r--r--doc/user/compliance/license_compliance/index.md3
-rw-r--r--lib/backup/manager.rb10
-rw-r--r--lib/gitlab/usage_data_queries.rb22
-rw-r--r--lib/object_storage/config.rb20
-rw-r--r--lib/rspec_flaky/listener.rb6
-rw-r--r--lib/tasks/gitlab/cleanup.rake6
-rw-r--r--lib/tasks/gitlab/usage_data.rake13
-rw-r--r--locale/gitlab.pot9
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/merge_request/show.rb3
-rw-r--r--qa/qa/page/project/web_ide/edit.rb26
-rw-r--r--spec/frontend/alert_management/components/alert_details_spec.js6
-rw-r--r--spec/frontend/boards/boards_store_spec.js2
-rw-r--r--spec/frontend/boards/list_spec.js1
-rw-r--r--spec/frontend/boards/mock_data.js23
-rw-r--r--spec/frontend/boards/stores/actions_spec.js39
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js55
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js8
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js20
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js8
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/alert_detail_table_spec.js74
-rw-r--r--spec/graphql/types/current_user_todos_type_spec.rb9
-rw-r--r--spec/graphql/types/design_management/design_type_spec.rb4
-rw-r--r--spec/graphql/types/issue_type_spec.rb4
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb4
-rw-r--r--spec/lib/backup/manager_spec.rb23
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb21
-rw-r--r--spec/lib/object_storage/config_spec.rb41
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb3
-rw-r--r--spec/models/event_spec.rb7
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb69
-rw-r--r--spec/models/project_wiki_spec.rb13
-rw-r--r--spec/policies/project_policy_spec.rb73
-rw-r--r--spec/requests/api/graphql/current_user_todos_spec.rb81
-rw-r--r--spec/services/git/wiki_push_service_spec.rb38
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb6
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb57
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb12
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb28
-rw-r--r--spec/workers/post_receive_spec.rb6
-rw-r--r--yarn.lock8
108 files changed, 1819 insertions, 530 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index c1ebd234088..c6605452616 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -10,7 +10,6 @@ import {
GlTabs,
GlTab,
GlButton,
- GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql';
@@ -28,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -55,6 +55,7 @@ export default {
},
],
components: {
+ AlertDetailsTable,
GlBadge,
GlAlert,
GlIcon,
@@ -63,7 +64,6 @@ export default {
GlTab,
GlTabs,
GlButton,
- GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
@@ -331,20 +331,7 @@ export default {
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
- <gl-table
- class="alert-management-details-table"
- :items="[{ 'Full Alert Payload': 'Value', ...alert }]"
- :show-empty="true"
- :busy="loading"
- stacked
- >
- <template #empty>
- {{ s__('AlertManagement|No alert data to display.') }}
- </template>
- <template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
- </template>
- </gl-table>
+ <alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 8060938c72a..fd12c282b62 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,7 +1,7 @@
+import $ from 'jquery';
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
-import initGFMInput from './markdown/gfm_auto_complete';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
@@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi
import initSelect2Dropdowns from './select2';
installGlEmojiElement();
-initGFMInput();
+
initCopyAsGFM();
initCopyToClipboard();
+
initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 6bbd2133344..83f2ca0bdc2 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { parseBoolean } from '~/lib/utils/common_utils';
-export default function initGFMInput() {
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+export default function initGFMInput($els) {
+ $els.each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = parseBoolean(el.dataset.supportsAutocomplete);
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 9251af01aff..06f436adb8e 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlPopover, GlSprintf, GlButton } from '@gitlab/ui';
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
@@ -114,7 +113,7 @@ export default {
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
>
<template #title>
- <span v-html="suggestTitle"></span>
+ <span>{{ suggestTitle }}</span>
<span class="ml-auto">
<gl-button
:aria-label="__('Close')"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 34e8438ba4c..2817f9cb13d 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,13 @@
<script>
import $ from 'jquery';
+import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardNewIssue',
@@ -13,6 +15,7 @@ export default {
ProjectSelect,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
groupId: {
type: Number,
@@ -32,6 +35,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isSwimlanesOn']),
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
@@ -44,6 +48,7 @@ export default {
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
+ ...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return Promise.resolve();
@@ -70,21 +75,31 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
+ if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
+ this.addListIssue({ list: this.list, issue, position: 0 });
+ }
+
return this.list
.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
+ if (!this.glFeatures.boardsWithSwimlanes || !this.isSwimlanesOn) {
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
+ }
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
// Remove the issue
- this.list.removeIssue(issue);
+ if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
+ this.addListIssueFailure({ list: this.list, issue });
+ } else {
+ this.list.removeIssue(issue);
+ }
// Show error message
this.error = true;
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 4e808c809fb..8d9b58f71cb 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -235,6 +235,14 @@ export default {
notImplemented();
},
+ addListIssue: ({ commit }, { list, issue, position }) => {
+ commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
+ },
+
+ addListIssueFailure: ({ commit }, { list, issue }) => {
+ commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index f336d6d03c9..4fdbfbc36c5 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -15,6 +15,7 @@ import {
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import IssueProject from '../models/project';
@@ -303,7 +304,7 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
- if (list.issuesSize > 1) {
+ if (!gon.features.boardsWithSwimlanes && list.issuesSize > 1) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
@@ -710,6 +711,10 @@ const boardsStore = {
},
newIssue(id, issue) {
+ if (typeof id === 'string') {
+ id = getIdFromGraphQLId(id);
+ }
+
return axios.post(this.generateIssuesPath(id), {
issue,
});
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 12dd96380f6..a3b84108cb3 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -24,6 +24,8 @@ export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
+export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
+export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 837bccce091..f25c339836f 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { sortBy } from 'lodash';
+import { sortBy, pull } from 'lodash';
import * as mutationTypes from './mutation_types';
import { __ } from '~/locale';
@@ -8,6 +8,10 @@ const notImplemented = () => {
throw new Error('Not implemented!');
};
+const removeIssueFromList = (state, listId, issueId) => {
+ Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
+};
+
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, showPromotion, ...endpoints } = data;
@@ -131,6 +135,18 @@ export default {
notImplemented();
},
+ [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
+ const listIssues = state.issuesByListId[list.id];
+ listIssues.splice(position, 0, issue.id);
+ Vue.set(state.issuesByListId, list.id, listIssues);
+ Vue.set(state.issues, issue.id, issue);
+ },
+
+ [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
+ state.error = __('An error occurred while creating the issue. Please try again.');
+ removeIssueFromList(state, list.id, issue.id);
+ },
+
[mutationTypes.SET_CURRENT_PAGE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index f87bd695560..845f1aec8cf 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,6 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -27,6 +27,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -148,14 +149,14 @@ export default {
}
},
onCreateNoteError(err) {
- this.$emit('createNoteError', err);
+ this.$emit('create-note-error', err);
},
hideForm() {
this.isFormRendered = false;
this.discussionComment = '';
},
showForm() {
- this.$emit('openForm', this.discussion.id);
+ this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
@@ -167,11 +168,11 @@ export default {
})
.then(({ data }) => {
if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
+ this.$emit('resolve-discussion-error', data.errors[0]);
}
})
.catch(err => {
- this.$emit('resolveDiscussionError', err);
+ this.$emit('resolve-discussion-error', err);
})
.finally(() => {
this.isResolving = false;
@@ -192,13 +193,12 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
+ <gl-badge
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
:class="{ resolved: discussion.resolved }"
- type="button"
>
{{ discussion.index }}
- </div>
+ </gl-badge>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
@@ -208,7 +208,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- @error="$emit('updateNoteError', $event)"
+ @error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
@@ -216,7 +216,6 @@ export default {
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
- type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
@@ -252,7 +251,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- @error="$emit('updateNoteError', $event)"
+ @error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
@@ -275,8 +274,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 6c380153a3f..18444a2cc2f 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -18,6 +18,7 @@ export default {
DesignReplyForm,
ApolloMutation,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -83,27 +84,27 @@ export default {
:img-alt="author.username"
:img-size="40"
/>
- <div class="d-flex justify-content-between">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <a
+ <gl-link
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
- <span class="note-header-author-name bold">{{ author.name }}</span>
+ <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ </gl-link>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
- <a
+ <gl-link
class="note-timestamp system-note-separator gl-display-block gl-mb-2"
:href="`#note_${noteAnchorId}`"
>
<time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
+ </gl-link>
</span>
</div>
<div class="gl-display-flex">
@@ -122,7 +123,7 @@ export default {
</div>
<template v-if="!isEditing">
<div
- class="note-text js-note-text md"
+ class="note-text js-note-text"
data-qa-selector="note_content"
v-html="note.bodyHtml"
></div>
@@ -143,9 +144,9 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ class="gl-mt-5"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
/>
</apollo-mutation>
</timeline-entry-item>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 969034909f2..3754e1dbbc1 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default {
name: 'DesignReplyForm',
components: {
MarkdownField,
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
props: {
@@ -66,13 +66,13 @@ export default {
},
methods: {
submitForm() {
- if (this.hasValue) this.$emit('submitForm');
+ if (this.hasValue) this.$emit('submit-form');
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
this.$refs.cancelCommentModal.show();
} else {
- this.$emit('cancelForm');
+ this.$emit('cancel-form');
}
},
focusInput() {
@@ -112,20 +112,21 @@ export default {
</markdown-field>
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
+ <gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ category="primary"
variant="success"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
+ @click="$emit('submit-form')"
>
{{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ </gl-button>
+ <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
__('Cancel')
- }}</gl-deprecated-button>
+ }}</gl-button>
</div>
<gl-modal
ref="cancelCommentModal"
@@ -134,7 +135,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
+ @ok="$emit('cancel-form')"
>{{ modalSettings.content }}
</gl-modal>
</form>
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 9cfd2ea43a9..df425e3b96d 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -159,11 +159,11 @@ export default {
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 8a9911f55a3..c6225c516e2 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -372,8 +372,8 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
/> </apollo-mutation
></template>
</design-sidebar>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 36c586ddfd2..d57fde59db7 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -71,12 +71,15 @@ class GfmAutoComplete {
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
- // This triggers at.js again
- // Needed for quick actions with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ if (!$input.hasClass('js-gfm-input-initialized')) {
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
+ // This triggers at.js again
+ // Needed for quick actions with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ $input.addClass('js-gfm-input-initialized');
+ }
});
}
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 3f9163e924d..575d3618313 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -2,9 +2,6 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
-import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
-import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/**
@@ -26,51 +23,43 @@ export default function initTodoToggle() {
function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
- const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
- if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
- Vue.use(Translate);
+ if (setStatusModalTriggerEl) {
+ setStatusModalTriggerEl.addEventListener('click', () => {
+ import(
+ /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
+ )
+ .then(({ default: SetStatusModalWrapper }) => {
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+ const statusModalElement = document.createElement('div');
+ setStatusModalWrapperEl.appendChild(statusModalElement);
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalTriggerEl,
- data() {
- const { hasStatus } = this.$options.el.dataset;
+ Vue.use(Translate);
- return {
- hasStatus: parseBoolean(hasStatus),
- };
- },
- render(createElement) {
- return createElement(SetStatusModalTrigger, {
- props: {
- hasStatus: this.hasStatus,
- },
- });
- },
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalWrapperEl,
- data() {
- const { currentEmoji, currentMessage } = this.$options.el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: statusModalElement,
+ data() {
+ const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset;
- return {
- currentEmoji,
- currentMessage,
- };
- },
- render(createElement) {
- const { currentEmoji, currentMessage } = this;
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
- return createElement(SetStatusModalWrapper, {
- props: {
- currentEmoji,
- currentMessage,
- },
- });
- },
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ })
+ .catch(() => {});
});
}
}
@@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() {
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers);
- initNavUserDropdownTracking();
+ requestIdleCallback(initNavUserDropdownTracking);
});
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 0bfad0befb3..183816921c1 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -44,6 +44,7 @@ export default {
:aria-label="s__('IDE|Edit')"
data-container="body"
data-placement="right"
+ data-qa-selector="edit_mode_tab"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
@@ -78,8 +79,9 @@ export default {
:aria-label="s__('IDE|Commit')"
data-container="body"
data-placement="right"
+ data-qa-selector="commit_mode_tab"
type="button"
- class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
+ class="ide-sidebar-link js-ide-commit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
<gl-icon name="commit" />
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 77a7151e275..146e818d654 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -103,6 +103,7 @@ export default {
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
+ data-qa-selector="commit_sha_content"
>{{ lastCommit.short_id }}</a
>
by
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2ef0feaa2af..4e2770a24c2 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -31,7 +31,6 @@ import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
-import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
@@ -164,8 +163,6 @@ document.addEventListener('DOMContentLoaded', () => {
const $document = $(document);
const bootstrapBreakpoint = bp.getBreakpointSize();
- if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
-
initUserTracking();
initLayoutNav();
initAlertHandler();
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 88d513f6076..cfe674f9c52 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -380,7 +380,7 @@ export default {
dir="auto"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
+ class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 0ef2d5743b1..88b4461cf38 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -337,7 +337,7 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input"
dir="auto"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index ac10d99612c..f88314dabcf 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
-export default ({ container }) =>
+const initPerformanceBar = ({ container }) =>
new Vue({
el: container,
components: {
@@ -118,3 +118,9 @@ export default ({ container }) =>
});
},
});
+
+document.addEventListener('DOMContentLoaded', () => {
+ initPerformanceBar({ container: '#js-peek' });
+});
+
+export default initPerformanceBar;
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
deleted file mode 100644
index 0e8b6d93f42..00000000000
--- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import eventHub from './event_hub';
-
-export default {
- props: {
- hasStatus: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- buttonText() {
- return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
- },
- },
- methods: {
- openModal() {
- eventHub.$emit('openModal');
- },
- },
-};
-</script>
-
-<template>
- <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button>
-</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index a841cca8c95..09e893ff285 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -6,7 +6,6 @@ import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
import * as Emoji from '~/emoji';
@@ -48,15 +47,12 @@ export default {
},
},
mounted() {
- eventHub.$on('openModal', this.openModal);
+ this.$root.$emit('bv::show::modal', this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
- openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
- },
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
new file mode 100644
index 00000000000..2cd71669ecb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ tableHeader: {
+ [s__('AlertManagement|Full Alert Payload')]: s__('AlertManagement|Value'),
+ },
+ computed: {
+ items() {
+ if (!this.alert) {
+ return [];
+ }
+ return [{ ...this.$options.tableHeader, ...this.alert }];
+ },
+ },
+};
+</script>
+<template>
+ <gl-table
+ class="alert-management-details-table"
+ :busy="loading"
+ :empty-text="s__('AlertManagement|No alert data to display.')"
+ :items="items"
+ show-empty
+ stacked
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index f6d0fa9d8ed..f30676e8ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,4 +1,6 @@
<script>
+/* eslint-disable vue/no-v-html */
+
/**
* Common component to render a system note, icon and user information.
*
@@ -106,7 +108,7 @@ export default {
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
class="note system-note note-wrapper"
>
- <div v-safe-html="iconHtml" class="timeline-icon"></div>
+ <div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d1491fb50b0..cfe62d73e5d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -152,6 +152,18 @@ $red-800: #8d1300 !default;
$red-900: #660e00 !default;
$red-950: #4d0a00 !default;
+$purple-50: #f4f0ff !default;
+$purple-100: #e1d8f9 !default;
+$purple-200: #cbbbf2 !default;
+$purple-300: #ac93e6 !default;
+$purple-400: #9475db !default;
+$purple-500: #7b58cf !default;
+$purple-600: #694cc0 !default;
+$purple-700: #5943b6 !default;
+$purple-800: #453894 !default;
+$purple-900: #2f2a6b !default;
+$purple-950: #232150 !default;
+
$gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
@@ -221,6 +233,20 @@ $reds: (
'950': $red-950
);
+$purples: (
+ '50': $purple-50,
+ '100': $purple-100,
+ '200': $purple-200,
+ '300': $purple-300,
+ '400': $purple-400,
+ '500': $purple-500,
+ '600': $purple-600,
+ '700': $purple-700,
+ '800': $purple-800,
+ '900': $purple-900,
+ '950': $purple-950
+);
+
$grays: (
'10': $gray-10,
'50': $gray-50,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c2841c254eb..af1fc870f54 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,13 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
- def issue_except_actions
- %i[index calendar new create bulk_update import_csv export_csv service_desk]
- end
-
- def set_issuables_index_only_actions
- %i[index calendar service_desk]
- end
+ ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
+ SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
@@ -25,10 +20,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
- after_action :log_issue_show, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
+ before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+ after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
- before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) }
+ before_action :set_issuables_index, if: ->(c) { SET_ISSUEABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) }
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb
new file mode 100644
index 00000000000..e610286c1a9
--- /dev/null
+++ b/app/graphql/types/current_user_todos.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Interface to expose todos for the current_user on the `object`
+module Types
+ module CurrentUserTodos
+ include BaseInterface
+
+ field_class Types::BaseField
+
+ field :current_user_todos, Types::TodoType.connection_type,
+ description: 'Todos for the current user',
+ null: false do
+ argument :state, Types::TodoStateEnum,
+ description: 'State of the todos',
+ required: false
+ end
+
+ def current_user_todos(state: nil)
+ state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending`
+
+ TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb
index 3c84dc151bd..4e11a7aaf09 100644
--- a/app/graphql/types/design_management/design_type.rb
+++ b/app/graphql/types/design_management/design_type.rb
@@ -12,6 +12,7 @@ module Types
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
+ implements(Types::CurrentUserTodos)
field :versions,
Types::DesignManagement::VersionType.connection_type,
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index df789f3cf47..d6253f74ce5 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_issue
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 01b02b7976f..805ae111ff7 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_merge_request
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 08e7fabeb74..4f21da3d897 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -26,7 +26,7 @@ module Types
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
field :author, Types::UserType,
- description: 'The owner of this todo',
+ description: 'The author of this todo',
null: false,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
index 3e7cb940a62..df7bbe4dc08 100644
--- a/app/models/concerns/has_wiki.rb
+++ b/app/models/concerns/has_wiki.rb
@@ -25,10 +25,6 @@ module HasWiki
wiki.repository_exists?
end
- def after_wiki_activity
- true
- end
-
private
def check_wiki_path_conflict
diff --git a/app/models/project.rb b/app/models/project.rb
index d588cb791de..4c189197c99 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -3,7 +3,6 @@
require 'carrierwave/orm/activerecord'
class Project < ApplicationRecord
- extend ::Gitlab::Utils::Override
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
@@ -2470,11 +2469,6 @@ class Project < ApplicationRecord
jira_imports.last
end
- override :after_wiki_activity
- def after_wiki_activity
- touch(:last_activity_at, :last_repository_updated_at)
- end
-
def metrics_setting
super || build_metrics_setting
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index c4fcdff8386..b9916a54d75 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -5,6 +5,7 @@ module ChatMessage
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
+ attr_reader :action
attr_reader :state
attr_reader :title
@@ -16,6 +17,7 @@ module ChatMessage
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
@@ -63,11 +65,17 @@ module ChatMessage
"#{project_url}/-/merge_requests/#{merge_request_iid}"
end
- # overridden in EE
def state_or_action_text
- state
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
end
end
end
-
-ChatMessage::MergeMessage.prepend_if_ee('::EE::ChatMessage::MergeMessage')
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 5df0a33dc9a..bd570cf7ead 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -10,6 +10,23 @@ class ProjectWiki < Wiki
def disk_path(*args, &block)
container.disk_path + '.wiki'
end
+
+ override :after_wiki_activity
+ def after_wiki_activity
+ # Update activity columns, this is done synchronously to avoid
+ # replication delays in Geo.
+ project.touch(:last_activity_at, :last_repository_updated_at)
+ end
+
+ override :after_post_receive
+ def after_post_receive
+ # Update storage statistics
+ ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
+
+ # This call is repeated for post-receive, to make sure we're updating
+ # the activity columns for Git pushes as well.
+ after_wiki_activity
+ end
end
# TODO: Remove this once we implement ES support for group wikis.
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 22ae9b65564..9462f7401c4 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -133,8 +133,9 @@ class Wiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
false
@@ -144,16 +145,18 @@ class Wiki
commit = commit_details(:updated, message, page.title)
wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
end
def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
+ after_wiki_activity
- update_container_activity
+ true
end
def page_title_and_dir(title)
@@ -209,6 +212,17 @@ class Wiki
web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
end
+ # Callbacks for synchronous processing after wiki changes.
+ # These will be executed after any change made through GitLab itself (web UI and API),
+ # but not for Git pushes.
+ def after_wiki_activity
+ end
+
+ # Callbacks for background processing after wiki changes.
+ # These will be executed after any change to the wiki repository.
+ def after_post_receive
+ end
+
private
def commit_details(action, message = nil, title = nil)
@@ -225,10 +239,6 @@ class Wiki
def default_message(action, title)
"#{user.username} #{action} page: #{title}"
end
-
- def update_container_activity
- container.after_wiki_activity
- end
end
Wiki.prepend_if_ee('EE::Wiki')
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index f9de72f2d5f..fa3019ee9d6 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -5,7 +5,16 @@ module Git
# Maximum number of change events we will process on any single push
MAX_CHANGES = 100
+ attr_reader :wiki
+
+ def initialize(wiki, current_user, params)
+ @wiki, @current_user, @params = wiki, current_user, params.dup
+ end
+
def execute
+ # Execute model-specific callbacks
+ wiki.after_post_receive
+
process_changes
end
@@ -23,7 +32,11 @@ module Git
end
def can_process_wiki_events?
- Feature.enabled?(:wiki_events_on_git_push, project)
+ # TODO: Support activity events for group wikis
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/209306
+ return false unless wiki.is_a?(ProjectWiki)
+
+ Feature.enabled?(:wiki_events_on_git_push, wiki.container)
end
def push_changes
@@ -36,10 +49,6 @@ module Git
wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
end
- def wiki
- project.wiki
- end
-
def create_event_for(change)
event_service.execute(
change.last_known_slug,
@@ -54,7 +63,7 @@ module Git
end
def on_default_branch?(change)
- project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
end
# See: [Gitlab::GitPostReceive#changes]
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
index 562c43487e9..3d1d0fe8c4e 100644
--- a/app/services/git/wiki_push_service/change.rb
+++ b/app/services/git/wiki_push_service/change.rb
@@ -5,11 +5,11 @@ module Git
class Change
include Gitlab::Utils::StrongMemoize
- # @param [ProjectWiki] wiki
+ # @param [Wiki] wiki
# @param [Hash] change - must have keys `:oldrev` and `:newrev`
# @param [Gitlab::Git::RawDiffChange] raw_change
- def initialize(project_wiki, change, raw_change)
- @wiki, @raw_change, @change = project_wiki, raw_change, change
+ def initialize(wiki, change, raw_change)
+ @wiki, @raw_change, @change = wiki, raw_change, change
end
def page
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c861f475cac..1c87452f0a3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -70,6 +70,7 @@
= yield :page_specific_javascripts
= webpack_controller_bundle_tags
+ = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
= webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<84.0.4147.125"]) || browser.edge?([">=84", "<84.0.522.59"])
= yield :project_javascripts
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 4c659241f99..22c2be3b7da 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -14,7 +14,11 @@
%li.divider
- if can?(current_user, :update_user_status, current_user)
%li
- .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
+ %button.btn.menu-item.js-set-status-modal-trigger{ type: 'button' }
+ - if current_user.status.present?
+ = s_('SetStatusModal|Edit status')
+ - else
+ = s_('SetStatusModal|Set status')
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 8f844bd0b47..27dad744d0c 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -12,8 +12,8 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
def perform(gl_repository, identifier, changes, push_options = {})
container, project, repo_type = Gitlab::GlRepository.parse(gl_repository)
- if project.nil? && (!repo_type.snippet? || container.is_a?(ProjectSnippet))
- log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"")
+ if container.nil? || (container.is_a?(ProjectSnippet) && project.nil?)
+ log("Triggered hook for non-existing gl_repository \"#{gl_repository}\"")
return false
end
@@ -24,7 +24,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
if repo_type.wiki?
- process_wiki_changes(post_received, container)
+ process_wiki_changes(post_received, container.wiki)
elsif repo_type.project?
process_project_changes(post_received, container)
elsif repo_type.snippet?
@@ -59,18 +59,15 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
after_project_changes_hooks(project, user, changes.refs, changes.repository_data)
end
- def process_wiki_changes(post_received, project)
- project.touch(:last_activity_at, :last_repository_updated_at)
- project.wiki.repository.expire_statistics_caches
- ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
-
+ def process_wiki_changes(post_received, wiki)
user = identify_user(post_received)
return false unless user
# We only need to expire certain caches once per push
- expire_caches(post_received, project.wiki.repository)
+ expire_caches(post_received, wiki.repository)
+ wiki.repository.expire_statistics_caches
- ::Git::WikiPushService.new(project, user, changes: post_received.changes).execute
+ ::Git::WikiPushService.new(wiki, user, changes: post_received.changes).execute
end
def process_snippet_changes(post_received, snippet)
diff --git a/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml
new file mode 100644
index 00000000000..221279e8b19
--- /dev/null
+++ b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Expose the todos of the current user on relevant objects in GraphQL
+merge_request: 40555
+author:
+type: added
diff --git a/changelogs/unreleased/241969-Replace-v-html.yml b/changelogs/unreleased/241969-Replace-v-html.yml
new file mode 100644
index 00000000000..c36591fbed2
--- /dev/null
+++ b/changelogs/unreleased/241969-Replace-v-html.yml
@@ -0,0 +1,5 @@
+---
+title: Replace v-html with v-safe-html in popover.vue
+merge_request: 41197
+author: Kev @KevSlashNull
+type: other
diff --git a/changelogs/unreleased/244298-rake-task-to-dump-usage-ping-sql-yaml-json.yml b/changelogs/unreleased/244298-rake-task-to-dump-usage-ping-sql-yaml-json.yml
new file mode 100644
index 00000000000..36c37910c2a
--- /dev/null
+++ b/changelogs/unreleased/244298-rake-task-to-dump-usage-ping-sql-yaml-json.yml
@@ -0,0 +1,5 @@
+---
+title: Rake task to generate raw SQLs for usage ping
+merge_request: 41091
+author:
+type: added
diff --git a/changelogs/unreleased/app-logger-16.yml b/changelogs/unreleased/app-logger-16.yml
new file mode 100644
index 00000000000..fd18816f537
--- /dev/null
+++ b/changelogs/unreleased/app-logger-16.yml
@@ -0,0 +1,5 @@
+---
+title: Use AppLogger in listener.rb, cleaner.rake, helpers.rb and spec files
+merge_request: 41116
+author: Rajendra Kadam
+type: other
diff --git a/changelogs/unreleased/approval-notification-bug.yml b/changelogs/unreleased/approval-notification-bug.yml
new file mode 100644
index 00000000000..a48537b45f3
--- /dev/null
+++ b/changelogs/unreleased/approval-notification-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge request chat messages for adding and removing approvals
+merge_request: 41775
+author:
+type: fixed
diff --git a/changelogs/unreleased/ntepluhina-refactor-design-management-to-gitlab-ui-1.yml b/changelogs/unreleased/ntepluhina-refactor-design-management-to-gitlab-ui-1.yml
new file mode 100644
index 00000000000..c97634d4992
--- /dev/null
+++ b/changelogs/unreleased/ntepluhina-refactor-design-management-to-gitlab-ui-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update design discussions to use GitLab UI components
+merge_request: 41686
+author:
+type: other
diff --git a/config/initializers/direct_upload_support.rb b/config/initializers/direct_upload_support.rb
index 94e90727f0c..919b80b79c0 100644
--- a/config/initializers/direct_upload_support.rb
+++ b/config/initializers/direct_upload_support.rb
@@ -1,5 +1,7 @@
class DirectUploadsValidator
- SUPPORTED_DIRECT_UPLOAD_PROVIDERS = %w(Google AWS AzureRM).freeze
+ SUPPORTED_DIRECT_UPLOAD_PROVIDERS = [ObjectStorage::Config::GOOGLE_PROVIDER,
+ ObjectStorage::Config::AWS_PROVIDER,
+ ObjectStorage::Config::AZURE_PROVIDER].freeze
ValidationError = Class.new(StandardError)
@@ -24,7 +26,7 @@ class DirectUploadsValidator
def provider_loaded?(provider)
return false unless SUPPORTED_DIRECT_UPLOAD_PROVIDERS.include?(provider)
- require 'fog/azurerm' if provider == 'AzureRM'
+ require 'fog/azurerm' if provider == ObjectStorage::Config::AZURE_PROVIDER
true
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index f0fcadf77bb..652ada1d832 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -79,6 +79,7 @@ function generateEntries() {
const manualEntries = {
default: defaultEntries,
sentry: './sentry/index.js',
+ performance_bar: './performance_bar/index.js',
chrome_84_icon_fix: './lib/chrome_84_icon_fix.js',
};
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c54b04c0d42..6f3897d1574 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2944,6 +2944,38 @@ type CreateSnippetPayload {
snippet: Snippet
}
+interface CurrentUserTodos {
+ """
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+}
+
"""
Autogenerated input type of DastOnDemandScanCreate
"""
@@ -3501,7 +3533,37 @@ type DeleteJobsResponse {
"""
A single design
"""
-type Design implements DesignFields & Noteable {
+type Design implements CurrentUserTodos & DesignFields & Noteable {
+ """
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+
"""
The diff refs for this design
"""
@@ -4896,7 +4958,7 @@ type EnvironmentEdge {
"""
Represents an epic.
"""
-type Epic implements Noteable {
+type Epic implements CurrentUserTodos & Noteable {
"""
Author of the epic
"""
@@ -5000,6 +5062,36 @@ type Epic implements Noteable {
createdAt: Time
"""
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+
+ """
Number of open and closed descendant epics and issues
"""
descendantCounts: EpicDescendantCount
@@ -5438,7 +5530,7 @@ type EpicHealthStatus {
"""
Relationship between an epic and an issue
"""
-type EpicIssue implements Noteable {
+type EpicIssue implements CurrentUserTodos & Noteable {
"""
Alert associated to this issue
"""
@@ -5495,6 +5587,36 @@ type EpicIssue implements Noteable {
createdAt: Time!
"""
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+
+ """
Description of the issue
"""
description: String
@@ -7328,7 +7450,7 @@ enum IssuableState {
opened
}
-type Issue implements Noteable {
+type Issue implements CurrentUserTodos & Noteable {
"""
Alert associated to this issue
"""
@@ -7385,6 +7507,36 @@ type Issue implements Noteable {
createdAt: Time!
"""
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+
+ """
Description of the issue
"""
description: String
@@ -8964,7 +9116,7 @@ type MemberInterfaceEdge {
node: MemberInterface
}
-type MergeRequest implements Noteable {
+type MergeRequest implements CurrentUserTodos & Noteable {
"""
Indicates if members of the target project can push to the fork
"""
@@ -9041,6 +9193,36 @@ type MergeRequest implements Noteable {
createdAt: Time!
"""
+ Todos for the current user
+ """
+ currentUserTodos(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ State of the todos
+ """
+ state: TodoStateEnum
+ ): TodoConnection!
+
+ """
Default merge commit message of the merge request
"""
defaultMergeCommitMessage: String
@@ -16234,7 +16416,7 @@ type Todo {
action: TodoActionEnum!
"""
- The owner of this todo
+ The author of this todo
"""
author: User!
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index de62bf43923..44405c21f6a 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -7986,6 +7986,110 @@
"possibleTypes": null
},
{
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "description": null,
+ "fields": [
+ {
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": [
+ {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicIssue",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ }
+ ]
+ },
+ {
"kind": "INPUT_OBJECT",
"name": "DastOnDemandScanCreateInput",
"description": "Autogenerated input type of DastOnDemandScanCreate",
@@ -9561,6 +9665,73 @@
"description": "A single design",
"fields": [
{
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "diffRefs",
"description": "The diff refs for this design",
"args": [
@@ -9939,6 +10110,11 @@
"kind": "INTERFACE",
"name": "DesignFields",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "ofType": null
}
],
"enumValues": null,
@@ -13999,6 +14175,73 @@
"deprecationReason": null
},
{
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "descendantCounts",
"description": "Number of open and closed descendant epics and issues",
"args": [
@@ -14777,6 +15020,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "ofType": null
}
],
"enumValues": null,
@@ -15376,6 +15624,73 @@
"deprecationReason": null
},
{
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "description",
"description": "Description of the issue",
"args": [
@@ -16155,6 +16470,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "ofType": null
}
],
"enumValues": null,
@@ -20372,6 +20692,73 @@
"deprecationReason": null
},
{
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "description",
"description": "Description of the issue",
"args": [
@@ -21123,6 +21510,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "ofType": null
}
],
"enumValues": null,
@@ -25131,6 +25523,73 @@
"deprecationReason": null
},
{
+ "name": "currentUserTodos",
+ "description": "Todos for the current user",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todos",
+ "type": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "defaultMergeCommitMessage",
"description": "Default merge commit message of the merge request",
"args": [
@@ -26288,6 +26747,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "CurrentUserTodos",
+ "ofType": null
}
],
"enumValues": null,
@@ -47766,7 +48230,7 @@
},
{
"name": "author",
- "description": "The owner of this todo",
+ "description": "The author of this todo",
"args": [
],
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index bc61283cc33..1089bfb6aad 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2374,7 +2374,7 @@ Representing a todo entry
| Name | Type | Description |
| --- | ---- | ---------- |
| `action` | TodoActionEnum! | Action of the todo |
-| `author` | User! | The owner of this todo |
+| `author` | User! | The author of this todo |
| `body` | String! | Body of the todo |
| `createdAt` | Time! | Timestamp this todo was created |
| `group` | Group | Group this todo is associated with |
diff --git a/doc/operations/incident_management/index.md b/doc/operations/incident_management/index.md
index 1575c48492e..28e69a6bbfe 100644
--- a/doc/operations/incident_management/index.md
+++ b/doc/operations/incident_management/index.md
@@ -74,8 +74,8 @@ team members can join the Zoom call without requesting a link.
For information about GitLab and incident management, see:
-- [Generic alerts](./generic_alerts.md)
-- [Alerts](./alerts.md)
-- [Alert details](./alert_details.md)
-- [Incidents](./incidents.md)
-- [Status page](./status_page.md)
+- [Generic alerts](generic_alerts.md)
+- [Alerts](alerts.md)
+- [Alert details](alert_details.md)
+- [Incidents](incidents.md)
+- [Status page](status_page.md)
diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md
index 2a9c0910a13..3b2ddb32e28 100644
--- a/doc/user/application_security/api_fuzzing/index.md
+++ b/doc/user/application_security/api_fuzzing/index.md
@@ -606,36 +606,36 @@ Example profile definition:
```yaml
Profiles:
-- Name: Quick-10
- DefaultProfile: Quick
- Routes:
- - Route: *Route0
- Checks:
- - Name: FormBodyFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: GeneralFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: JsonFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: XmlFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
+ - Name: Quick-10
+ DefaultProfile: Quick
+ Routes:
+ - Route: *Route0
+ Checks:
+ - Name: FormBodyFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: GeneralFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: JsonFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: XmlFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
```
To turn off the General Fuzzing Check you can remove these lines:
```yaml
- - Name: GeneralFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
+- Name: GeneralFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
```
This results in the following YAML:
@@ -644,20 +644,20 @@ This results in the following YAML:
- Name: Quick-10
DefaultProfile: Quick
Routes:
- - Route: *Route0
- Checks:
- - Name: FormBodyFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: JsonFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: XmlFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
+ - Route: *Route0
+ Checks:
+ - Name: FormBodyFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: JsonFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: XmlFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
```
### Turn off an Assertion for a Check
@@ -671,14 +671,14 @@ This example shows the FormBody Fuzzing Check:
```yaml
Checks:
-- Name: FormBodyFuzzingCheck
- Configuration:
- FuzzingCount: 30
- UnicodeFuzzing: true
- Assertions:
- - Name: LogAnalysisAssertion
- - Name: ResponseAnalysisAssertion
- - Name: StatusCodeAssertion
+ - Name: FormBodyFuzzingCheck
+ Configuration:
+ FuzzingCount: 30
+ UnicodeFuzzing: true
+ Assertions:
+ - Name: LogAnalysisAssertion
+ - Name: ResponseAnalysisAssertion
+ - Name: StatusCodeAssertion
```
Here you can see three Assertions are on by default. A common source of false positives is
@@ -688,30 +688,30 @@ example provides only the other two Assertions (`LogAnalysisAssertion`,
```yaml
Profiles:
-- Name: Quick-10
- DefaultProfile: Quick
- Routes:
- - Route: *Route0
- Checks:
- - Name: FormBodyFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- Assertions:
- - Name: LogAnalysisAssertion
- - Name: ResponseAnalysisAssertion
- - Name: GeneralFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: JsonFuzzingCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
- - Name: XmlInjectionCheck
- Configuration:
- FuzzingCount: 10
- UnicodeFuzzing: true
+ - Name: Quick-10
+ DefaultProfile: Quick
+ Routes:
+ - Route: *Route0
+ Checks:
+ - Name: FormBodyFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ Assertions:
+ - Name: LogAnalysisAssertion
+ - Name: ResponseAnalysisAssertion
+ - Name: GeneralFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: JsonFuzzingCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
+ - Name: XmlInjectionCheck
+ Configuration:
+ FuzzingCount: 10
+ UnicodeFuzzing: true
```
<!--
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 14860a39b07..17529d57e59 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -177,9 +177,9 @@ include:
variables:
DAST_WEBSITE: https://example.com
DAST_AUTH_URL: https://example.com/sign-in
- DAST_USERNAME_FIELD: session[user] # the name of username field at the sign-in HTML form
- DAST_PASSWORD_FIELD: session[password] # the name of password field at the sign-in HTML form
- DAST_AUTH_EXCLUDE_URLS: http://example.com/sign-out,http://example.com/sign-out-2 # optional, URLs to skip during the authenticated scan; comma-separated, no spaces in between
+ DAST_USERNAME_FIELD: session[user] # the name of username field at the sign-in HTML form
+ DAST_PASSWORD_FIELD: session[password] # the name of password field at the sign-in HTML form
+ DAST_AUTH_EXCLUDE_URLS: http://example.com/sign-out,http://example.com/sign-out-2 # optional, URLs to skip during the authenticated scan; comma-separated, no spaces in between
```
The results are saved as a
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 332ab81cbcc..edc0310667c 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -166,7 +166,8 @@ reports. You can specify the list of all headers to be masked. For details, see
### Dismissing a vulnerability
-To dismiss a vulnerability, you must set its status to Dismissed. Follow these steps to do so:
+To dismiss a vulnerability, you must set its status to Dismissed. This dismisses the vulnerability
+for the entire project. Follow these steps to do so:
1. Select the vulnerability in the Security Dashboard.
1. Select **Dismissed** from the **Status** selector menu at the top-right.
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index d14efb3e6a5..2b717157259 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -244,8 +244,8 @@ analyzer and compilation will be skipped:
image: maven:3.6-jdk-8-alpine
stages:
- - build
- - test
+ - build
+ - test
include:
- template: SAST.gitlab-ci.yml
@@ -523,13 +523,13 @@ For details on saving and transporting Docker images as a file, see Docker's doc
Add the following configuration to your `.gitlab-ci.yml` file. You must replace
`SECURE_ANALYZERS_PREFIX` to refer to your local Docker container registry:
- ```yaml
+```yaml
include:
- template: SAST.gitlab-ci.yml
variables:
SECURE_ANALYZERS_PREFIX: "localhost:5000/analyzers"
- ```
+```
The SAST job should now use local copies of the SAST analyzers to scan your code and generate
security reports without requiring internet access.
diff --git a/doc/user/application_security/threat_monitoring/index.md b/doc/user/application_security/threat_monitoring/index.md
index c916cdbfe7c..0fdd244a8f6 100644
--- a/doc/user/application_security/threat_monitoring/index.md
+++ b/doc/user/application_security/threat_monitoring/index.md
@@ -66,7 +66,7 @@ global:
enabled: true
metrics:
enabled:
- - 'flow:sourceContext=namespace;destinationContext=namespace'
+ - 'flow:sourceContext=namespace;destinationContext=namespace'
```
The **Container Network Policy** section displays the following information
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index af76ff7061e..d51b6484fd9 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -268,8 +268,7 @@ You can supply a custom root certificate to complete TLS verification by using t
#### Using private Python repos
If you have a private Python repository you can use the `PIP_INDEX_URL` [environment variable](#available-variables)
-to specify its location. It's also possible to provide a custom `pip.conf` for
-[additional configuration](#custom-root-certificates-for-python).
+to specify its location.
### Configuring NPM projects
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 6bce9a2760d..2b28b30fd74 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -47,7 +47,7 @@ module Backup
return
end
- directory = connect_to_remote_directory(connection_settings)
+ directory = connect_to_remote_directory(Gitlab.config.backup.upload)
if directory.files.create(create_attributes)
progress.puts "done".color(:green)
@@ -195,9 +195,11 @@ module Backup
@backup_file_list.map {|item| item.gsub("#{FILE_NAME_SUFFIX}", "")}
end
- def connect_to_remote_directory(connection_settings)
- # our settings use string keys, but Fog expects symbols
- connection = ::Fog::Storage.new(connection_settings.symbolize_keys)
+ def connect_to_remote_directory(options)
+ config = ObjectStorage::Config.new(options)
+ config.load_provider
+
+ connection = ::Fog::Storage.new(config.credentials)
# We only attempt to create the directory for local backups. For AWS
# and other cloud providers, we cannot guarantee the user will have
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
new file mode 100644
index 00000000000..f38050ceee6
--- /dev/null
+++ b/lib/gitlab/usage_data_queries.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class UsageDataQueries < UsageData
+ class << self
+ def count(relation, column = nil, *rest)
+ raw_sql(relation, column)
+ end
+
+ def distinct_count(relation, column = nil, *rest)
+ raw_sql(relation, column, :distinct)
+ end
+
+ private
+
+ def raw_sql(relation, column, distinct = nil)
+ column ||= relation.primary_key
+ relation.select(relation.all.table[column].count(distinct)).to_sql
+ end
+ end
+ end
+end
diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb
index e91fda29880..cc536ce9b46 100644
--- a/lib/object_storage/config.rb
+++ b/lib/object_storage/config.rb
@@ -2,12 +2,26 @@
module ObjectStorage
class Config
+ AWS_PROVIDER = 'AWS'
+ AZURE_PROVIDER = 'AzureRM'
+ GOOGLE_PROVIDER = 'Google'
+
attr_reader :options
def initialize(options)
@options = options.to_hash.deep_symbolize_keys
end
+ def load_provider
+ if aws?
+ require 'fog/aws'
+ elsif google?
+ require 'fog/google'
+ elsif azure?
+ require 'fog/azurerm'
+ end
+ end
+
def credentials
@credentials ||= options[:connection] || {}
end
@@ -30,7 +44,7 @@ module ObjectStorage
# AWS-specific options
def aws?
- provider == 'AWS'
+ provider == AWS_PROVIDER
end
def use_iam_profile?
@@ -61,11 +75,11 @@ module ObjectStorage
# End Azure-specific options
def google?
- provider == 'Google'
+ provider == GOOGLE_PROVIDER
end
def azure?
- provider == 'AzureRM'
+ provider == AZURE_PROVIDER
end
def fog_attributes
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index 37e4e16e87e..e0c0aacfe4e 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -32,21 +32,19 @@ module RspecFlaky
flaky_examples[current_example.uid] = flaky_example
end
- # rubocop:disable Gitlab/RailsLogger
def dump_summary(_)
RspecFlaky::Report.new(flaky_examples).write(RspecFlaky::Config.flaky_examples_report_path)
# write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
- Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn Gitlab::Json.pretty_generate(new_flaky_examples.to_h)
+ Gitlab::AppLogger.warn "\nNew flaky examples detected:\n"
+ Gitlab::AppLogger.warn Gitlab::Json.pretty_generate(new_flaky_examples.to_h)
RspecFlaky::Report.new(new_flaky_examples).write(RspecFlaky::Config.new_flaky_examples_report_path)
# write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
- # rubocop:enable Gitlab/RailsLogger
private
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index a56a0435673..7e76db93158 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -178,19 +178,17 @@ namespace :gitlab do
end
end
- # rubocop:disable Gitlab/RailsLogger
def logger
return @logger if defined?(@logger)
@logger = if Rails.env.development? || Rails.env.production?
Logger.new(STDOUT).tap do |stdout_logger|
- stdout_logger.extend(ActiveSupport::Logger.broadcast(Rails.logger))
+ stdout_logger.extend(ActiveSupport::Logger.broadcast(Gitlab::AppLogger))
stdout_logger.level = debug? ? Logger::DEBUG : Logger::INFO
end
else
- Rails.logger
+ Gitlab::AppLogger
end
end
- # rubocop:enable Gitlab/RailsLogger
end
end
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
new file mode 100644
index 00000000000..6f3db91c2b0
--- /dev/null
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :usage_data do
+ desc 'GitLab | UsageData | Generate raw SQLs for usage ping in YAML'
+ task dump_sql_in_yaml: :environment do
+ puts Gitlab::UsageDataQueries.uncached_data.to_yaml
+ end
+
+ desc 'GitLab | UsageData | Generate raw SQLs for usage ping in JSON'
+ task dump_sql_in_json: :environment do
+ puts Gitlab::Json.pretty_generate(Gitlab::UsageDataQueries.uncached_data)
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4f703dfb2c9..173eb2da353 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2165,6 +2165,9 @@ msgstr ""
msgid "AlertManagement|Events"
msgstr ""
+msgid "AlertManagement|Full Alert Payload"
+msgstr ""
+
msgid "AlertManagement|High"
msgstr ""
@@ -2267,6 +2270,9 @@ msgstr ""
msgid "AlertManagement|Unknown"
msgstr ""
+msgid "AlertManagement|Value"
+msgstr ""
+
msgid "AlertManagement|View alerts in Opsgenie"
msgstr ""
@@ -2621,6 +2627,9 @@ msgstr ""
msgid "An error occurred while committing your changes."
msgstr ""
+msgid "An error occurred while creating the issue. Please try again."
+msgstr ""
+
msgid "An error occurred while creating the list. Please try again."
msgstr ""
diff --git a/package.json b/package.json
index 0f0b08e8d5b..31746279459 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.164.0",
- "@gitlab/ui": "20.19.0",
+ "@gitlab/ui": "20.20.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.22.3",
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index f85340e1086..8431e47831e 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -275,7 +275,8 @@ module QA
end
def click_open_in_web_ide
- click_element :open_in_web_ide_button
+ click_element(:open_in_web_ide_button)
+ wait_for_requests
end
end
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index b962b0c673b..56c8d343cf5 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -10,6 +10,11 @@ module QA
view 'app/assets/javascripts/ide/components/activity_bar.vue' do
element :commit_mode_tab
+ element :edit_mode_tab
+ end
+
+ view 'app/assets/javascripts/ide/components/ide_status_bar.vue' do
+ element :commit_sha_content
end
view 'app/assets/javascripts/ide/components/ide_tree.vue' do
@@ -104,11 +109,19 @@ module QA
end
end
+ def commit_sha
+ return unless has_element?(:commit_sha_content, wait: 0)
+
+ find_element(:commit_sha_content).text
+ end
+
def commit_changes(open_merge_request: false)
# Clicking :begin_commit_button switches from the
# edit to the commit view
- click_element :begin_commit_button
- active_element? :commit_mode_tab
+ click_element(:begin_commit_button)
+ active_element?(:commit_mode_tab)
+
+ original_commit = commit_sha
# After clicking :begin_commit_button, there is an animation
# that hides :begin_commit_button and shows :commit_button
@@ -126,16 +139,17 @@ module QA
# Click :commit_button and keep retrying just in case part of the
# animation is still in process even when the buttons have the
# expected visibility.
- commit_success_msg_shown = retry_until(sleep_interval: 5) do
+ commit_success = retry_until(sleep_interval: 5) do
click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio)
click_element(:commit_button) if has_element?(:commit_button)
- wait_until(reload: false) do
- has_text?('Your changes have been committed')
+ # If this is the first commit, the commit SHA only appears after reloading
+ wait_until(reload: true) do
+ active_element?(:edit_mode_tab) && commit_sha != original_commit
end
end
- raise "The changes do not appear to have been committed successfully." unless commit_success_msg_shown
+ raise "The changes do not appear to have been committed successfully." unless commit_success
end
end
diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/alert_management/components/alert_details_spec.js
index 504aebd078f..8aa26dbca3b 100644
--- a/spec/frontend/alert_management/components/alert_details_spec.js
+++ b/spec/frontend/alert_management/components/alert_details_spec.js
@@ -1,7 +1,8 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
@@ -22,8 +23,6 @@ describe('AlertDetails', () => {
const projectId = '1';
const $router = { replace: jest.fn() };
- const findDetailsTable = () => wrapper.find(GlTable);
-
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, {
provide: {
@@ -66,6 +65,7 @@ describe('AlertDetails', () => {
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findViewIncidentBtn = () => wrapper.find('[data-testid="viewIncidentBtn"]');
const findIncidentCreationAlert = () => wrapper.find('[data-testid="incidentCreationError"]');
+ const findDetailsTable = () => wrapper.find(AlertDetailsTable);
describe('Alert details', () => {
describe('when alert is null', () => {
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 29cc8f981bd..41971137b95 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -312,7 +312,7 @@ describe('boardsStore', () => {
});
describe('newIssue', () => {
- const id = 'not-creative';
+ const id = 1;
const issue = { some: 'issue data' };
const url = `${endpoints.listsEndpoint}/${id}/issues`;
const expectedRequest = expect.objectContaining({
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index b731bb6e474..9c3a6e66ef4 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -184,6 +184,7 @@ describe('List model', () => {
}),
);
list.issues = [];
+ global.gon.features = { boardsWithSwimlanes: false };
});
it('adds new issue to top of list', done => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 7461e9dc0e9..8d20db9f24a 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -117,6 +117,29 @@ export const mockIssue = {
],
};
+export const mockIssue2 = {
+ title: 'Planning',
+ id: 2,
+ iid: 2,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'plan',
+ color: 'blue',
+ description: 'planning',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+};
+
export const BoardsMockData = {
GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index ab38abd15ba..17792a8f597 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,5 +1,5 @@
import testAction from 'helpers/vuex_action_helper';
-import { mockListsWithModel } from '../mock_data';
+import { mockListsWithModel, mockLists, mockIssue } from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId, ListType } from '~/boards/constants';
@@ -236,6 +236,43 @@ describe('createNewIssue', () => {
expectNotImplemented(actions.createNewIssue);
});
+describe('addListIssue', () => {
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ const payload = {
+ list: mockLists[0],
+ issue: mockIssue,
+ position: 0,
+ };
+
+ testAction(
+ actions.addListIssue,
+ payload,
+ {},
+ [{ type: types.ADD_ISSUE_TO_LIST, payload }],
+ [],
+ done,
+ );
+ });
+});
+
+describe('addListIssueFailure', () => {
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ const payload = {
+ list: mockLists[0],
+ issue: mockIssue,
+ };
+
+ testAction(
+ actions.addListIssueFailure,
+ payload,
+ {},
+ [{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }],
+ [],
+ done,
+ );
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index f80236afacd..2927337f455 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,7 +1,14 @@
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import { listObj, listObjDuplicate, mockIssue, mockListsWithModel } from '../mock_data';
+import {
+ listObj,
+ listObjDuplicate,
+ mockIssue,
+ mockIssue2,
+ mockListsWithModel,
+ mockLists,
+} from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -148,7 +155,7 @@ describe('Board Store Mutations', () => {
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
it('sets isLoadingIssues to false and updates issuesByListId object', () => {
const listIssues = {
- '1': [mockIssue.id],
+ '': [mockIssue.id],
};
const issues = {
'1': mockIssue,
@@ -264,6 +271,50 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
});
+ describe('ADD_ISSUE_TO_LIST', () => {
+ it('adds issue to issues state and issue id in list in issuesByListId', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ issues,
+ };
+
+ mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 });
+
+ expect(state.issuesByListId['gid://gitlab/List/1']).toContain(mockIssue2.id);
+ expect(state.issues[mockIssue2.id]).toEqual(mockIssue2);
+ });
+ });
+
+ describe('ADD_ISSUE_TO_LIST_FAILURE', () => {
+ it('removes issue id from list in issuesByListId', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ '2': mockIssue2,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ issues,
+ };
+
+ mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
+
+ expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
+ });
+ });
+
describe('SET_CURRENT_PAGE', () => {
expectNotImplemented(mutations.SET_CURRENT_PAGE);
});
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index d20d81c5230..e1db42aeb0b 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -17,15 +17,15 @@ exports[`Design note component should match the snapshot 1`] = `
/>
<div
- class="d-flex justify-content-between"
+ class="gl-display-flex gl-justify-content-space-between"
>
<div>
- <a
+ <gl-link-stub
class="js-user-link"
data-user-id="author-id"
>
<span
- class="note-header-author-name bold"
+ class="note-header-author-name gl-font-weight-bold"
>
</span>
@@ -37,7 +37,7 @@ exports[`Design note component should match the snapshot 1`] = `
>
@
</span>
- </a>
+ </gl-link-stub>
<span
class="note-headline-light note-headline-meta"
@@ -46,7 +46,7 @@ exports[`Design note component should match the snapshot 1`] = `
class="system-note-message"
/>
- <a
+ <gl-link-stub
class="note-timestamp system-note-separator gl-display-block gl-mb-2"
href="#note_123"
>
@@ -55,7 +55,7 @@ exports[`Design note component should match the snapshot 1`] = `
time="2019-07-26T15:02:20Z"
tooltipplacement="bottom"
/>
- </a>
+ </gl-link-stub>
</span>
</div>
@@ -68,7 +68,7 @@ exports[`Design note component should match the snapshot 1`] = `
</div>
<div
- class="note-text js-note-text md"
+ class="note-text js-note-text"
data-qa-selector="note_content"
/>
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index e01c79e3520..f8c68ca4c83 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -1,15 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!---->
- Comment
-</button>"
+ <!----> <span class=\\"gl-button-text\\">
+ Comment
+ </span></button>"
`;
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!---->
- Save comment
-</button>"
+ <!----> <span class=\\"gl-button-text\\">
+ Save comment
+ </span></button>"
`;
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 98f190bc33a..9fbd9b2c2a3 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -232,7 +232,7 @@ describe('Design discussions component', () => {
{ discussionComment: 'test', isFormRendered: true },
);
- findReplyForm().vm.$emit('submitForm');
+ findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(mutationVariables);
await mutate();
@@ -250,7 +250,7 @@ describe('Design discussions component', () => {
return wrapper.vm
.$nextTick()
.then(() => {
- findReplyForm().vm.$emit('cancelForm');
+ findReplyForm().vm.$emit('cancel-form');
expect(wrapper.vm.discussionComment).toBe('');
return wrapper.vm.$nextTick();
@@ -321,6 +321,6 @@ describe('Design discussions component', () => {
createComponent();
findReplyPlaceholder().vm.$emit('onClick');
- expect(wrapper.emitted('openForm')).toBeTruthy();
+ expect(wrapper.emitted('open-form')).toBeTruthy();
});
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index c35bb503c96..043091e3dc2 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -133,8 +133,8 @@ describe('Design note component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('hides the form on hideForm event', () => {
- findReplyForm().vm.$emit('cancelForm');
+ it('hides the form on cancel-form event', () => {
+ findReplyForm().vm.$emit('cancel-form');
return wrapper.vm.$nextTick().then(() => {
expect(findReplyForm().exists()).toBe(false);
@@ -142,8 +142,8 @@ describe('Design note component', () => {
});
});
- it('calls a mutation on submitForm event and hides a form', () => {
- findReplyForm().vm.$emit('submitForm');
+ it('calls a mutation on submit-form event and hides a form', () => {
+ findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalled();
return mutate()
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index 16b34f150b8..1a80fc4e761 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -70,7 +70,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
});
@@ -80,20 +80,20 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
});
it('emits cancelForm event on pressing escape button on textarea', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('emits cancelForm event on clicking Cancel button', () => {
findCancelButton().vm.$emit('click');
- expect(wrapper.emitted('cancelForm')).toHaveLength(1);
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
});
@@ -112,7 +112,7 @@ describe('Design reply form component', () => {
findSubmitButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -122,7 +122,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -132,7 +132,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -147,7 +147,7 @@ describe('Design reply form component', () => {
it('emits cancelForm event on Escape key if text was not changed', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('opens confirmation modal on Escape key when text has changed', () => {
@@ -162,7 +162,7 @@ describe('Design reply form component', () => {
it('emits cancelForm event on Cancel button click if text was not changed', () => {
findCancelButton().trigger('click');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('opens confirmation modal on Cancel button click when text has changed', () => {
@@ -178,7 +178,7 @@ describe('Design reply form component', () => {
findTextarea().trigger('keyup.esc');
findModal().vm.$emit('ok');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
});
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 85513e08d46..700faa8a70f 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -154,22 +154,22 @@ describe('Design management design sidebar component', () => {
});
it('emits correct event on discussion create note error', () => {
- findFirstDiscussion().vm.$emit('createNoteError', 'payload');
+ findFirstDiscussion().vm.$emit('create-note-error', 'payload');
expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]);
});
it('emits correct event on discussion update note error', () => {
- findFirstDiscussion().vm.$emit('updateNoteError', 'payload');
+ findFirstDiscussion().vm.$emit('update-note-error', 'payload');
expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]);
});
it('emits correct event on discussion resolve error', () => {
- findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload');
+ findFirstDiscussion().vm.$emit('resolve-discussion-error', 'payload');
expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]);
});
it('changes prop correctly on opening discussion form', () => {
- findFirstDiscussion().vm.$emit('openForm', 'some-id');
+ findFirstDiscussion().vm.$emit('open-form', 'some-id');
return wrapper.vm.$nextTick().then(() => {
expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id');
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index d189bdb4345..d78d3dc7edd 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -210,7 +210,7 @@ describe('Design management design index page', () => {
},
);
- findDiscussionForm().vm.$emit('submitForm');
+ findDiscussionForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
return wrapper.vm
@@ -235,7 +235,7 @@ describe('Design management design index page', () => {
},
);
- findDiscussionForm().vm.$emit('cancelForm');
+ findDiscussionForm().vm.$emit('cancel-form');
expect(wrapper.vm.comment).toBe('');
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index f8c71d76968..3b101e9e815 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -41,7 +41,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
>
<textarea
aria-label="Description"
- class="note-textarea js-gfm-input js-autosize markdown-area"
+ class="note-textarea js-gfm-input js-autosize markdown-area js-gfm-input-initialized"
data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
dir="auto"
diff --git a/spec/frontend/vue_shared/components/alert_detail_table_spec.js b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
new file mode 100644
index 00000000000..9c38ccad8a7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
@@ -0,0 +1,74 @@
+import { mount } from '@vue/test-utils';
+import { GlTable, GlLoadingIcon } from '@gitlab/ui';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+const mockAlert = {
+ iid: '1527542',
+ title: 'SyntaxError: Invalid or unexpected token',
+ severity: 'CRITICAL',
+ eventCount: 7,
+ createdAt: '2020-04-17T23:18:14.996Z',
+ startedAt: '2020-04-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ status: 'TRIGGERED',
+ assignees: { nodes: [] },
+ notes: { nodes: [] },
+ todos: { nodes: [] },
+};
+
+describe('AlertDetails', () => {
+ let wrapper;
+
+ function mountComponent(propsData = {}) {
+ wrapper = mount(AlertDetailsTable, {
+ propsData: {
+ alert: mockAlert,
+ loading: false,
+ ...propsData,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTableComponent = () => wrapper.find(GlTable);
+
+ describe('Alert details', () => {
+ describe('empty state', () => {
+ beforeEach(() => {
+ mountComponent({ alert: null });
+ });
+
+ it('shows an empty state when no alert is provided', () => {
+ expect(wrapper.text()).toContain('No alert data to display.');
+ });
+ });
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mountComponent({ loading: true });
+ });
+
+ it('displays a loading state when loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('with table data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders a table', () => {
+ expect(findTableComponent().exists()).toBe(true);
+ });
+
+ it('renders a cell based on alert data', () => {
+ expect(findTableComponent().text()).toContain('SyntaxError: Invalid or unexpected token');
+ });
+ });
+ });
+});
diff --git a/spec/graphql/types/current_user_todos_type_spec.rb b/spec/graphql/types/current_user_todos_type_spec.rb
new file mode 100644
index 00000000000..a0015e96788
--- /dev/null
+++ b/spec/graphql/types/current_user_todos_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CurrentUserTodos'] do
+ specify { expect(described_class.graphql_name).to eq('CurrentUserTodos') }
+
+ specify { expect(described_class).to have_graphql_fields(:current_user_todos).only }
+end
diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb
index 7a38b397965..cae98a013e1 100644
--- a/spec/graphql/types/design_management/design_type_spec.rb
+++ b/spec/graphql/types/design_management/design_type_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Design'] do
+ specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) }
+
it_behaves_like 'a GraphQL type with design fields' do
- let(:extra_design_fields) { %i[notes discussions versions] }
+ let(:extra_design_fields) { %i[notes current_user_todos discussions versions] }
let_it_be(:design) { create(:design, :with_versions) }
let(:object_id) { GitlabSchema.id_from_object(design) }
let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) }
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index db2a1751be0..c55e624dd11 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -11,11 +11,13 @@ RSpec.describe GitlabSchema.types['Issue'] do
specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
+ specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) }
+
it 'has specific fields' do
fields = %i[id iid title description state reference author assignees participants labels milestone due_date
confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position
subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status
- designs design_collection alert_management_alert severity]
+ designs design_collection alert_management_alert severity current_user_todos]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index a9a74114dda..1279f01f104 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
+ specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) }
+
it 'has the expected fields' do
expected_fields = %w[
notes discussions user_permissions id iid title title_html description
@@ -24,7 +26,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
source_branch_exists target_branch_exists
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
- total_time_spent reference author merged_at commit_count
+ total_time_spent reference author merged_at commit_count current_user_todos
]
if Gitlab.ee?
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 38a5c30506b..feaca6164eb 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -416,5 +416,28 @@ RSpec.describe Backup::Manager do
subject.upload
end
end
+
+ context 'with AzureRM provider' do
+ before do
+ stub_backup_setting(
+ upload: {
+ connection: {
+ provider: 'AzureRM',
+ azure_storage_account_name: 'test-access-id',
+ azure_storage_access_key: 'secret'
+ },
+ remote_directory: 'directory',
+ multipart_chunk_size: nil,
+ encryption: nil,
+ encryption_key: nil,
+ storage_class: nil
+ }
+ )
+ end
+
+ it 'loads the provider' do
+ expect { subject.upload }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index 57bde6262a9..155e66e2fcd 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Deployment do
describe '.build' do
it 'returns the object kind for a deployment' do
- deployment = build(:deployment)
+ deployment = build(:deployment, deployable: nil, environment: create(:environment))
data = described_class.build(deployment)
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
new file mode 100644
index 00000000000..06f3174dd34
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataQueries do
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ end
+
+ describe '.count' do
+ it 'returns the raw SQL' do
+ expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')
+ end
+ end
+
+ describe '.distinct_count' do
+ it 'returns the raw SQL' do
+ expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
+ end
+ end
+end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index a48b5100065..0ead2a1d269 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -2,6 +2,7 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'fog/core'
RSpec.describe ObjectStorage::Config do
using RSpec::Parameterized::TableSyntax
@@ -35,6 +36,46 @@ RSpec.describe ObjectStorage::Config do
subject { described_class.new(raw_config.as_json) }
+ describe '#load_provider' do
+ before do
+ subject.load_provider
+ end
+
+ context 'with AWS' do
+ it 'registers AWS as a provider' do
+ expect(Fog.providers.keys).to include(:aws)
+ end
+ end
+
+ context 'with Google' do
+ let(:credentials) do
+ {
+ provider: 'Google',
+ google_storage_access_key_id: 'GOOGLE_ACCESS_KEY_ID',
+ google_storage_secret_access_key: 'GOOGLE_SECRET_ACCESS_KEY'
+ }
+ end
+
+ it 'registers Google as a provider' do
+ expect(Fog.providers.keys).to include(:google)
+ end
+ end
+
+ context 'with Azure' do
+ let(:credentials) do
+ {
+ provider: 'AzureRM',
+ azure_storage_account_name: 'azuretest',
+ azure_storage_access_key: 'ABCD1234'
+ }
+ end
+
+ it 'registers AzureRM as a provider' do
+ expect(Fog.providers.keys).to include(:azurerm)
+ end
+ end
+ end
+
describe '#credentials' do
it { expect(subject.credentials).to eq(credentials) }
end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index 2920bbf2b58..3b903fe34f9 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -61,7 +61,8 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
describe 'namespace uniqueness validation' do
- let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
+ let_it_be(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster: cluster, namespace: 'my-namespace') }
subject { kubernetes_namespace }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index ff53478fea3..bafcb7a3741 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -81,6 +81,8 @@ RSpec.describe Event do
describe 'validations' do
describe 'action' do
context 'for a design' do
+ let_it_be(:author) { create(:user) }
+
where(:action, :valid) do
valid = described_class::DESIGN_ACTIONS.map(&:to_s).to_set
@@ -90,7 +92,7 @@ RSpec.describe Event do
end
with_them do
- let(:event) { build(:design_event, action: action) }
+ let(:event) { build(:design_event, author: author, action: action) }
specify { expect(event.valid?).to eq(valid) }
end
@@ -731,7 +733,8 @@ RSpec.describe Event do
end
target = kind == :project ? nil : build(kind, **extra_data)
- [kind, build(:event, :created, project: project, target: target)]
+
+ [kind, build(:event, :created, author: project.owner, project: project, target: target)]
end.to_h
end
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index 45be5212508..02b266e4fae 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -29,23 +29,6 @@ RSpec.describe ChatMessage::MergeMessage do
}
end
- # Integration point in EE
- context 'when state is overridden' do
- it 'respects the overridden state' do
- allow(subject).to receive(:state_or_action_text) { 'devoured' }
-
- aggregate_failures do
- expect(subject.summary).not_to include('opened')
- expect(subject.summary).to include('devoured')
-
- activity_title = subject.activity[:title]
-
- expect(activity_title).not_to include('opened')
- expect(activity_title).to include('devoured')
- end
- end
- end
-
context 'without markdown' do
let(:color) { '#345' }
@@ -106,4 +89,56 @@ RSpec.describe ChatMessage::MergeMessage do
end
end
end
+
+ context 'approved' do
+ before do
+ args[:object_attributes][:action] = 'approved'
+ end
+
+ it 'returns a message regarding completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) approved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproved' do
+ before do
+ args[:object_attributes][:action] = 'unapproved'
+ end
+
+ it 'returns a message regarding revocation of completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) unapproved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'approval' do
+ before do
+ args[:object_attributes][:action] = 'approval'
+ end
+
+ it 'returns a message regarding added approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) added their approval to merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproval' do
+ before do
+ args[:object_attributes][:action] = 'unapproval'
+ end
+
+ it 'returns a message regarding revoking approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) removed their approval from merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index d9c5fed542e..29c3d0e1a73 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -17,19 +17,28 @@ RSpec.describe ProjectWiki do
end
end
- describe '#update_container_activity' do
+ describe '#after_wiki_activity' do
it 'updates project activity' do
wiki_container.update!(
last_activity_at: nil,
last_repository_updated_at: nil
)
- subject.create_page('Test Page', 'This is content')
+ subject.send(:after_wiki_activity)
wiki_container.reload
expect(wiki_container.last_activity_at).to be_within(1.minute).of(Time.current)
expect(wiki_container.last_repository_updated_at).to be_within(1.minute).of(Time.current)
end
end
+
+ describe '#after_post_receive' do
+ it 'updates project activity and expires caches' do
+ expect(wiki).to receive(:after_wiki_activity)
+ expect(ProjectCacheWorker).to receive(:perform_async).with(wiki_container.id, [], [:wiki_size])
+
+ subject.send(:after_post_receive)
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 9879fc53461..af17434b4f2 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe ProjectPolicy do
include ExternalAuthorizationServiceHelpers
include_context 'ProjectPolicy context'
+
let_it_be(:other_user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
@@ -14,78 +15,6 @@ RSpec.describe ProjectPolicy do
let_it_be(:admin) { create(:admin) }
let(:project) { create(:project, :public, namespace: owner.namespace) }
- let(:base_guest_permissions) do
- %i[
- read_project read_board read_list read_wiki read_issue
- read_project_for_iids read_issue_iid read_label
- read_milestone read_snippet read_project_member read_note
- create_project create_issue create_note upload_file create_merge_request_in
- award_emoji read_release read_issue_link
- ]
- end
-
- let(:base_reporter_permissions) do
- %i[
- download_code fork_project create_snippet update_issue
- admin_issue admin_label admin_list read_commit_status read_build
- read_container_image read_pipeline read_environment read_deployment
- read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation
- metrics_dashboard read_confidential_issues admin_issue_link
- ]
- end
-
- let(:team_member_reporter_permissions) do
- %i[build_download_code build_read_container_image]
- end
-
- let(:developer_permissions) do
- %i[
- admin_tag admin_milestone admin_merge_request update_merge_request create_commit_status
- update_commit_status create_build update_build create_pipeline
- update_pipeline create_merge_request_from create_wiki push_code
- resolve_note create_container_image update_container_image destroy_container_image daily_statistics
- create_environment update_environment create_deployment update_deployment create_release update_release
- create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation
- read_terraform_state read_pod_logs
- ]
- end
-
- let(:base_maintainer_permissions) do
- %i[
- push_to_delete_protected_branch update_snippet
- admin_snippet admin_project_member admin_note admin_wiki admin_project
- admin_commit_status admin_build admin_container_image
- admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- read_deploy_token create_deploy_token destroy_deploy_token
- admin_terraform_state
- ]
- end
-
- let(:public_permissions) do
- %i[
- download_code fork_project read_commit_status read_pipeline
- read_container_image build_download_code build_read_container_image
- download_wiki_code read_release
- ]
- end
-
- let(:owner_permissions) do
- %i[
- change_namespace change_visibility_level rename_project remove_project
- archive_project remove_fork_project destroy_merge_request destroy_issue
- set_issue_iid set_issue_created_at set_issue_updated_at set_note_created_at
- ]
- end
-
- # Used in EE specs
- let(:additional_guest_permissions) { [] }
- let(:additional_reporter_permissions) { [] }
- let(:additional_maintainer_permissions) { [] }
-
- let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
- let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
- let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions }
-
before do
project.add_guest(guest)
project.add_maintainer(maintainer)
diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb
new file mode 100644
index 00000000000..b657f15d0e9
--- /dev/null
+++ b/spec/requests/api/graphql/current_user_todos_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:todoable) { create(:issue, project: project) }
+ let_it_be(:done_todo) { create(:todo, state: :done, target: todoable, user: current_user) }
+ let_it_be(:pending_todo) { create(:todo, state: :pending, target: todoable, user: current_user) }
+ let(:state) { 'null' }
+
+ let(:todoable_response) do
+ graphql_data_at(:project, :issue, :currentUserTodos, :nodes)
+ end
+
+ let(:query) do
+ <<~GQL
+ {
+ project(fullPath: "#{project.full_path}") {
+ issue(iid: "#{todoable.iid}") {
+ currentUserTodos(state: #{state}) {
+ nodes {
+ #{all_graphql_fields_for('Todo', max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns todos of the current user' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(done_todo)),
+ a_hash_including('id' => global_id_of(pending_todo))
+ )
+ end
+
+ it 'does not return todos of another user', :aggregate_failures do
+ post_graphql(query, current_user: create(:user))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(todoable_response).to be_empty
+ end
+
+ it 'does not error when there is no logged in user', :aggregate_failures do
+ post_graphql(query)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(todoable_response).to be_empty
+ end
+
+ context 'when `state` argument is `pending`' do
+ let(:state) { 'pending' }
+
+ it 'returns just the pending todo' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(pending_todo))
+ )
+ end
+ end
+
+ context 'when `state` argument is `done`' do
+ let(:state) { 'done' }
+
+ it 'returns just the done todo' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(done_todo))
+ )
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index 7f709be8593..816f20f0bc3 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -6,12 +6,20 @@ RSpec.describe Git::WikiPushService, services: true do
include RepoHelpers
let_it_be(:key_id) { create(:key, user: current_user).shell_id }
- let_it_be(:project) { create(:project, :wiki_repo) }
- let_it_be(:current_user) { create(:user) }
- let_it_be(:git_wiki) { project.wiki.wiki }
- let_it_be(:repository) { git_wiki.repository }
+ let_it_be(:wiki) { create(:project_wiki) }
+ let_it_be(:current_user) { wiki.container.default_owner }
+ let_it_be(:git_wiki) { wiki.wiki }
+ let_it_be(:repository) { wiki.repository }
describe '#execute' do
+ it 'executes model-specific callbacks' do
+ expect(wiki).to receive(:after_post_receive)
+
+ create_service(current_sha).execute
+ end
+ end
+
+ describe '#process_changes' do
context 'the push contains more than the permitted number of changes' do
def run_service
process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } }
@@ -37,8 +45,8 @@ RSpec.describe Git::WikiPushService, services: true do
let(:count) { Event::WIKI_ACTIONS.size }
def run_service
- wiki_page_a = create(:wiki_page, project: project)
- wiki_page_b = create(:wiki_page, project: project)
+ wiki_page_a = create(:wiki_page, wiki: wiki)
+ wiki_page_b = create(:wiki_page, wiki: wiki)
process_changes do
write_new_page
@@ -135,7 +143,7 @@ RSpec.describe Git::WikiPushService, services: true do
end
context 'when a page we already know about has been updated' do
- let(:wiki_page) { create(:wiki_page, project: project) }
+ let(:wiki_page) { create(:wiki_page, wiki: wiki) }
before do
create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page)
@@ -165,7 +173,7 @@ RSpec.describe Git::WikiPushService, services: true do
context 'when a page we do not know about has been updated' do
def run_service
- wiki_page = create(:wiki_page, project: project)
+ wiki_page = create(:wiki_page, wiki: wiki)
process_changes { update_page(wiki_page.title) }
end
@@ -189,7 +197,7 @@ RSpec.describe Git::WikiPushService, services: true do
context 'when a page we do not know about has been deleted' do
def run_service
- wiki_page = create(:wiki_page, project: project)
+ wiki_page = create(:wiki_page, wiki: wiki)
process_changes { delete_page(wiki_page.page.path) }
end
@@ -254,9 +262,9 @@ RSpec.describe Git::WikiPushService, services: true do
it_behaves_like 'a no-op push'
- context 'but is enabled for a given project' do
+ context 'but is enabled for a given container' do
before do
- stub_feature_flags(wiki_events_on_git_push: project)
+ stub_feature_flags(wiki_events_on_git_push: wiki.container)
end
it 'creates events' do
@@ -280,19 +288,19 @@ RSpec.describe Git::WikiPushService, services: true do
def create_service(base, refs = ['refs/heads/master'])
changes = post_received(base, refs).changes
- described_class.new(project, current_user, changes: changes)
+ described_class.new(wiki, current_user, changes: changes)
end
def post_received(base, refs)
change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n")
- post_received = ::Gitlab::GitPostReceive.new(project, key_id, change_str, {})
+ post_received = ::Gitlab::GitPostReceive.new(wiki.container, key_id, change_str, {})
allow(post_received).to receive(:identify).with(key_id).and_return(current_user)
post_received
end
def current_sha
- repository.gitaly_ref_client.find_branch('master')&.dereferenced_target&.id || Gitlab::Git::BLANK_SHA
+ repository.commit('master')&.id || Gitlab::Git::BLANK_SHA
end
# It is important not to re-use the WikiPage services here, since they create
@@ -312,7 +320,7 @@ RSpec.describe Git::WikiPushService, services: true do
file_content: 'some stuff',
branch_name: 'master'
}
- ::Wikis::CreateAttachmentService.new(container: project, current_user: project.owner, params: params).execute
+ ::Wikis::CreateAttachmentService.new(container: wiki.container, current_user: current_user, params: params).execute
end
def update_page(title)
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 794491fc50d..3c02b56f1a5 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -210,11 +210,13 @@ RSpec.describe Notes::QuickActionsService do
let(:service) { described_class.new(project, maintainer) }
it_behaves_like 'note on noteable that supports quick actions' do
- let(:note) { build(:note_on_issue, project: project) }
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+ let(:note) { build(:note_on_issue, project: project, noteable: issue) }
end
it_behaves_like 'note on noteable that supports quick actions' do
- let(:note) { build(:note_on_merge_request, project: project) }
+ let_it_be(:merge_request, reload: true) { create(:merge_request, source_project: project) }
+ let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
end
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 5339fa003b9..dd08d81bc46 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -11,20 +11,22 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_guest_permissions) do
%i[
- read_project read_board read_list read_wiki read_issue
- read_project_for_iids read_issue_iid read_label
- read_milestone read_snippet read_project_member read_note
- create_project create_issue create_note upload_file create_merge_request_in
- award_emoji
+ award_emoji create_issue create_merge_request_in create_note
+ create_project read_board read_issue read_issue_iid read_issue_link
+ read_label read_list read_milestone read_note read_project
+ read_project_for_iids read_project_member read_release read_snippet
+ read_wiki upload_file
]
end
let(:base_reporter_permissions) do
%i[
- download_code fork_project create_snippet update_issue
- admin_issue admin_label admin_list read_commit_status read_build
- read_container_image read_pipeline read_environment read_deployment
- read_merge_request download_wiki_code read_sentry_issue read_prometheus
+ admin_issue admin_issue_link admin_label admin_list create_snippet
+ download_code download_wiki_code fork_project metrics_dashboard
+ read_build read_commit_status read_confidential_issues
+ read_container_image read_deployment read_environment read_merge_request
+ read_metrics_dashboard_annotation read_pipeline read_prometheus
+ read_sentry_issue update_issue
]
end
@@ -34,37 +36,42 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:developer_permissions) do
%i[
- admin_milestone admin_merge_request update_merge_request create_commit_status
- update_commit_status create_build update_build create_pipeline
- update_pipeline create_merge_request_from create_wiki push_code
- resolve_note create_container_image update_container_image
- create_environment create_deployment update_deployment create_release update_release
- update_environment daily_statistics
+ admin_merge_request admin_milestone admin_tag create_build
+ create_commit_status create_container_image create_deployment
+ create_environment create_merge_request_from
+ create_metrics_dashboard_annotation create_pipeline create_release
+ create_wiki daily_statistics delete_metrics_dashboard_annotation
+ destroy_container_image push_code read_pod_logs read_terraform_state
+ resolve_note update_build update_commit_status update_container_image
+ update_deployment update_environment update_merge_request
+ update_metrics_dashboard_annotation update_pipeline update_release
]
end
let(:base_maintainer_permissions) do
%i[
- push_to_delete_protected_branch update_snippet
- admin_snippet admin_project_member admin_note admin_wiki admin_project
- admin_commit_status admin_build admin_container_image
- admin_pipeline admin_environment admin_deployment destroy_release add_cluster
+ add_cluster admin_build admin_commit_status admin_container_image
+ admin_deployment admin_environment admin_note admin_pipeline
+ admin_project admin_project_member admin_snippet admin_terraform_state
+ admin_wiki create_deploy_token destroy_deploy_token destroy_release
+ push_to_delete_protected_branch read_deploy_token update_snippet
]
end
let(:public_permissions) do
%i[
- download_code fork_project read_commit_status read_pipeline
- read_container_image build_download_code build_read_container_image
- download_wiki_code read_release
+ build_download_code build_read_container_image download_code
+ download_wiki_code fork_project read_commit_status read_container_image
+ read_pipeline read_release
]
end
let(:base_owner_permissions) do
%i[
- change_namespace change_visibility_level rename_project remove_project
- archive_project remove_fork_project destroy_merge_request destroy_issue
- set_issue_iid set_issue_created_at set_issue_updated_at set_note_created_at
+ archive_project change_namespace change_visibility_level destroy_issue
+ destroy_merge_request remove_fork_project remove_project rename_project
+ set_issue_created_at set_issue_iid set_issue_updated_at
+ set_note_created_at
]
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index a881d5f036c..b87f7fe97e1 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -322,8 +322,8 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
- expect(subject).to receive(:update_container_activity)
+ it 'runs after_wiki_activity callbacks' do
+ expect(subject).to receive(:after_wiki_activity)
subject.create_page('Test Page', 'This is content')
end
@@ -363,10 +363,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
update_page
end
@@ -389,10 +389,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
subject.delete_page(page)
end
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
new file mode 100644
index 00000000000..2a596946d86
--- /dev/null
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:usage data take tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/usage_data'
+ # stub prometheus external http calls https://gitlab.com/gitlab-org/gitlab/-/issues/245277
+ stub_request(:get, %r{^http://::1:9090/api/v1/query\?query=.*})
+ .to_return(
+ status: 200,
+ body: [{}].to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ describe 'dump_sql_in_yaml' do
+ it 'dumps SQL queries in yaml format' do
+ expect { run_rake_task('gitlab:usage_data:dump_sql_in_yaml') }.to output(/.*recorded_at:.*/).to_stdout
+ end
+ end
+
+ describe 'dump_sql_in_json' do
+ it 'dumps SQL queries in json format' do
+ expect { run_rake_task('gitlab:usage_data:dump_sql_in_json') }.to output(/.*"recorded_at":.*/).to_stdout
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index f64ee4aa2f7..50d164d1705 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe PostReceive do
context 'with a non-existing project' do
let(:gl_repository) { "project-123456789" }
let(:error_message) do
- "Triggered hook for non-existing project with gl_repository \"#{gl_repository}\""
+ "Triggered hook for non-existing gl_repository \"#{gl_repository}\""
end
it "returns false and logs an error" do
@@ -314,7 +314,7 @@ RSpec.describe PostReceive do
it 'processes the changes on the master branch' do
expect_next_instance_of(Git::WikiPushService) do |service|
- expect(service).to receive(:process_changes).and_call_original
+ expect(service).to receive(:execute).and_call_original
end
expect(project.wiki).to receive(:default_branch).twice.and_return(default_branch)
expect(project.wiki.repository).to receive(:raw).and_return(raw_repo)
@@ -334,7 +334,7 @@ RSpec.describe PostReceive do
before do
allow_next_instance_of(Git::WikiPushService) do |service|
- allow(service).to receive(:process_changes)
+ allow(service).to receive(:execute)
end
end
diff --git a/yarn.lock b/yarn.lock
index 5e01d222b15..7c160a211fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.164.0.tgz#6cefad871c45f945ef92b99015d0f510b1d2de4a"
integrity sha512-a9e/cYUc1QQk7azjH4x/m6/p3icavwGEi5F9ipNlDqiJtUor5tqojxvMxPOhuVbN/mTwnC6lGsSZg4tqTsdJAQ==
-"@gitlab/ui@20.19.0":
- version "20.19.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.19.0.tgz#4f7f3ff5ffa59baf6390f7ab25f199ba27b9b84b"
- integrity sha512-QXccwQNWfyCKqhRNIKZRnaE1JJR3g29hcHZoTQKKSlPVolHbqssszBOL8A4/H7TWuCFWRjswJPHFHfHeBHWccQ==
+"@gitlab/ui@20.20.0":
+ version "20.20.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.20.0.tgz#3985ee6dac8acdfa292d1878d4a61f5daff0e69c"
+ integrity sha512-bNP0qHdQj486xyRDvlecOgrnt6XWpAbqyYS7wcECo3W23hfuldMu9sM3N+wP2c6o/tMzCSO8wvuL1bc6ec+XTg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"