Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-09 21:08:33 +00:00
parent c172bb9967
commit b296ffa543
108 changed files with 1819 additions and 530 deletions

View File

@ -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" />

View File

@ -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 },
);
});

View File

@ -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);

View File

@ -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')"

View File

@ -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;

View File

@ -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();
},

View File

@ -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,
});

View File

@ -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';

View File

@ -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();
},

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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');
}
});
}

View File

@ -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: statusModalElement,
data() {
const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalWrapperEl,
data() {
const { currentEmoji, currentMessage } = this.$options.el.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);
});

View File

@ -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" />

View File

@ -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

View File

@ -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();

View File

@ -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…')"

View File

@ -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…')"

View File

@ -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;

View File

@ -1,3 +0,0 @@
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();

View File

@ -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>

View File

@ -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);
},

View File

@ -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>

View File

@ -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">

View File

@ -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,

View File

@ -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]

View File

@ -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

View File

@ -12,6 +12,7 @@ module Types
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
implements(Types::CurrentUserTodos)
field :versions,
Types::DesignManagement::VersionType.connection_type,

View File

@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
authorize :read_issue

View File

@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
authorize :read_merge_request

View File

@ -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 }

View File

@ -25,10 +25,6 @@ module HasWiki
wiki.repository_exists?
end
def after_wiki_activity
true
end
private
def check_wiki_path_conflict

View File

@ -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

View File

@ -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')

View File

@ -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.

View File

@ -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')

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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)

View File

@ -0,0 +1,5 @@
---
title: Expose the todos of the current user on relevant objects in GraphQL
merge_request: 40555
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in popover.vue
merge_request: 41197
author: Kev @KevSlashNull
type: other

View File

@ -0,0 +1,5 @@
---
title: Rake task to generate raw SQLs for usage ping
merge_request: 41091
author:
type: added

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fix merge request chat messages for adding and removing approvals
merge_request: 41775
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update design discussions to use GitLab UI components
merge_request: 41686
author:
type: other

View File

@ -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

View File

@ -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',
};

View File

@ -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
"""
@ -4999,6 +5061,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
"""
@ -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
"""
@ -5494,6 +5586,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
"""
@ -7328,7 +7450,7 @@ enum IssuableState {
opened
}
type Issue implements Noteable {
type Issue implements CurrentUserTodos & Noteable {
"""
Alert associated to this issue
"""
@ -7384,6 +7506,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
"""
@ -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
"""
@ -9040,6 +9192,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
"""
@ -16234,7 +16416,7 @@ type Todo {
action: TodoActionEnum!
"""
The owner of this todo
The author of this todo
"""
author: User!

View File

@ -7985,6 +7985,110 @@
"enumValues": null,
"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",
@ -9560,6 +9664,73 @@
"name": "Design",
"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",
@ -9939,6 +10110,11 @@
"kind": "INTERFACE",
"name": "DesignFields",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
}
],
"enumValues": null,
@ -13998,6 +14174,73 @@
"isDeprecated": false,
"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",
@ -14777,6 +15020,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
}
],
"enumValues": null,
@ -15375,6 +15623,73 @@
"isDeprecated": false,
"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",
@ -16155,6 +16470,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
}
],
"enumValues": null,
@ -20371,6 +20691,73 @@
"isDeprecated": false,
"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",
@ -21123,6 +21510,11 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
}
],
"enumValues": null,
@ -25130,6 +25522,73 @@
"isDeprecated": false,
"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",
@ -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": [
],

View File

@ -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 |

View File

@ -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)

View File

@ -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
```
<!--

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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', () => {

View File

@ -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({

View File

@ -184,6 +184,7 @@ describe('List model', () => {
}),
);
list.issues = [];
global.gon.features = { boardsWithSwimlanes: false };
});
it('adds new issue to top of list', done => {

View File

@ -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': {

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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"
/>

View File

@ -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>"
`;

View File

@ -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();
});
});

View File

@ -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()

View File

@ -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();
});
});
});

View File

@ -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');

View File

@ -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('');

View File

@ -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"

View File

@ -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');
});
});
});
});

View File

@ -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

View File

@ -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)) }

View File

@ -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)

View File

@ -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?

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More