diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-09 21:08:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-09 21:08:33 +0000 |
commit | b296ffa543e23f57fa2692539e6f0028c59e2203 (patch) | |
tree | f5224a37cac088506d1c8fd53925ee1d9fd2a02c | |
parent | c172bb9967f280e05bd904188d60a959dff10f00 (diff) | |
download | gitlab-ce-b296ffa543e23f57fa2692539e6f0028c59e2203.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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" |