Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bcfab67c0f
commit
b2452a3692
|
@ -1 +1 @@
|
|||
b7d1a76c7837d4df1896d52b8d10097216750ac7
|
||||
ef061fd0ccb16fadf3d8550c12b27c4cb3159990
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['labels', 'labelsLoading']),
|
||||
...mapState(['labels', 'labelsLoading', 'isEpicBoard']),
|
||||
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
|
||||
selectedLabel() {
|
||||
return this.labels.find(({ id }) => id === this.selectedLabelId);
|
||||
|
@ -57,7 +57,7 @@ export default {
|
|||
methods: {
|
||||
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
|
||||
getListByLabel(label) {
|
||||
if (this.shouldUseGraphQL) {
|
||||
if (this.shouldUseGraphQL || this.isEpicBoard) {
|
||||
return this.getListByLabelId(label);
|
||||
}
|
||||
return boardsStore.findListByLabelId(label.id);
|
||||
|
@ -66,7 +66,7 @@ export default {
|
|||
return Boolean(this.getListByLabel(label));
|
||||
},
|
||||
highlight(listId) {
|
||||
if (this.shouldUseGraphQL) {
|
||||
if (this.shouldUseGraphQL || this.isEpicBoard) {
|
||||
this.highlightList(listId);
|
||||
} else {
|
||||
const list = boardsStore.state.lists.find(({ id }) => id === listId);
|
||||
|
@ -95,7 +95,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.shouldUseGraphQL) {
|
||||
if (this.shouldUseGraphQL || this.isEpicBoard) {
|
||||
this.createList({ labelId: this.selectedLabelId });
|
||||
} else {
|
||||
boardsStore.new({
|
||||
|
@ -127,6 +127,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
|
||||
data-testid="board-add-new-column"
|
||||
data-qa-selector="board_add_new_list"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -128,7 +128,11 @@ export default {
|
|||
}, flashAnimationDuration);
|
||||
},
|
||||
|
||||
createList: (
|
||||
createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
|
||||
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
|
||||
},
|
||||
|
||||
createIssueList: (
|
||||
{ state, commit, dispatch, getters },
|
||||
{ backlog, labelId, milestoneId, assigneeId },
|
||||
) => {
|
||||
|
@ -172,7 +176,7 @@ export default {
|
|||
},
|
||||
|
||||
fetchLabels: ({ state, commit, getters }, searchTerm) => {
|
||||
const { fullPath, boardType } = state;
|
||||
const { fullPath, boardType, isEpicBoard } = state;
|
||||
|
||||
const variables = {
|
||||
fullPath,
|
||||
|
@ -191,7 +195,7 @@ export default {
|
|||
.then(({ data }) => {
|
||||
let labels = data[boardType]?.labels.nodes;
|
||||
|
||||
if (!getters.shouldUseGraphQL) {
|
||||
if (!getters.shouldUseGraphQL && !isEpicBoard) {
|
||||
labels = labels.map((label) => ({
|
||||
...label,
|
||||
id: getIdFromGraphQLId(label.id),
|
||||
|
|
|
@ -34,6 +34,11 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelFilterParam: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'label_name',
|
||||
},
|
||||
showCheckbox: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -105,9 +110,8 @@ export default {
|
|||
},
|
||||
labelTarget(label) {
|
||||
if (this.enableLabelPermalinks) {
|
||||
const key = encodeURIComponent('label_name[]');
|
||||
const value = encodeURIComponent(this.labelTitle(label));
|
||||
return `?${key}=${value}`;
|
||||
return `?${this.labelFilterParam}[]=${value}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
|
|
|
@ -122,6 +122,11 @@ export default {
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
labelFilterParam: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -180,7 +185,7 @@ export default {
|
|||
handler(params) {
|
||||
if (Object.keys(params).length) {
|
||||
updateHistory({
|
||||
url: setUrlParams(params, window.location.href, true),
|
||||
url: setUrlParams(params, window.location.href, true, false, true),
|
||||
title: document.title,
|
||||
replace: true,
|
||||
});
|
||||
|
@ -258,6 +263,7 @@ export default {
|
|||
:issuable-symbol="issuableSymbol"
|
||||
:issuable="issuable"
|
||||
:enable-label-permalinks="enableLabelPermalinks"
|
||||
:label-filter-param="labelFilterParam"
|
||||
:show-checkbox="showBulkEditSidebar"
|
||||
:checked="issuableChecked(issuable)"
|
||||
@checked-input="handleIssuableCheckedInput(issuable, $event)"
|
||||
|
|
|
@ -4,7 +4,6 @@ import $ from 'jquery';
|
|||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import TaskList from '../../task_list';
|
||||
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
|
||||
import animateMixin from '../mixins/animate';
|
||||
|
||||
export default {
|
||||
|
@ -12,7 +11,7 @@ export default {
|
|||
SafeHtml,
|
||||
},
|
||||
|
||||
mixins: [animateMixin, recaptchaModalImplementor],
|
||||
mixins: [animateMixin],
|
||||
|
||||
props: {
|
||||
canUpdate: {
|
||||
|
@ -87,21 +86,11 @@ export default {
|
|||
fieldName: 'description',
|
||||
lockVersion: this.lockVersion,
|
||||
selector: '.detail-page-description',
|
||||
onSuccess: this.taskListUpdateSuccess.bind(this),
|
||||
onError: this.taskListUpdateError.bind(this),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
taskListUpdateSuccess(data) {
|
||||
try {
|
||||
this.checkForSpam(data);
|
||||
this.closeRecaptcha();
|
||||
} catch (error) {
|
||||
if (error && error.name === 'SpamError') this.openRecaptcha();
|
||||
}
|
||||
},
|
||||
|
||||
taskListUpdateError() {
|
||||
createFlash(
|
||||
sprintf(
|
||||
|
@ -165,7 +154,5 @@ export default {
|
|||
>
|
||||
</textarea>
|
||||
<!-- eslint-enable vue/no-mutating-props -->
|
||||
|
||||
<recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptcha" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -473,6 +473,7 @@ export const setUrlParams = (
|
|||
url = window.location.href,
|
||||
clearParams = false,
|
||||
railsArraySyntax = false,
|
||||
decodeParams = false,
|
||||
) => {
|
||||
const urlObj = new URL(url);
|
||||
const queryString = urlObj.search;
|
||||
|
@ -495,7 +496,9 @@ export const setUrlParams = (
|
|||
}
|
||||
});
|
||||
|
||||
urlObj.search = searchParams.toString();
|
||||
urlObj.search = decodeParams
|
||||
? decodeURIComponent(searchParams.toString())
|
||||
: searchParams.toString();
|
||||
|
||||
return urlObj.toString();
|
||||
};
|
||||
|
|
|
@ -363,7 +363,7 @@ export default {
|
|||
data-qa-selector="comment_field"
|
||||
data-testid="comment-field"
|
||||
:data-supports-quick-actions="!glFeatures.tributeAutocomplete"
|
||||
:aria-label="__('Description')"
|
||||
:aria-label="__('Comment')"
|
||||
:placeholder="__('Write a comment or drag your files here…')"
|
||||
@keydown.up="editCurrentUserLastNote()"
|
||||
@keydown.meta.enter="handleSave()"
|
||||
|
|
|
@ -17,6 +17,7 @@ import commentForm from './comment_form.vue';
|
|||
import discussionFilterNote from './discussion_filter_note.vue';
|
||||
import noteableDiscussion from './noteable_discussion.vue';
|
||||
import noteableNote from './noteable_note.vue';
|
||||
import SidebarSubscription from './sidebar_subscription.vue';
|
||||
|
||||
export default {
|
||||
name: 'NotesApp',
|
||||
|
@ -30,6 +31,7 @@ export default {
|
|||
skeletonLoadingContainer,
|
||||
discussionFilterNote,
|
||||
OrderedLayout,
|
||||
SidebarSubscription,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
|
@ -261,6 +263,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div v-show="shouldShow" id="notes">
|
||||
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
|
||||
<ordered-layout :slot-keys="slotKeys">
|
||||
<template #form>
|
||||
<comment-form
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { confidentialityQueries } from '~/sidebar/constants';
|
||||
import { defaultClient as gqlClient } from '~/sidebar/graphql';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
noteableData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
iid: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fullPath() {
|
||||
if (this.noteableData.web_url) {
|
||||
return this.noteableData.web_url.split('/-/')[0].substring(1);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
issuableType() {
|
||||
return this.noteableData.noteableType.toLowerCase();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.issuableType !== IssuableType.Issue) {
|
||||
return;
|
||||
}
|
||||
|
||||
gqlClient
|
||||
.watchQuery({
|
||||
query: confidentialityQueries[this.issuableType].query,
|
||||
variables: {
|
||||
iid: String(this.iid),
|
||||
fullPath: this.fullPath,
|
||||
},
|
||||
fetchPolicy: fetchPolicies.CACHE_ONLY,
|
||||
})
|
||||
.subscribe((res) => {
|
||||
const issuable = res.data?.workspace?.issuable;
|
||||
if (issuable) {
|
||||
this.setConfidentiality(issuable.confidential);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setConfidentiality']),
|
||||
},
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -4,7 +4,7 @@ import Vue from 'vue';
|
|||
import Api from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
|
||||
import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
|
||||
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
|
||||
import loadAwardsHandler from '../../awards_handler';
|
||||
|
@ -340,6 +340,15 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
if (hasQuickActions && message) {
|
||||
eTagPoll.makeRequest();
|
||||
|
||||
// synchronizing the quick action with the sidebar widget
|
||||
// this is a temporary solution until we have confidentiality real-time updates
|
||||
if (
|
||||
confidentialWidget.setConfidentiality &&
|
||||
message.some((m) => m.includes('confidential'))
|
||||
) {
|
||||
confidentialWidget.setConfidentiality();
|
||||
}
|
||||
|
||||
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
|
||||
|
||||
Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
|
||||
|
@ -719,33 +728,3 @@ export const updateAssignees = ({ commit }, assignees) => {
|
|||
export const updateDiscussionPosition = ({ commit }, updatedPosition) => {
|
||||
commit(types.UPDATE_DISCUSSION_POSITION, updatedPosition);
|
||||
};
|
||||
|
||||
export const updateConfidentialityOnIssuable = (
|
||||
{ getters, commit },
|
||||
{ confidential, fullPath },
|
||||
) => {
|
||||
const { iid } = getters.getNoteableData;
|
||||
|
||||
return utils.gqClient
|
||||
.mutate({
|
||||
mutation: updateIssueConfidentialMutation,
|
||||
variables: {
|
||||
input: {
|
||||
projectPath: fullPath,
|
||||
iid: String(iid),
|
||||
confidential,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const {
|
||||
issueSetConfidential: { issue, errors },
|
||||
} = data;
|
||||
|
||||
if (errors?.length) {
|
||||
Flash(errors[0], 'alert');
|
||||
} else {
|
||||
setConfidentiality({ commit }, issue.confidential);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import ProjectsList from '~/projects_list';
|
||||
import initCustomizeHomepageBanner from './init_customize_homepage_banner';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ProjectsList(); // eslint-disable-line no-new
|
||||
|
||||
initCustomizeHomepageBanner();
|
||||
});
|
||||
new ProjectsList(); // eslint-disable-line no-new
|
||||
initCustomizeHomepageBanner();
|
||||
|
|
|
@ -64,13 +64,6 @@ export default {
|
|||
hasHighlightedJob() {
|
||||
return Boolean(this.highlightedJob);
|
||||
},
|
||||
alert() {
|
||||
if (this.hasError) {
|
||||
return this.failure;
|
||||
}
|
||||
|
||||
return this.warning;
|
||||
},
|
||||
failure() {
|
||||
switch (this.failureType) {
|
||||
case DRAW_FAILURE:
|
||||
|
@ -210,11 +203,11 @@ export default {
|
|||
<div>
|
||||
<gl-alert
|
||||
v-if="hasError"
|
||||
:variant="alert.variant"
|
||||
:dismissible="alert.dismissible"
|
||||
@dismiss="alert.dismissible ? resetFailure : null"
|
||||
:variant="failure.variant"
|
||||
:dismissible="failure.dismissible"
|
||||
@dismiss="resetFailure"
|
||||
>
|
||||
{{ alert.text }}
|
||||
{{ failure.text }}
|
||||
</gl-alert>
|
||||
<div
|
||||
v-if="!hideGraph"
|
||||
|
|
|
@ -6,8 +6,8 @@ import CommitComponent from '~/vue_shared/components/commit.vue';
|
|||
import eventHub from '../../event_hub';
|
||||
import PipelineTriggerer from './pipeline_triggerer.vue';
|
||||
import PipelineUrl from './pipeline_url.vue';
|
||||
import PipelinesActionsComponent from './pipelines_actions.vue';
|
||||
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
|
||||
import PipelinesManualActionsComponent from './pipelines_manual_actions.vue';
|
||||
import PipelineStage from './stage.vue';
|
||||
import PipelinesTimeago from './time_ago.vue';
|
||||
|
||||
|
@ -21,7 +21,7 @@ export default {
|
|||
GlModalDirective,
|
||||
},
|
||||
components: {
|
||||
PipelinesActionsComponent,
|
||||
PipelinesManualActionsComponent,
|
||||
PipelinesArtifactsComponent,
|
||||
CommitComponent,
|
||||
PipelineStage,
|
||||
|
@ -242,7 +242,7 @@ export default {
|
|||
class="table-section section-20 table-button-footer pipeline-actions"
|
||||
>
|
||||
<div class="btn-group table-action-buttons">
|
||||
<pipelines-actions-component v-if="actions.length > 0" :actions="actions" />
|
||||
<pipelines-manual-actions-component v-if="actions.length > 0" :actions="actions" />
|
||||
|
||||
<pipelines-artifacts-component
|
||||
v-if="pipeline.details.artifacts.length"
|
||||
|
|
|
@ -26,6 +26,8 @@ Sidebar.prototype.removeListeners = function () {
|
|||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
this.sidebar.off('hidden.gl.dropdown');
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
this.sidebar.off('hiddenGlDropdown');
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$('.dropdown').off('loading.gl.dropdown');
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$('.dropdown').off('loaded.gl.dropdown');
|
||||
|
@ -37,6 +39,7 @@ Sidebar.prototype.addEventListeners = function () {
|
|||
|
||||
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
|
||||
this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
|
||||
this.sidebar.on('hiddenGlDropdown', this, this.onSidebarDropdownHidden);
|
||||
|
||||
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
|
||||
return $(document)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
confidential: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
confidentialText() {
|
||||
return this.confidential
|
||||
? sprintf(__('This %{issuableType} is confidential'), {
|
||||
issuableType: this.issuableType,
|
||||
})
|
||||
: __('Not confidential');
|
||||
},
|
||||
confidentialIcon() {
|
||||
return this.confidential ? 'eye-slash' : 'eye';
|
||||
},
|
||||
tooltipLabel() {
|
||||
return this.confidential ? __('Confidential') : __('Not confidential');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-gl-tooltip.viewport.left :title="tooltipLabel" class="sidebar-collapsed-icon">
|
||||
<gl-icon
|
||||
:size="16"
|
||||
:name="confidentialIcon"
|
||||
class="sidebar-item-icon inline"
|
||||
:class="{ 'is-active': confidential }"
|
||||
/>
|
||||
</div>
|
||||
<gl-icon
|
||||
:size="16"
|
||||
:name="confidentialIcon"
|
||||
class="sidebar-item-icon inline hide-collapsed"
|
||||
:class="{ 'is-active': confidential }"
|
||||
/>
|
||||
<span class="hide-collapsed" data-testid="confidential-text">{{ confidentialText }}</span>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,126 @@
|
|||
<script>
|
||||
import { GlSprintf, GlButton } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { confidentialityQueries } from '~/sidebar/constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
confidentialityOnWarning: __(
|
||||
'You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}.',
|
||||
),
|
||||
confidentialityOffWarning: __(
|
||||
'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
},
|
||||
inject: ['fullPath', 'iid'],
|
||||
props: {
|
||||
confidential: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
issuableType: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toggleButtonText() {
|
||||
if (this.loading) {
|
||||
return __('Applying');
|
||||
}
|
||||
return this.confidential ? __('Turn off') : __('Turn on');
|
||||
},
|
||||
warningMessage() {
|
||||
return this.confidential
|
||||
? this.$options.i18n.confidentialityOffWarning
|
||||
: this.$options.i18n.confidentialityOnWarning;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.loading = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: confidentialityQueries[this.issuableType].mutation,
|
||||
variables: {
|
||||
input: {
|
||||
projectPath: this.fullPath,
|
||||
iid: this.iid,
|
||||
confidential: !this.confidential,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(
|
||||
({
|
||||
data: {
|
||||
issuableSetConfidential: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors.length) {
|
||||
createFlash({
|
||||
message: errors[0],
|
||||
});
|
||||
} else {
|
||||
this.$emit('closeForm');
|
||||
}
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash({
|
||||
message: sprintf(
|
||||
__('Something went wrong while setting %{issuableType} confidentiality.'),
|
||||
{
|
||||
issuableType: this.issuableType,
|
||||
},
|
||||
),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown show">
|
||||
<div class="dropdown-menu sidebar-item-warning-message">
|
||||
<div>
|
||||
<p data-testid="warning-message">
|
||||
<gl-sprintf :message="warningMessage">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #issuableType>{{ issuableType }}</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<div class="sidebar-item-warning-message-actions">
|
||||
<gl-button class="gl-mr-3" data-testid="confidential-cancel" @click="$emit('closeForm')">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
category="secondary"
|
||||
variant="warning"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
data-testid="confidential-toggle"
|
||||
@click.prevent="submitForm"
|
||||
>
|
||||
{{ toggleButtonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,130 @@
|
|||
<script>
|
||||
import produce from 'immer';
|
||||
import Vue from 'vue';
|
||||
import createFlash from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import { confidentialityQueries } from '~/sidebar/constants';
|
||||
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
|
||||
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
|
||||
|
||||
export const confidentialWidget = Vue.observable({
|
||||
setConfidentiality: null,
|
||||
});
|
||||
|
||||
const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
|
||||
bubbles: true,
|
||||
});
|
||||
|
||||
export default {
|
||||
tracking: {
|
||||
event: 'click_edit_button',
|
||||
label: 'right_sidebar',
|
||||
property: 'confidentiality',
|
||||
},
|
||||
components: {
|
||||
SidebarEditableItem,
|
||||
SidebarConfidentialityContent,
|
||||
SidebarConfidentialityForm,
|
||||
},
|
||||
inject: ['fullPath', 'iid'],
|
||||
props: {
|
||||
issuableType: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
confidential: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
confidential: {
|
||||
query() {
|
||||
return confidentialityQueries[this.issuableType].query;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.iid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.issuable?.confidential || false;
|
||||
},
|
||||
error() {
|
||||
createFlash({
|
||||
message: sprintf(
|
||||
__('Something went wrong while setting %{issuableType} confidentiality.'),
|
||||
{
|
||||
issuableType: this.issuableType,
|
||||
},
|
||||
),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.confidential.loading;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
confidentialWidget.setConfidentiality = this.setConfidentiality;
|
||||
},
|
||||
destroyed() {
|
||||
confidentialWidget.setConfidentiality = null;
|
||||
},
|
||||
methods: {
|
||||
closeForm() {
|
||||
this.$refs.editable.collapse();
|
||||
this.$el.dispatchEvent(hideDropdownEvent);
|
||||
},
|
||||
// synchronizing the quick action with the sidebar widget
|
||||
// this is a temporary solution until we have confidentiality real-time updates
|
||||
setConfidentiality() {
|
||||
const { defaultClient: client } = this.$apollo.provider.clients;
|
||||
const sourceData = client.readQuery({
|
||||
query: confidentialityQueries[this.issuableType].query,
|
||||
variables: { fullPath: this.fullPath, iid: this.iid },
|
||||
});
|
||||
|
||||
const data = produce(sourceData, (draftData) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
draftData.workspace.issuable.confidential = !this.confidential;
|
||||
});
|
||||
|
||||
client.writeQuery({
|
||||
query: confidentialityQueries[this.issuableType].query,
|
||||
variables: { fullPath: this.fullPath, iid: this.iid },
|
||||
data,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<sidebar-editable-item
|
||||
ref="editable"
|
||||
:title="__('Confidentiality')"
|
||||
:tracking="$options.tracking"
|
||||
:loading="isLoading"
|
||||
class="block confidentiality"
|
||||
>
|
||||
<template #collapsed>
|
||||
<div>
|
||||
<sidebar-confidentiality-content v-if="!isLoading" :confidential="confidential" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<sidebar-confidentiality-content :confidential="confidential" />
|
||||
<sidebar-confidentiality-form
|
||||
:confidential="confidential"
|
||||
:issuable-type="issuableType"
|
||||
@closeForm="closeForm"
|
||||
/>
|
||||
</template>
|
||||
</sidebar-editable-item>
|
||||
</template>
|
|
@ -15,6 +15,15 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
tracking: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
event: null,
|
||||
label: null,
|
||||
property: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -71,14 +80,18 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-display-flex gl-align-items-center gl-mb-3" @click.self="collapse">
|
||||
<span data-testid="title">{{ title }}</span>
|
||||
<gl-loading-icon v-if="loading" inline class="gl-ml-2" />
|
||||
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
|
||||
<span class="hide-collapsed" data-testid="title">{{ title }}</span>
|
||||
<gl-loading-icon v-if="loading" inline class="gl-ml-2 hide-collapsed" />
|
||||
<gl-loading-icon v-if="loading" inline class="gl-mx-auto gl-my-0 hide-expanded" />
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
variant="link"
|
||||
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle"
|
||||
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle hide-collapsed"
|
||||
data-testid="edit-button"
|
||||
:data-track-event="tracking.event"
|
||||
:data-track-label="tracking.label"
|
||||
:data-track-property="tracking.property"
|
||||
@keyup.esc="toggle"
|
||||
@click="toggle"
|
||||
>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { IssuableType } from '~/issue_show/constants';
|
||||
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
|
||||
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
|
||||
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
|
||||
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
|
||||
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
|
||||
|
@ -14,3 +16,10 @@ export const assigneesQueries = {
|
|||
mutation: updateMergeRequestParticipantsMutation,
|
||||
},
|
||||
};
|
||||
|
||||
export const confidentialityQueries = {
|
||||
[IssuableType.Issue]: {
|
||||
query: issueConfidentialQuery,
|
||||
mutation: updateIssueConfidentialMutation,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
|
||||
export const defaultClient = createDefaultClient();
|
||||
|
||||
export const apolloProvider = new VueApollo({
|
||||
defaultClient,
|
||||
});
|
|
@ -2,7 +2,6 @@ import $ from 'jquery';
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createFlash from '~/flash';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import {
|
||||
isInIssuePage,
|
||||
isInDesignPage,
|
||||
|
@ -10,9 +9,10 @@ import {
|
|||
parseBoolean,
|
||||
} from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||
import { apolloProvider } from '~/sidebar/graphql';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
|
||||
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
|
||||
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
|
||||
import SidebarLabels from './components/labels/sidebar_labels.vue';
|
||||
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
|
||||
|
@ -54,9 +54,6 @@ function getSidebarAssigneeAvailabilityData() {
|
|||
|
||||
function mountAssigneesComponent(mediator) {
|
||||
const el = document.getElementById('js-vue-sidebar-assignees');
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
if (!el) return;
|
||||
|
||||
|
@ -87,9 +84,6 @@ function mountAssigneesComponent(mediator) {
|
|||
|
||||
function mountReviewersComponent(mediator) {
|
||||
const el = document.getElementById('js-vue-sidebar-reviewers');
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
if (!el) return;
|
||||
|
||||
|
@ -121,10 +115,6 @@ export function mountSidebarLabels() {
|
|||
return false;
|
||||
}
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
|
@ -139,39 +129,37 @@ export function mountSidebarLabels() {
|
|||
});
|
||||
}
|
||||
|
||||
function mountConfidentialComponent(mediator) {
|
||||
function mountConfidentialComponent() {
|
||||
const el = document.getElementById('js-confidential-entry-point');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fullPath, iid } = getSidebarOptions();
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const dataNode = document.getElementById('js-confidential-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
import(/* webpackChunkName: 'notesStore' */ '~/notes/stores')
|
||||
.then(
|
||||
({ store }) =>
|
||||
new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
ConfidentialIssueSidebar,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement('confidential-issue-sidebar', {
|
||||
props: {
|
||||
iid: String(iid),
|
||||
fullPath,
|
||||
isEditable: initialData.is_editable,
|
||||
service: mediator.service,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash({ message: __('Failed to load sidebar confidential toggle') });
|
||||
});
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
components: {
|
||||
SidebarConfidentialityWidget,
|
||||
},
|
||||
provide: {
|
||||
iid: String(iid),
|
||||
fullPath,
|
||||
canUpdate: initialData.is_editable,
|
||||
},
|
||||
|
||||
render: (createElement) =>
|
||||
createElement('sidebar-confidentiality-widget', {
|
||||
props: {
|
||||
issuableType:
|
||||
isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request',
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function mountLockComponent() {
|
||||
|
@ -280,9 +268,6 @@ function mountSeverityComponent() {
|
|||
if (!severityContainerEl) {
|
||||
return false;
|
||||
}
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
const { fullPath, iid, severity } = getSidebarOptions();
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
query issueConfidential($fullPath: ID!, $iid: String) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: issue(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
confidential
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
|
||||
issuableSetConfidential: issueSetConfidential(input: $input) {
|
||||
issuable: issue {
|
||||
id
|
||||
confidential
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@
|
|||
color: var(--gray-500, $gray-500);
|
||||
}
|
||||
|
||||
[data-page$='epic_boards:show'],
|
||||
.issue-boards-page {
|
||||
.content-wrapper {
|
||||
padding-bottom: 0;
|
||||
|
|
|
@ -50,15 +50,16 @@ module Emails
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def access_token_about_to_expire_email(user)
|
||||
def access_token_about_to_expire_email(user, token_names)
|
||||
return unless user
|
||||
|
||||
@user = user
|
||||
@token_names = token_names
|
||||
@target_url = profile_personal_access_tokens_url
|
||||
@days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE
|
||||
|
||||
Gitlab::I18n.with_locale(@user.preferred_language) do
|
||||
mail(to: @user.notification_email, subject: subject(_("Your Personal Access Tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
|
||||
mail(to: @user.notification_email, subject: subject(_("Your personal access tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -259,12 +259,8 @@ class Namespace < ApplicationRecord
|
|||
# Includes projects from this namespace and projects from all subgroups
|
||||
# that belongs to this namespace
|
||||
def all_projects
|
||||
if Feature.enabled?(:recursive_approach_for_all_projects)
|
||||
namespace = user? ? self : self_and_descendants
|
||||
Project.where(namespace: namespace)
|
||||
else
|
||||
Project.inside_path(full_path)
|
||||
end
|
||||
namespace = user? ? self : self_and_descendants
|
||||
Project.where(namespace: namespace)
|
||||
end
|
||||
|
||||
# Includes pipelines from this namespace and pipelines from all subgroups
|
||||
|
|
|
@ -57,8 +57,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord
|
|||
end
|
||||
|
||||
def attributes_from_personal_snippets
|
||||
# Return if the type of namespace does not belong to a user
|
||||
return {} unless namespace.type.nil?
|
||||
return {} unless namespace.user?
|
||||
|
||||
from_personal_snippets.take.slice(SNIPPETS_SIZE_STAT_NAME)
|
||||
end
|
||||
|
@ -70,3 +69,5 @@ class Namespace::RootStorageStatistics < ApplicationRecord
|
|||
.select("COALESCE(SUM(s.repository_size), 0) AS #{SNIPPETS_SIZE_STAT_NAME}")
|
||||
end
|
||||
end
|
||||
|
||||
Namespace::RootStorageStatistics.prepend_if_ee('EE::Namespace::RootStorageStatistics')
|
||||
|
|
|
@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
|
|||
include Expirable
|
||||
include TokenAuthenticatable
|
||||
include Sortable
|
||||
include EachBatch
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
add_authentication_token_field :token, digest: true
|
||||
|
@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord
|
|||
end
|
||||
|
||||
def set_default_scopes
|
||||
# When only loading a select set of attributes, for example using `EachBatch`,
|
||||
# the `scopes` attribute is not present, so we can't initialize it.
|
||||
return unless has_attribute?(:scopes)
|
||||
|
||||
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
|
||||
end
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
module AuthorizedProjectUpdate
|
||||
class PeriodicRecalculateService
|
||||
BATCH_SIZE = 480
|
||||
DELAY_INTERVAL = 30.seconds.to_i
|
||||
BATCH_SIZE = 450
|
||||
DELAY_INTERVAL = 50.seconds.to_i
|
||||
|
||||
def execute
|
||||
# Using this approach (instead of eg. User.each_batch) keeps the arguments
|
||||
|
|
|
@ -66,10 +66,10 @@ class NotificationService
|
|||
|
||||
# Notify the owner of the personal access token, when it is about to expire
|
||||
# And mark the token with about_to_expire_delivered
|
||||
def access_token_about_to_expire(user)
|
||||
def access_token_about_to_expire(user, token_names)
|
||||
return unless user.can?(:receive_notifications)
|
||||
|
||||
mailer.access_token_about_to_expire_email(user).deliver_later
|
||||
mailer.access_token_about_to_expire_email(user, token_names).deliver_later
|
||||
end
|
||||
|
||||
# Notify the user when at least one of their personal access tokens has expired today
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- expanded = local_assigns.fetch(:expanded)
|
||||
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Variables')
|
||||
|
||||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
%p
|
||||
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
|
||||
%p
|
||||
= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire }
|
||||
= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less:') % { days_to_expire: @days_to_expire }
|
||||
%p
|
||||
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
|
||||
%ul
|
||||
- @token_names.each do |token|
|
||||
%li= token
|
||||
%p
|
||||
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
|
||||
= html_escape(_('You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
|
||||
|
||||
<%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire} %>
|
||||
<%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less:') % { days_to_expire: @days_to_expire } %>
|
||||
|
||||
<% @token_names.each do |token| %>
|
||||
- <%= token %>
|
||||
<% end %>
|
||||
|
||||
<%= _('You can create a new one or check them in your personal access tokens settings %{pat_link}') % { pat_link: @target_url } %>
|
||||
|
|
|
@ -197,7 +197,7 @@
|
|||
#js-board-epics-swimlanes-toggle
|
||||
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } }
|
||||
- if user_can_admin_list
|
||||
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml)
|
||||
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml) || board.to_type == "EpicBoard"
|
||||
.js-create-column-trigger{ data: board_list_data }
|
||||
- else
|
||||
= render 'shared/issuable/board_create_list_dropdown', board: board
|
||||
|
|
|
@ -133,5 +133,13 @@ module WorkerAttributes
|
|||
def get_deduplication_options
|
||||
class_attributes[:deduplication_options] || {}
|
||||
end
|
||||
|
||||
def big_payload!
|
||||
class_attributes[:big_payload] = true
|
||||
end
|
||||
|
||||
def big_payload?
|
||||
class_attributes[:big_payload]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,17 +7,32 @@ module PersonalAccessTokens
|
|||
|
||||
feature_category :authentication_and_authorization
|
||||
|
||||
MAX_TOKENS = 100
|
||||
|
||||
def perform(*args)
|
||||
notification_service = NotificationService.new
|
||||
limit_date = PersonalAccessToken::DAYS_TO_EXPIRE.days.from_now.to_date
|
||||
|
||||
User.with_expiring_and_not_notified_personal_access_tokens(limit_date).find_each do |user|
|
||||
with_context(user: user) do
|
||||
notification_service.access_token_about_to_expire(user)
|
||||
expiring_user_tokens = user.personal_access_tokens.without_impersonation.expiring_and_not_notified(limit_date)
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# We never materialise the token instances. We need the names to mention them in the
|
||||
# email. Later we trigger an update query on the entire relation, not on individual instances.
|
||||
token_names = expiring_user_tokens.limit(MAX_TOKENS).pluck(:name)
|
||||
# We're limiting to 100 tokens so we avoid loading too many tokens into memory.
|
||||
# At the time of writing this would only affect 69 users on GitLab.com
|
||||
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
notification_service.access_token_about_to_expire(user, token_names)
|
||||
|
||||
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring tokens"
|
||||
|
||||
user.personal_access_tokens.without_impersonation.expiring_and_not_notified(limit_date).update_all(expire_notification_delivered: true)
|
||||
expiring_user_tokens.each_batch do |expiring_tokens|
|
||||
expiring_tokens.update_all(expire_notification_delivered: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Project Settings CI/CD Variables header expands/collapses on click / tap
|
||||
merge_request: 54117
|
||||
author: Daniel Schömer
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use recursive approach to query all projects for a namespace
|
||||
merge_request: 55043
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Sidebar confidentiality component updates in real-time
|
||||
merge_request: 53858
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Provide name of expiring token in personal access token expiration mail
|
||||
merge_request: 53766
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Harden added metrics
|
||||
merge_request: 54805
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API endpoint for fetching a single job by CI_JOB_TOKEN
|
||||
merge_request: 51727
|
||||
author: ahmet2mir
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rename comment form textarea label to `Comment`
|
||||
merge_request: 55088
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Github Importer: Import Pull request "merged_at" attribute'
|
||||
merge_request: 54862
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Limit the payload size of Sidekiq jobs before scheduling
|
||||
merge_request: 53829
|
||||
author:
|
||||
type: added
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: recursive_approach_for_all_projects
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44740
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263442
|
||||
milestone: '13.5'
|
||||
type: development
|
||||
group: group::fulfillment
|
||||
default_enabled: false
|
|
@ -7,7 +7,7 @@ product_group: group::ops release
|
|||
product_category:
|
||||
value_type: number
|
||||
status: data_available
|
||||
milestone: 13.2
|
||||
milestone: "13.2"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35493
|
||||
time_frame: 28d
|
||||
data_source: database
|
||||
|
|
|
@ -8,7 +8,7 @@ product_group: group::product planning
|
|||
product_category: issue_tracking
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: 13.10
|
||||
milestone: "13.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49264
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -8,7 +8,7 @@ product_group: group::product planning
|
|||
product_category: issue_tracking
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: 13.10
|
||||
milestone: "13.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49264
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -7,7 +7,7 @@ product_group: group::project management
|
|||
product_category: issue_tracking
|
||||
value_type: number
|
||||
status: data_available
|
||||
milestone: 13.6
|
||||
milestone: "13.6"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -6,7 +6,7 @@ product_stage: release
|
|||
product_group: group::ops release
|
||||
value_type: number
|
||||
status: data_available
|
||||
milestone: 8.12
|
||||
milestone: "8.12"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/735
|
||||
time_frame: all
|
||||
data_source: database
|
||||
|
|
|
@ -7,7 +7,7 @@ product_group: group::product intelligence
|
|||
product_category: collection
|
||||
value_type: string
|
||||
status: data_available
|
||||
milestone: 8.10
|
||||
milestone: "8.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557
|
||||
time_frame: none
|
||||
data_source: ruby
|
||||
|
|
|
@ -7,7 +7,7 @@ product_group: group::product intelligence
|
|||
product_category: collection
|
||||
value_type: string
|
||||
status: data_available
|
||||
milestone: 9.1
|
||||
milestone: "9.1"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1521
|
||||
time_frame: none
|
||||
data_source: database
|
||||
|
|
|
@ -29,10 +29,12 @@
|
|||
"enum": ["data_available", "planned", "in_progress", "implemented", "not_used", "deprecated"]
|
||||
},
|
||||
"milestone": {
|
||||
"type": ["number", "null"]
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[0-9]+\\.[0-9]+$"
|
||||
},
|
||||
"milestone_removed": {
|
||||
"type": ["number", "null"]
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[0-9]+\\.[0-9]+$"
|
||||
},
|
||||
"introduced_by_url": {
|
||||
"type": ["string", "null"]
|
||||
|
|
|
@ -211,6 +211,7 @@ to authenticate with the API:
|
|||
- [Package Registry for PyPI](../user/packages/pypi_repository/index.md#authenticate-with-a-ci-job-token)
|
||||
- [Package Registry for generic packages](../user/packages/generic_packages/index.md#publish-a-generic-package-by-using-cicd)
|
||||
- [Get job artifacts](job_artifacts.md#get-job-artifacts)
|
||||
- [Get job token's job](jobs.md#get-job-tokens-job)
|
||||
- [Pipeline triggers](pipeline_triggers.md) (using the `token=` parameter)
|
||||
- [Release creation](releases/index.md#create-a-release)
|
||||
- [Terraform plan](../user/infrastructure/index.md)
|
||||
|
|
|
@ -146,6 +146,7 @@ The following API resources are available outside of project and group contexts
|
|||
| [Instance clusters](instance_clusters.md) | `/admin/clusters` |
|
||||
| [Issues](issues.md) | `/issues` (also available for groups and projects) |
|
||||
| [Issues Statistics](issues_statistics.md) | `/issues_statistics` (also available for groups and projects) |
|
||||
| [Jobs](jobs.md) | `/job` |
|
||||
| [Keys](keys.md) | `/keys` |
|
||||
| [License](license.md) **(FREE SELF)** | `/license` |
|
||||
| [Markdown](markdown.md) | `/markdown` |
|
||||
|
|
|
@ -380,6 +380,76 @@ Example of response
|
|||
]
|
||||
```
|
||||
|
||||
## Get job token's job
|
||||
|
||||
Retrieve the job that generated a job token.
|
||||
|
||||
```plaintext
|
||||
GET /job
|
||||
```
|
||||
|
||||
Examples
|
||||
|
||||
```shell
|
||||
curl --header "JOB-TOKEN: <your_job_token>" "https://gitlab.example.com/api/v4/job"
|
||||
curl "https://gitlab.example.com/api/v4/job?job_token=<your_job_token>"
|
||||
```
|
||||
|
||||
Example of response
|
||||
|
||||
```json
|
||||
{
|
||||
"commit": {
|
||||
"author_email": "admin@example.com",
|
||||
"author_name": "Administrator",
|
||||
"created_at": "2015-12-24T16:51:14.000+01:00",
|
||||
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"message": "Test the CI integration.",
|
||||
"short_id": "0ff3ae19",
|
||||
"title": "Test the CI integration."
|
||||
},
|
||||
"coverage": null,
|
||||
"allow_failure": false,
|
||||
"created_at": "2015-12-24T15:51:21.880Z",
|
||||
"started_at": "2015-12-24T17:54:30.733Z",
|
||||
"finished_at": "2015-12-24T17:54:31.198Z",
|
||||
"duration": 0.465,
|
||||
"artifacts_expire_at": "2016-01-23T17:54:31.198Z",
|
||||
"id": 8,
|
||||
"name": "rubocop",
|
||||
"pipeline": {
|
||||
"id": 6,
|
||||
"ref": "master",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
"status": "failed",
|
||||
"tag": false,
|
||||
"web_url": "https://example.com/foo/bar/-/jobs/8",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||
"web_url": "http://gitlab.dev/root",
|
||||
"created_at": "2015-12-21T13:14:24.077Z",
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"public_email": "",
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
"website_url": "",
|
||||
"organization": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get a single job
|
||||
|
||||
Get a single job of a project
|
||||
|
|
|
@ -19,7 +19,7 @@ it should be restricted on namespace scope.
|
|||
|
||||
1. Add the feature symbol on `EES_FEATURES`, `EEP_FEATURES` or `EEU_FEATURES` constants in
|
||||
`ee/app/models/license.rb`. Note on `ee/app/models/ee/namespace.rb` that _Bronze_ GitLab.com
|
||||
features maps to on-premise _EES_, _Silver_ to _EEP_ and _Gold_ to _EEU_.
|
||||
features maps to on-premise _EES_, _Silver/Premium_ to _EEP_ and _Gold/Ultimate_ to _EEU_.
|
||||
1. Check using:
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -539,21 +539,6 @@ When looking at this initially you'd suspect that the component is setup before
|
|||
|
||||
This is however not entirely true as the `destroy` method does not remove everything which has been mutated on the `wrapper` object. For functional components, destroy only removes the rendered DOM elements from the document.
|
||||
|
||||
In order to ensure that a clean wrapper object and DOM are being used in each test, the breakdown of the component should rather be performed as follows:
|
||||
|
||||
```javascript
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
```
|
||||
|
||||
<!-- vale gitlab.Spelling = NO -->
|
||||
|
||||
See also the [Vue Test Utils documentation on `destroy`](https://vue-test-utils.vuejs.org/api/wrapper/#destroy).
|
||||
|
||||
<!-- vale gitlab.Spelling = YES -->
|
||||
|
||||
### Jest best practices
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34209) in GitLab 13.2.
|
||||
|
|
|
@ -11211,7 +11211,7 @@ When the Usage Ping computation was started
|
|||
| `product_category` | `collection` |
|
||||
| `value_type` | string |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 8.1 |
|
||||
| `milestone` | 8.10 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557) |
|
||||
| `time_frame` | none |
|
||||
| `data_source` | Ruby |
|
||||
|
@ -17653,7 +17653,7 @@ Missing description
|
|||
| `product_category` | `issue_tracking` |
|
||||
| `value_type` | number |
|
||||
| `status` | implemented |
|
||||
| `milestone` | 13.1 |
|
||||
| `milestone` | 13.10 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49264) |
|
||||
| `time_frame` | 28d |
|
||||
| `data_source` | Redis_hll |
|
||||
|
@ -17674,7 +17674,7 @@ Missing description
|
|||
| `product_category` | `issue_tracking` |
|
||||
| `value_type` | number |
|
||||
| `status` | implemented |
|
||||
| `milestone` | 13.1 |
|
||||
| `milestone` | 13.10 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49264) |
|
||||
| `time_frame` | 28d |
|
||||
| `data_source` | Redis_hll |
|
||||
|
|
|
@ -8,7 +8,7 @@ product_group:
|
|||
product_category:
|
||||
value_type: <%= value_type %>
|
||||
status: implemented
|
||||
milestone: <%= milestone %>
|
||||
milestone: "<%= milestone %>"
|
||||
introduced_by_url:
|
||||
time_frame: <%= time_frame %>
|
||||
data_source:
|
||||
|
|
|
@ -8,10 +8,11 @@ module API
|
|||
|
||||
feature_category :continuous_integration
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
|
||||
helpers do
|
||||
params :optional_scope do
|
||||
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
|
||||
|
@ -168,6 +169,20 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
resource :job do
|
||||
desc 'Get current project using job token' do
|
||||
success Entities::Ci::Job
|
||||
end
|
||||
route_setting :authentication, job_token_allowed: true
|
||||
get do
|
||||
# current_authenticated_job will be nil if user is using
|
||||
# a valid authentication that is not CI_JOB_TOKEN
|
||||
not_found!('Job') unless current_authenticated_job
|
||||
|
||||
present current_authenticated_job, with: Entities::Ci::Job
|
||||
end
|
||||
end
|
||||
|
||||
helpers do
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def filter_builds(builds, scope)
|
||||
|
|
|
@ -61,9 +61,6 @@ module Gitlab
|
|||
remove_known_trial_form_fields: {
|
||||
tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields'
|
||||
},
|
||||
trimmed_skip_trial_copy: {
|
||||
tracking_category: 'Growth::Conversion::Experiment::TrimmedSkipTrialCopy'
|
||||
},
|
||||
trial_registration_with_social_signin: {
|
||||
tracking_category: 'Growth::Conversion::Experiment::TrialRegistrationWithSocialSigning'
|
||||
},
|
||||
|
|
|
@ -12,25 +12,27 @@ module Gitlab
|
|||
|
||||
def execute
|
||||
merge_request = project.merge_requests.find_by_iid(pull_request.iid)
|
||||
timestamp = Time.new.utc
|
||||
merged_at = pull_request.merged_at
|
||||
user_finder = GithubImport::UserFinder.new(project, client)
|
||||
gitlab_user_id = user_finder.user_id_for(pull_request.merged_by)
|
||||
|
||||
if gitlab_user_id
|
||||
timestamp = Time.new.utc
|
||||
MergeRequest::Metrics.upsert({
|
||||
target_project_id: project.id,
|
||||
merge_request_id: merge_request.id,
|
||||
merged_by_id: gitlab_user_id,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp
|
||||
}, unique_by: :merge_request_id)
|
||||
else
|
||||
MergeRequest::Metrics.upsert({
|
||||
target_project_id: project.id,
|
||||
merge_request_id: merge_request.id,
|
||||
merged_by_id: gitlab_user_id,
|
||||
merged_at: merged_at,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp
|
||||
}, unique_by: :merge_request_id)
|
||||
|
||||
unless gitlab_user_id
|
||||
merge_request.notes.create!(
|
||||
importing: true,
|
||||
note: "*Merged by: #{pull_request.merged_by.login}*",
|
||||
note: missing_author_note,
|
||||
author_id: project.creator_id,
|
||||
project: project,
|
||||
created_at: pull_request.created_at
|
||||
created_at: merged_at
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -38,6 +40,13 @@ module Gitlab
|
|||
private
|
||||
|
||||
attr_reader :project, :pull_request, :client
|
||||
|
||||
def missing_author_note
|
||||
s_("GitHubImporter|*Merged by: %{author} at %{timestamp}*") % {
|
||||
author: pull_request.merged_by.login,
|
||||
timestamp: pull_request.merged_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,6 +36,8 @@ module Gitlab
|
|||
chain.add ::Gitlab::SidekiqMiddleware::DuplicateJobs::Client
|
||||
chain.add ::Gitlab::SidekiqStatus::ClientMiddleware
|
||||
chain.add ::Gitlab::SidekiqMiddleware::AdminMode::Client
|
||||
# Size limiter should be placed at the bottom, but before the metrics midleware
|
||||
chain.add ::Gitlab::SidekiqMiddleware::SizeLimiter::Client
|
||||
chain.add ::Gitlab::SidekiqMiddleware::ClientMetrics
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
module SizeLimiter
|
||||
# This midleware is inserted into Sidekiq **client** middleware chain. It
|
||||
# prevents the caller from dispatching a too-large job payload. The job
|
||||
# payload should be small and simple. Read more at:
|
||||
# https://github.com/mperham/sidekiq/wiki/Best-Practices#1-make-your-job-parameters-small-and-simple
|
||||
class Client
|
||||
def call(worker_class, job, queue, _redis_pool)
|
||||
Validator.validate!(worker_class, job)
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
module SizeLimiter
|
||||
# A custom exception for size limiter. It contains worker class and its
|
||||
# size to easier track later
|
||||
class ExceedLimitError < StandardError
|
||||
attr_reader :worker_class, :size, :size_limit
|
||||
|
||||
def initialize(worker_class, size, size_limit)
|
||||
@worker_class = worker_class
|
||||
@size = size
|
||||
@size_limit = size_limit
|
||||
|
||||
super "#{@worker_class} job exceeds payload size limit (#{size}/#{size_limit})"
|
||||
end
|
||||
|
||||
def sentry_extra_data
|
||||
{
|
||||
worker_class: @worker_class.to_s,
|
||||
size: @size.to_i,
|
||||
size_limit: @size_limit.to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
module SizeLimiter
|
||||
# Validate a Sidekiq job payload limit based on current configuration.
|
||||
# This validator pulls the configuration from the environment variables:
|
||||
#
|
||||
# - GITLAB_SIDEKIQ_SIZE_LIMITER_MODE: the current mode of the size
|
||||
# limiter. This must be either `track` or `raise`.
|
||||
#
|
||||
# - GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES: the size limit in bytes.
|
||||
#
|
||||
# If the size of job payload after serialization exceeds the limit, an
|
||||
# error is tracked raised adhering to the mode.
|
||||
class Validator
|
||||
def self.validate!(worker_class, job)
|
||||
new(worker_class, job).validate!
|
||||
end
|
||||
|
||||
DEFAULT_SIZE_LIMIT = 0
|
||||
|
||||
MODES = [
|
||||
TRACK_MODE = 'track',
|
||||
RAISE_MODE = 'raise'
|
||||
].freeze
|
||||
|
||||
attr_reader :mode, :size_limit
|
||||
|
||||
def initialize(
|
||||
worker_class, job,
|
||||
mode: ENV['GITLAB_SIDEKIQ_SIZE_LIMITER_MODE'],
|
||||
size_limit: ENV['GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES']
|
||||
)
|
||||
@worker_class = worker_class
|
||||
@job = job
|
||||
|
||||
@mode = (mode || TRACK_MODE).to_s.strip
|
||||
unless MODES.include?(@mode)
|
||||
::Sidekiq.logger.warn "Invalid Sidekiq size limiter mode: #{@mode}. Fallback to #{TRACK_MODE} mode."
|
||||
@mode = TRACK_MODE
|
||||
end
|
||||
|
||||
@size_limit = (size_limit || DEFAULT_SIZE_LIMIT).to_i
|
||||
if @size_limit < 0
|
||||
::Sidekiq.logger.warn "Invalid Sidekiq size limiter limit: #{@size_limit}"
|
||||
end
|
||||
end
|
||||
|
||||
def validate!
|
||||
return unless @size_limit > 0
|
||||
|
||||
return if allow_big_payload?
|
||||
return if job_size <= @size_limit
|
||||
|
||||
exception = ExceedLimitError.new(@worker_class, job_size, @size_limit)
|
||||
# This should belong to Gitlab::ErrorTracking. We'll remove this
|
||||
# after this epic is done:
|
||||
# https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/396
|
||||
exception.set_backtrace(backtrace)
|
||||
|
||||
if raise_mode?
|
||||
raise exception
|
||||
else
|
||||
track(exception)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def job_size
|
||||
# This maynot be the optimal solution, but can be acceptable solution
|
||||
# for now. Internally, Sidekiq calls Sidekiq.dump_json everywhere.
|
||||
# There is no clean way to intefere to prevent double serialization.
|
||||
@job_size ||= ::Sidekiq.dump_json(@job).bytesize
|
||||
end
|
||||
|
||||
def allow_big_payload?
|
||||
worker_class = @worker_class.to_s.safe_constantize
|
||||
worker_class.respond_to?(:big_payload?) && worker_class.big_payload?
|
||||
end
|
||||
|
||||
def raise_mode?
|
||||
@mode == RAISE_MODE
|
||||
end
|
||||
|
||||
def track(exception)
|
||||
Gitlab::ErrorTracking.track_exception(exception)
|
||||
end
|
||||
|
||||
def backtrace
|
||||
Gitlab::BacktraceCleaner.clean_backtrace(caller)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -192,7 +192,7 @@ module Gitlab
|
|||
container_expiration_policies_usage,
|
||||
service_desk_counts
|
||||
).tap do |data|
|
||||
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
|
||||
data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
|
||||
end
|
||||
}
|
||||
end
|
||||
|
@ -227,7 +227,7 @@ module Gitlab
|
|||
snowplow_event_counts(last_28_days_time_period(column: :collector_tstamp)),
|
||||
aggregated_metrics_monthly
|
||||
).tap do |data|
|
||||
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
|
||||
data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
|
||||
end
|
||||
}
|
||||
end
|
||||
|
@ -821,11 +821,9 @@ module Gitlab
|
|||
def total_alert_issues
|
||||
# Remove prometheus table queries once they are deprecated
|
||||
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/217407.
|
||||
[
|
||||
count(Issue.with_alert_management_alerts, start: issue_minimum_id, finish: issue_maximum_id),
|
||||
add count(Issue.with_alert_management_alerts, start: issue_minimum_id, finish: issue_maximum_id),
|
||||
count(::Issue.with_self_managed_prometheus_alert_events, start: issue_minimum_id, finish: issue_maximum_id),
|
||||
count(::Issue.with_prometheus_alert_events, start: issue_minimum_id, finish: issue_maximum_id)
|
||||
].reduce(:+)
|
||||
end
|
||||
|
||||
def user_minimum_id
|
||||
|
@ -952,7 +950,7 @@ module Gitlab
|
|||
csv_issue_imports = distinct_count(Issues::CsvImport.where(time_period), :user_id)
|
||||
group_imports = distinct_count(::GroupImportState.where(time_period), :user_id)
|
||||
|
||||
project_imports + bulk_imports + jira_issue_imports + csv_issue_imports + group_imports
|
||||
add(project_imports, bulk_imports, jira_issue_imports, csv_issue_imports, group_imports)
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -8884,6 +8884,9 @@ msgstr ""
|
|||
msgid "Credentials"
|
||||
msgstr ""
|
||||
|
||||
msgid "CredentialsInventory|GPG Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "CredentialsInventory|No credentials found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9785,7 +9788,7 @@ msgstr ""
|
|||
msgid "DeleteValueStream|'%{name}' Value Stream deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeleteValueStream|Are you sure you want to delete \"%{name}\" Value Stream?"
|
||||
msgid "DeleteValueStream|Are you sure you want to delete the \"%{name}\" Value Stream?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeleteValueStream|Delete %{name}"
|
||||
|
@ -12493,9 +12496,6 @@ msgstr ""
|
|||
msgid "Failed to load related branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load sidebar confidential toggle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load sidebar lock status"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13746,6 +13746,9 @@ msgstr ""
|
|||
msgid "GitHub import"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitHubImporter|*Merged by: %{author} at %{timestamp}*"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14922,6 +14925,9 @@ msgstr ""
|
|||
msgid "Hi %{username}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide archived projects"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19571,6 +19577,9 @@ msgstr ""
|
|||
msgid "Move"
|
||||
msgstr ""
|
||||
|
||||
msgid "Move down"
|
||||
msgstr ""
|
||||
|
||||
msgid "Move issue"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19589,6 +19598,9 @@ msgstr ""
|
|||
msgid "Move this issue to another project."
|
||||
msgstr ""
|
||||
|
||||
msgid "Move up"
|
||||
msgstr ""
|
||||
|
||||
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21058,7 +21070,7 @@ msgstr ""
|
|||
msgid "One or more of your personal access tokens has expired."
|
||||
msgstr ""
|
||||
|
||||
msgid "One or more of your personal access tokens will expire in %{days_to_expire} days or less."
|
||||
msgid "One or more of your personal access tokens will expire in %{days_to_expire} days or less:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Only 'Reporter' roles and above on tiers Premium and above can see Value Stream Analytics."
|
||||
|
@ -27809,6 +27821,9 @@ msgstr ""
|
|||
msgid "Something went wrong while resolving this discussion. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while setting %{issuableType} confidentiality."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while stopping this environment. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31356,9 +31371,6 @@ msgstr ""
|
|||
msgid "Trials|Skip Trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trials|Skip Trial (Continue with Free Account)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31500,6 +31512,12 @@ msgstr ""
|
|||
msgid "Turn On"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn on"
|
||||
msgstr ""
|
||||
|
||||
msgid "Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -33804,6 +33822,9 @@ msgstr ""
|
|||
msgid "You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -34371,9 +34392,6 @@ msgstr ""
|
|||
msgid "Your Personal Access Token was revoked"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Personal Access Tokens will expire in %{days_to_expire} days or less"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Primary Email will be used for avatar detection."
|
||||
msgstr ""
|
||||
|
||||
|
@ -34545,6 +34563,9 @@ msgstr ""
|
|||
msgid "Your personal access token has expired"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your personal access tokens will expire in %{days_to_expire} days or less"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -54,9 +54,9 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.182.0",
|
||||
"@gitlab/svgs": "1.183.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "27.7.1",
|
||||
"@gitlab/ui": "27.14.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-4",
|
||||
"@rails/ujs": "^6.0.3-4",
|
||||
|
@ -168,7 +168,7 @@
|
|||
"devDependencies": {
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
|
||||
"@gitlab/eslint-plugin": "8.1.0",
|
||||
"@gitlab/stylelint-config": "2.2.0",
|
||||
"@gitlab/stylelint-config": "2.3.0",
|
||||
"@testing-library/dom": "^7.16.2",
|
||||
"@vue/test-utils": "1.1.2",
|
||||
"acorn": "^6.3.0",
|
||||
|
|
|
@ -209,16 +209,16 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
# Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
|
||||
it 'shows username when pasting then pressing Enter' do
|
||||
fill_in 'Description', with: "@#{user.username}\n"
|
||||
fill_in 'Comment', with: "@#{user.username}\n"
|
||||
|
||||
expect(find_field('Description').value).to have_content "@#{user.username}"
|
||||
expect(find_field('Comment').value).to have_content "@#{user.username}"
|
||||
end
|
||||
|
||||
it 'does not show `@undefined` when pressing `@` then Enter' do
|
||||
fill_in 'Description', with: "@\n"
|
||||
fill_in 'Comment', with: "@\n"
|
||||
|
||||
expect(find_field('Description').value).to have_content "@"
|
||||
expect(find_field('Description').value).not_to have_content "@undefined"
|
||||
expect(find_field('Comment').value).to have_content "@"
|
||||
expect(find_field('Comment').value).not_to have_content "@undefined"
|
||||
end
|
||||
|
||||
it 'selects the first item for non-assignee dropdowns if a query is entered' do
|
||||
|
|
|
@ -210,6 +210,16 @@ describe('fetchIssueLists', () => {
|
|||
});
|
||||
|
||||
describe('createList', () => {
|
||||
it('should dispatch createIssueList action', () => {
|
||||
testAction({
|
||||
action: actions.createList,
|
||||
payload: { backlog: true },
|
||||
expectedActions: [{ type: 'createIssueList', payload: { backlog: true } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIssueList', () => {
|
||||
let commit;
|
||||
let dispatch;
|
||||
let getters;
|
||||
|
@ -249,7 +259,7 @@ describe('createList', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
|
||||
});
|
||||
|
@ -271,7 +281,7 @@ describe('createList', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await actions.createList({ getters, state, commit, dispatch }, { labelId: '4' });
|
||||
await actions.createIssueList({ getters, state, commit, dispatch }, { labelId: '4' });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('addList', list);
|
||||
expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
|
||||
|
@ -289,7 +299,7 @@ describe('createList', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
|
||||
});
|
||||
|
@ -306,7 +316,7 @@ describe('createList', () => {
|
|||
getListByLabelId: jest.fn().mockReturnValue(existingList),
|
||||
};
|
||||
|
||||
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -202,7 +202,7 @@ describe('IssuableItem', () => {
|
|||
describe('labelTarget', () => {
|
||||
it('returns target string for a provided label param when `enableLabelPermalinks` is true', () => {
|
||||
expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe(
|
||||
'?label_name%5B%5D=Documentation%20Update',
|
||||
'?label_name[]=Documentation%20Update',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -70,36 +70,6 @@ describe('Description component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('opens reCAPTCHA dialog if update rejected as spam', () => {
|
||||
let modal;
|
||||
const recaptchaChild = vm.$children.find(
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(child) => child.$options._componentTag === 'recaptcha-modal',
|
||||
);
|
||||
|
||||
recaptchaChild.scriptSrc = '//scriptsrc';
|
||||
|
||||
vm.taskListUpdateSuccess({
|
||||
recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
|
||||
});
|
||||
|
||||
return vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
modal = vm.$el.querySelector('.js-recaptcha-modal');
|
||||
|
||||
expect(modal.style.display).not.toEqual('none');
|
||||
expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
|
||||
expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
|
||||
})
|
||||
.then(() => modal.querySelector('.close').click())
|
||||
.then(() => vm.$nextTick())
|
||||
.then(() => {
|
||||
expect(modal.style.display).toEqual('none');
|
||||
expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies syntax highlighting and math when description changed', () => {
|
||||
const vmSpy = jest.spyOn(vm, 'renderGFM');
|
||||
const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
|
||||
|
@ -144,7 +114,6 @@ describe('Description component', () => {
|
|||
dataType: 'issuableType',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
lockVersion: 0,
|
||||
});
|
||||
|
|
|
@ -814,6 +814,14 @@ describe('URL utility', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('decodes URI when decodeURI=true', () => {
|
||||
const url = 'https://gitlab.com/test';
|
||||
|
||||
expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true, true)).toEqual(
|
||||
'https://gitlab.com/test?labels[]=foo&labels[]=bar',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes all existing URL params and sets a new param when cleanParams=true', () => {
|
||||
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import * as actions from '~/notes/stores/actions';
|
|||
import * as mutationTypes from '~/notes/stores/mutation_types';
|
||||
import mutations from '~/notes/stores/mutations';
|
||||
import * as utils from '~/notes/stores/utils';
|
||||
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
|
||||
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
|
||||
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
|
||||
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
|
||||
|
@ -1276,68 +1275,6 @@ describe('Actions Notes Store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateConfidentialityOnIssuable', () => {
|
||||
state = { noteableData: { confidential: false } };
|
||||
const iid = '1';
|
||||
const projectPath = 'full/path';
|
||||
const getters = { getNoteableData: { iid } };
|
||||
const actionArgs = { fullPath: projectPath, confidential: true };
|
||||
const confidential = true;
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(utils.gqClient, 'mutate')
|
||||
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential } } } });
|
||||
});
|
||||
|
||||
it('calls gqClient mutation one time', () => {
|
||||
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
|
||||
|
||||
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls gqClient mutation with the correct values', () => {
|
||||
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
|
||||
|
||||
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateIssueConfidentialMutation,
|
||||
variables: { input: { iid, projectPath, confidential } },
|
||||
});
|
||||
});
|
||||
|
||||
describe('on success of mutation', () => {
|
||||
it('calls commit with the correct values', () => {
|
||||
const commitSpy = jest.fn();
|
||||
|
||||
return actions
|
||||
.updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs)
|
||||
.then(() => {
|
||||
expect(Flash).not.toHaveBeenCalled();
|
||||
expect(commitSpy).toHaveBeenCalledWith(
|
||||
mutationTypes.SET_ISSUE_CONFIDENTIAL,
|
||||
confidential,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on user recoverable error', () => {
|
||||
it('sends the error to Flash', () => {
|
||||
const error = 'error';
|
||||
|
||||
jest
|
||||
.spyOn(utils.gqClient, 'mutate')
|
||||
.mockResolvedValue({ data: { issueSetConfidential: { errors: [error] } } });
|
||||
|
||||
return actions
|
||||
.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs)
|
||||
.then(() => {
|
||||
expect(Flash).toHaveBeenCalledWith(error, 'alert');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
issuableType
|
||||
${'issue'} | ${'merge_request'}
|
||||
|
|
|
@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
|
||||
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
|
||||
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
@ -15,7 +15,7 @@ describe('Pipelines Actions dropdown', () => {
|
|||
let mock;
|
||||
|
||||
const createComponent = (props, mountFn = shallowMount) => {
|
||||
wrapper = mountFn(PipelinesActions, {
|
||||
wrapper = mountFn(PipelinesManualActions, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
|
||||
|
||||
describe('Sidebar Confidentiality Content', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findText = () => wrapper.find('[data-testid="confidential-text"]');
|
||||
|
||||
const createComponent = (confidential = false) => {
|
||||
wrapper = shallowMount(SidebarConfidentialityContent, {
|
||||
propsData: {
|
||||
confidential,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when issue is non-confidential', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders a non-confidential icon', () => {
|
||||
expect(findIcon().props('name')).toBe('eye');
|
||||
});
|
||||
|
||||
it('does not add `is-active` class to the icon', () => {
|
||||
expect(findIcon().classes()).not.toContain('is-active');
|
||||
});
|
||||
|
||||
it('displays a non-confidential text', () => {
|
||||
expect(findText().text()).toBe('Not confidential');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issue is confidential', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(true);
|
||||
});
|
||||
|
||||
it('renders a non-confidential icon', () => {
|
||||
expect(findIcon().props('name')).toBe('eye-slash');
|
||||
});
|
||||
|
||||
it('does not add `is-active` class to the icon', () => {
|
||||
expect(findIcon().classes()).toContain('is-active');
|
||||
});
|
||||
|
||||
it('displays a non-confidential text', () => {
|
||||
expect(findText().text()).toBe('This is confidential');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
|
||||
import { confidentialityQueries } from '~/sidebar/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('Sidebar Confidentiality Form', () => {
|
||||
let wrapper;
|
||||
|
||||
const findWarningMessage = () => wrapper.find(`[data-testid="warning-message"]`);
|
||||
const findConfidentialToggle = () => wrapper.find(`[data-testid="confidential-toggle"]`);
|
||||
const findCancelButton = () => wrapper.find(`[data-testid="confidential-cancel"]`);
|
||||
|
||||
const createComponent = ({
|
||||
props = {},
|
||||
mutate = jest.fn().mockResolvedValue('Success'),
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(SidebarConfidentialityForm, {
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
iid: '1',
|
||||
},
|
||||
propsData: {
|
||||
confidential: false,
|
||||
issuableType: 'issue',
|
||||
...props,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
mutate,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('emits a `closeForm` event when Cancel button is clicked', () => {
|
||||
createComponent();
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted().closeForm).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a loading state after clicking on turn on/off button', async () => {
|
||||
createComponent();
|
||||
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
expect(findConfidentialToggle().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('creates a flash if mutation is rejected', async () => {
|
||||
createComponent({ mutate: jest.fn().mockRejectedValue('Error!') });
|
||||
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'Something went wrong while setting issue confidentiality.',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a flash if mutation contains errors', async () => {
|
||||
createComponent({
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
|
||||
}),
|
||||
});
|
||||
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'Houston, we have a problem!',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issue is not confidential', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders a message about making an issue confidential', () => {
|
||||
expect(findWarningMessage().text()).toBe(
|
||||
'You are going to turn on confidentiality. Only team members with at least Reporter access will be able to see and leave comments on the issue.',
|
||||
);
|
||||
});
|
||||
|
||||
it('has a `Turn on` button text', () => {
|
||||
expect(findConfidentialToggle().text()).toBe('Turn on');
|
||||
});
|
||||
|
||||
it('calls a mutation to set confidential to true on button click', () => {
|
||||
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
|
||||
variables: {
|
||||
input: {
|
||||
confidential: true,
|
||||
iid: '1',
|
||||
projectPath: 'group/project',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issue is confidential', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { confidential: true } });
|
||||
});
|
||||
|
||||
it('renders a message about making an issue non-confidential', () => {
|
||||
expect(findWarningMessage().text()).toBe(
|
||||
'You are going to turn off the confidentiality. This means everyone will be able to see and leave a comment on this issue.',
|
||||
);
|
||||
});
|
||||
|
||||
it('has a `Turn off` button text', () => {
|
||||
expect(findConfidentialToggle().text()).toBe('Turn off');
|
||||
});
|
||||
|
||||
it('calls a mutation to set confidential to false on button click', () => {
|
||||
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
|
||||
variables: {
|
||||
input: {
|
||||
confidential: false,
|
||||
iid: '1',
|
||||
projectPath: 'group/project',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
|
||||
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
|
||||
import SidebarConfidentialityWidget, {
|
||||
confidentialWidget,
|
||||
} from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
|
||||
import { issueConfidentialityResponse } from '../../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('Sidebar Confidentiality Widget', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
|
||||
const findConfidentialityForm = () => wrapper.findComponent(SidebarConfidentialityForm);
|
||||
const findConfidentialityContent = () => wrapper.findComponent(SidebarConfidentialityContent);
|
||||
|
||||
const createComponent = ({
|
||||
confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[issueConfidentialQuery, confidentialQueryHandler]]);
|
||||
|
||||
wrapper = shallowMount(SidebarConfidentialityWidget, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
iid: '1',
|
||||
canUpdate: true,
|
||||
},
|
||||
propsData: {
|
||||
issuableType: 'issue',
|
||||
},
|
||||
stubs: {
|
||||
SidebarEditableItem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
it('passes a `loading` prop as true to editable item when query is loading', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findEditableItem().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('exposes a method via external observable', () => {
|
||||
createComponent();
|
||||
|
||||
expect(confidentialWidget.setConfidentiality).toEqual(wrapper.vm.setConfidentiality);
|
||||
});
|
||||
|
||||
describe('when issue is not confidential', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('passes a `loading` prop as false to editable item', () => {
|
||||
expect(findEditableItem().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('passes false to `confidential` prop of child components', () => {
|
||||
expect(findConfidentialityForm().props('confidential')).toBe(false);
|
||||
expect(findConfidentialityContent().props('confidential')).toBe(false);
|
||||
});
|
||||
|
||||
it('changes confidentiality to true after setConfidentiality is called', async () => {
|
||||
confidentialWidget.setConfidentiality();
|
||||
await nextTick();
|
||||
expect(findConfidentialityForm().props('confidential')).toBe(true);
|
||||
expect(findConfidentialityContent().props('confidential')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issue is confidential', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('passes a `loading` prop as false to editable item', () => {
|
||||
expect(findEditableItem().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('passes false to `confidential` prop of child components', () => {
|
||||
expect(findConfidentialityForm().props('confidential')).toBe(true);
|
||||
expect(findConfidentialityContent().props('confidential')).toBe(true);
|
||||
});
|
||||
|
||||
it('changes confidentiality to false after setConfidentiality is called', async () => {
|
||||
confidentialWidget.setConfidentiality();
|
||||
await nextTick();
|
||||
expect(findConfidentialityForm().props('confidential')).toBe(false);
|
||||
expect(findConfidentialityContent().props('confidential')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a flash message when query is rejected', async () => {
|
||||
createComponent({
|
||||
confidentialQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
|
||||
createComponent();
|
||||
const el = wrapper.vm.$el;
|
||||
jest.spyOn(el, 'dispatchEvent');
|
||||
|
||||
await waitForPromises();
|
||||
wrapper.vm.$refs.editable.expand();
|
||||
await nextTick();
|
||||
|
||||
expect(findConfidentialityForm().isVisible()).toBe(true);
|
||||
|
||||
findConfidentialityForm().vm.$emit('closeForm');
|
||||
await nextTick();
|
||||
expect(findConfidentialityForm().isVisible()).toBe(false);
|
||||
|
||||
expect(el.dispatchEvent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -220,4 +220,17 @@ const mockData = {
|
|||
},
|
||||
};
|
||||
|
||||
export const issueConfidentialityResponse = (confidential = false) => ({
|
||||
data: {
|
||||
workspace: {
|
||||
__typename: 'Project',
|
||||
issuable: {
|
||||
__typename: 'Issue',
|
||||
id: 'gid://gitlab/Issue/4',
|
||||
confidential,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -5,37 +5,46 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :clean_gitlab_redis_cache do
|
||||
let_it_be(:merge_request) { create(:merged_merge_request) }
|
||||
let(:project) { merge_request.project }
|
||||
let(:created_at) { Time.new(2017, 1, 1, 12, 00).utc }
|
||||
let(:merged_at) { Time.new(2017, 1, 1, 12, 00).utc }
|
||||
let(:client_double) { double(user: double(id: 999, login: 'merger', email: 'merger@email.com')) }
|
||||
|
||||
let(:pull_request) do
|
||||
instance_double(
|
||||
Gitlab::GithubImport::Representation::PullRequest,
|
||||
iid: merge_request.iid,
|
||||
created_at: created_at,
|
||||
merged_at: merged_at,
|
||||
merged_by: double(id: 999, login: 'merger')
|
||||
)
|
||||
end
|
||||
|
||||
subject { described_class.new(pull_request, project, client_double) }
|
||||
|
||||
it 'assigns the merged by user when mapped' do
|
||||
merge_user = create(:user, email: 'merger@email.com')
|
||||
context 'when the merger user can be mapped' do
|
||||
it 'assigns the merged by user when mapped' do
|
||||
merge_user = create(:user, email: 'merger@email.com')
|
||||
|
||||
subject.execute
|
||||
subject.execute
|
||||
|
||||
expect(merge_request.metrics.reload.merged_by).to eq(merge_user)
|
||||
metrics = merge_request.metrics.reload
|
||||
expect(metrics.merged_by).to eq(merge_user)
|
||||
expect(metrics.merged_at).to eq(merged_at)
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a note referencing the merger user when the user cannot be mapped' do
|
||||
expect { subject.execute }
|
||||
.to change(Note, :count).by(1)
|
||||
.and not_change(merge_request, :updated_at)
|
||||
context 'when the merger user cannot be mapped to a gitlab user' do
|
||||
it 'adds a note referencing the merger user' do
|
||||
expect { subject.execute }
|
||||
.to change(Note, :count).by(1)
|
||||
.and not_change(merge_request, :updated_at)
|
||||
|
||||
last_note = merge_request.notes.last
|
||||
metrics = merge_request.metrics.reload
|
||||
expect(metrics.merged_by).to be_nil
|
||||
expect(metrics.merged_at).to eq(merged_at)
|
||||
|
||||
expect(last_note.note).to eq("*Merged by: merger*")
|
||||
expect(last_note.created_at).to eq(created_at)
|
||||
expect(last_note.author).to eq(project.creator)
|
||||
last_note = merge_request.notes.last
|
||||
expect(last_note.note).to eq("*Merged by: merger at 2017-01-01 12:00:00 UTC*")
|
||||
expect(last_note.created_at).to eq(merged_at)
|
||||
expect(last_note.author).to eq(project.creator)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Client, :clean_gitlab_redis_queues do
|
||||
let(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
"TestSizeLimiterWorker"
|
||||
end
|
||||
|
||||
include ApplicationWorker
|
||||
|
||||
def perform(*args); end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("TestSizeLimiterWorker", worker_class)
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
context 'when the validator rejects the job' do
|
||||
before do
|
||||
allow(Gitlab::SidekiqMiddleware::SizeLimiter::Validator).to receive(:validate!).and_raise(
|
||||
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(
|
||||
TestSizeLimiterWorker, 500, 300
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_at' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_at(30.seconds.from_now, 1, 2, 3)
|
||||
end.to raise_error Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_async' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_async(1, 2, 3)
|
||||
end.to raise_error Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_in' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_in(3.seconds, 1, 2, 3)
|
||||
end.to raise_error Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the validator validates the job suscessfully' do
|
||||
before do
|
||||
# Do nothing
|
||||
allow(Gitlab::SidekiqMiddleware::SizeLimiter::Client).to receive(:validate!)
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_at' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_at(30.seconds.from_now, 1, 2, 3)
|
||||
end.not_to raise_error
|
||||
|
||||
expect(TestSizeLimiterWorker.jobs).to contain_exactly(
|
||||
a_hash_including(
|
||||
"class" => "TestSizeLimiterWorker",
|
||||
"args" => [1, 2, 3],
|
||||
"at" => be_a(Float)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_async' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_async(1, 2, 3)
|
||||
end.not_to raise_error
|
||||
|
||||
expect(TestSizeLimiterWorker.jobs).to contain_exactly(
|
||||
a_hash_including(
|
||||
"class" => "TestSizeLimiterWorker",
|
||||
"args" => [1, 2, 3]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises an exception when scheduling job with #perform_in' do
|
||||
expect do
|
||||
TestSizeLimiterWorker.perform_in(3.seconds, 1, 2, 3)
|
||||
end.not_to raise_error
|
||||
|
||||
expect(TestSizeLimiterWorker.jobs).to contain_exactly(
|
||||
a_hash_including(
|
||||
"class" => "TestSizeLimiterWorker",
|
||||
"args" => [1, 2, 3],
|
||||
"at" => be_a(Float)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError do
|
||||
let(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
"TestSizeLimiterWorker"
|
||||
end
|
||||
|
||||
include ApplicationWorker
|
||||
|
||||
def perform(*args); end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("TestSizeLimiterWorker", worker_class)
|
||||
end
|
||||
|
||||
it 'encapsulates worker info' do
|
||||
exception = described_class.new(TestSizeLimiterWorker, 500, 300)
|
||||
|
||||
expect(exception.message).to eql("TestSizeLimiterWorker job exceeds payload size limit (500/300)")
|
||||
expect(exception.worker_class).to eql(TestSizeLimiterWorker)
|
||||
expect(exception.size).to be(500)
|
||||
expect(exception.size_limit).to be(300)
|
||||
expect(exception.sentry_extra_data).to eql(
|
||||
worker_class: 'TestSizeLimiterWorker',
|
||||
size: 500,
|
||||
size_limit: 300
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,253 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
|
||||
let(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
"TestSizeLimiterWorker"
|
||||
end
|
||||
|
||||
include ApplicationWorker
|
||||
|
||||
def perform(*args); end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("TestSizeLimiterWorker", worker_class)
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'when the input mode is valid' do
|
||||
it 'does not log a warning message' do
|
||||
expect(::Sidekiq.logger).not_to receive(:warn)
|
||||
|
||||
described_class.new(TestSizeLimiterWorker, {}, mode: 'track')
|
||||
described_class.new(TestSizeLimiterWorker, {}, mode: 'raise')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the input mode is invalid' do
|
||||
it 'defaults to track mode and logs a warning message' do
|
||||
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter mode: invalid. Fallback to track mode.')
|
||||
|
||||
validator = described_class.new(TestSizeLimiterWorker, {}, mode: 'invalid')
|
||||
|
||||
expect(validator.mode).to eql('track')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the input mode is empty' do
|
||||
it 'defaults to track mode' do
|
||||
expect(::Sidekiq.logger).not_to receive(:warn)
|
||||
|
||||
validator = described_class.new(TestSizeLimiterWorker, {})
|
||||
|
||||
expect(validator.mode).to eql('track')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the size input is valid' do
|
||||
it 'does not log a warning message' do
|
||||
expect(::Sidekiq.logger).not_to receive(:warn)
|
||||
|
||||
described_class.new(TestSizeLimiterWorker, {}, size_limit: 300)
|
||||
described_class.new(TestSizeLimiterWorker, {}, size_limit: 0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the size input is invalid' do
|
||||
it 'defaults to 0 and logs a warning message' do
|
||||
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter limit: -1')
|
||||
|
||||
described_class.new(TestSizeLimiterWorker, {}, size_limit: -1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the size input is empty' do
|
||||
it 'defaults to 0' do
|
||||
expect(::Sidekiq.logger).not_to receive(:warn)
|
||||
|
||||
validator = described_class.new(TestSizeLimiterWorker, {})
|
||||
|
||||
expect(validator.size_limit).to be(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'validate limit job payload size' do
|
||||
context 'in track mode' do
|
||||
let(:mode) { 'track' }
|
||||
|
||||
context 'when size limit negative' do
|
||||
let(:size_limit) { -1 }
|
||||
|
||||
it 'does not track jobs' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
|
||||
end
|
||||
|
||||
it 'does not raise exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when size limit is 0' do
|
||||
let(:size_limit) { 0 }
|
||||
|
||||
it 'does not track jobs' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
|
||||
end
|
||||
|
||||
it 'does not raise exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job size is bigger than size limit' do
|
||||
let(:size_limit) { 50 }
|
||||
|
||||
it 'tracks job' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
||||
be_a(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
|
||||
)
|
||||
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' * 100 })
|
||||
end
|
||||
|
||||
it 'does not raise an exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when the worker has big_payload attribute' do
|
||||
before do
|
||||
worker_class.big_payload!
|
||||
end
|
||||
|
||||
it 'does not track jobs' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
|
||||
validate.call('TestSizeLimiterWorker', { a: 'a' * 300 })
|
||||
end
|
||||
|
||||
it 'does not raise an exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
expect { validate.call('TestSizeLimiterWorker', { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job size is less than size limit' do
|
||||
let(:size_limit) { 50 }
|
||||
|
||||
it 'does not track job' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' })
|
||||
end
|
||||
|
||||
it 'does not raise an exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'in raise mode' do
|
||||
let(:mode) { 'raise' }
|
||||
|
||||
context 'when size limit is negative' do
|
||||
let(:size_limit) { -1 }
|
||||
|
||||
it 'does not raise exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when size limit is 0' do
|
||||
let(:size_limit) { 0 }
|
||||
|
||||
it 'does not raise exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job size is bigger than size limit' do
|
||||
let(:size_limit) { 50 }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect do
|
||||
validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
|
||||
end.to raise_error(
|
||||
Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError,
|
||||
/TestSizeLimiterWorker job exceeds payload size limit/i
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the worker has big_payload attribute' do
|
||||
before do
|
||||
worker_class.big_payload!
|
||||
end
|
||||
|
||||
it 'does not raise an exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
|
||||
expect { validate.call('TestSizeLimiterWorker', { a: 'a' * 300 }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job size is less than size limit' do
|
||||
let(:size_limit) { 50 }
|
||||
|
||||
it 'does not raise an exception' do
|
||||
expect { validate.call(TestSizeLimiterWorker, { a: 'a' }) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate!' do
|
||||
context 'when calling SizeLimiter.validate!' do
|
||||
let(:validate) { ->(worker_clas, job) { described_class.validate!(worker_class, job) } }
|
||||
|
||||
before do
|
||||
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_MODE', mode)
|
||||
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES', size_limit)
|
||||
end
|
||||
|
||||
it_behaves_like 'validate limit job payload size'
|
||||
end
|
||||
|
||||
context 'when creating an instance with the related ENV variables' do
|
||||
let(:validate) do
|
||||
->(worker_clas, job) do
|
||||
validator = described_class.new(worker_class, job, mode: mode, size_limit: size_limit)
|
||||
validator.validate!
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_MODE', mode)
|
||||
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES', size_limit)
|
||||
end
|
||||
|
||||
it_behaves_like 'validate limit job payload size'
|
||||
end
|
||||
|
||||
context 'when creating an instance with mode and size limit' do
|
||||
let(:validate) do
|
||||
->(worker_clas, job) do
|
||||
validator = described_class.new(worker_class, job, mode: mode, size_limit: size_limit)
|
||||
validator.validate!
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'validate limit job payload size'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -177,6 +177,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
|
|||
::Gitlab::SidekiqMiddleware::DuplicateJobs::Client,
|
||||
::Gitlab::SidekiqStatus::ClientMiddleware,
|
||||
::Gitlab::SidekiqMiddleware::AdminMode::Client,
|
||||
::Gitlab::SidekiqMiddleware::SizeLimiter::Client,
|
||||
::Gitlab::SidekiqMiddleware::ClientMetrics
|
||||
]
|
||||
end
|
||||
|
|
|
@ -125,8 +125,9 @@ RSpec.describe Emails::Profile do
|
|||
|
||||
describe 'user personal access token is about to expire' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:expiring_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
|
||||
|
||||
subject { Notify.access_token_about_to_expire_email(user) }
|
||||
subject { Notify.access_token_about_to_expire_email(user, [expiring_token.name]) }
|
||||
|
||||
it_behaves_like 'an email sent from GitLab'
|
||||
it_behaves_like 'it should not have Gmail Actions links'
|
||||
|
@ -137,13 +138,17 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /^Your Personal Access Tokens will expire in 7 days or less$/i
|
||||
is_expected.to have_subject /^Your personal access tokens will expire in 7 days or less$/i
|
||||
end
|
||||
|
||||
it 'mentions the access tokens will expire' do
|
||||
is_expected.to have_body_text /One or more of your personal access tokens will expire in 7 days or less/
|
||||
end
|
||||
|
||||
it 'provides the names of expiring tokens' do
|
||||
is_expected.to have_body_text /#{expiring_token.name}/
|
||||
end
|
||||
|
||||
it 'includes a link to personal access tokens page' do
|
||||
is_expected.to have_body_text /#{profile_personal_access_tokens_path}/
|
||||
end
|
||||
|
|
|
@ -833,7 +833,7 @@ RSpec.describe Namespace do
|
|||
end
|
||||
|
||||
describe '#all_projects' do
|
||||
shared_examples 'all projects for a group' do
|
||||
context 'when namespace is a group' do
|
||||
let(:namespace) { create(:group) }
|
||||
let(:child) { create(:group, parent: namespace) }
|
||||
let!(:project1) { create(:project_empty_repo, namespace: namespace) }
|
||||
|
@ -841,49 +841,25 @@ RSpec.describe Namespace do
|
|||
|
||||
it { expect(namespace.all_projects.to_a).to match_array([project2, project1]) }
|
||||
it { expect(child.all_projects.to_a).to match_array([project2]) }
|
||||
|
||||
it 'queries for the namespace and its descendants' do
|
||||
expect(Project).to receive(:where).with(namespace: [namespace, child])
|
||||
|
||||
namespace.all_projects
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'all projects for personal namespace' do
|
||||
context 'when namespace is a user namespace' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user_namespace) { create(:namespace, owner: user) }
|
||||
let_it_be(:project) { create(:project, namespace: user_namespace) }
|
||||
|
||||
it { expect(user_namespace.all_projects.to_a).to match_array([project]) }
|
||||
end
|
||||
|
||||
context 'with recursive approach' do
|
||||
context 'when namespace is a group' do
|
||||
include_examples 'all projects for a group'
|
||||
it 'only queries for the namespace itself' do
|
||||
expect(Project).to receive(:where).with(namespace: user_namespace)
|
||||
|
||||
it 'queries for the namespace and its descendants' do
|
||||
expect(Project).to receive(:where).with(namespace: [namespace, child])
|
||||
|
||||
namespace.all_projects
|
||||
end
|
||||
end
|
||||
|
||||
context 'when namespace is a user namespace' do
|
||||
include_examples 'all projects for personal namespace'
|
||||
|
||||
it 'only queries for the namespace itself' do
|
||||
expect(Project).to receive(:where).with(namespace: user_namespace)
|
||||
|
||||
user_namespace.all_projects
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with route path wildcard approach' do
|
||||
before do
|
||||
stub_feature_flags(recursive_approach_for_all_projects: false)
|
||||
end
|
||||
|
||||
context 'when namespace is a group' do
|
||||
include_examples 'all projects for a group'
|
||||
end
|
||||
|
||||
context 'when namespace is a user namespace' do
|
||||
include_examples 'all projects for personal namespace'
|
||||
user_namespace.all_projects
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Jobs do
|
||||
include HttpBasicAuthHelpers
|
||||
include DependencyProxyHelpers
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include HttpIOHelpers
|
||||
|
||||
|
@ -16,20 +19,150 @@ RSpec.describe API::Jobs do
|
|||
ref: project.default_branch)
|
||||
end
|
||||
|
||||
let!(:job) do
|
||||
create(:ci_build, :success, :tags, pipeline: pipeline,
|
||||
artifacts_expire_at: 1.day.since)
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:api_user) { user }
|
||||
let(:reporter) { create(:project_member, :reporter, project: project).user }
|
||||
let(:guest) { create(:project_member, :guest, project: project).user }
|
||||
|
||||
let(:running_job) do
|
||||
create(:ci_build, :running, project: project,
|
||||
user: user,
|
||||
pipeline: pipeline,
|
||||
artifacts_expire_at: 1.day.since)
|
||||
end
|
||||
|
||||
let!(:job) do
|
||||
create(:ci_build, :success, :tags, pipeline: pipeline,
|
||||
artifacts_expire_at: 1.day.since)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
shared_examples 'returns common pipeline data' do
|
||||
it 'returns common pipeline data' do
|
||||
expect(json_response['pipeline']).not_to be_empty
|
||||
expect(json_response['pipeline']['id']).to eq jobx.pipeline.id
|
||||
expect(json_response['pipeline']['ref']).to eq jobx.pipeline.ref
|
||||
expect(json_response['pipeline']['sha']).to eq jobx.pipeline.sha
|
||||
expect(json_response['pipeline']['status']).to eq jobx.pipeline.status
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'returns common job data' do
|
||||
it 'returns common job data' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['id']).to eq(jobx.id)
|
||||
expect(json_response['status']).to eq(jobx.status)
|
||||
expect(json_response['stage']).to eq(jobx.stage)
|
||||
expect(json_response['name']).to eq(jobx.name)
|
||||
expect(json_response['ref']).to eq(jobx.ref)
|
||||
expect(json_response['tag']).to eq(jobx.tag)
|
||||
expect(json_response['coverage']).to eq(jobx.coverage)
|
||||
expect(json_response['allow_failure']).to eq(jobx.allow_failure)
|
||||
expect(Time.parse(json_response['created_at'])).to be_like_time(jobx.created_at)
|
||||
expect(Time.parse(json_response['started_at'])).to be_like_time(jobx.started_at)
|
||||
expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(jobx.artifacts_expire_at)
|
||||
expect(json_response['artifacts_file']).to be_nil
|
||||
expect(json_response['artifacts']).to be_an Array
|
||||
expect(json_response['artifacts']).to be_empty
|
||||
expect(json_response['web_url']).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'returns unauthorized' do
|
||||
it 'returns unauthorized' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /job' do
|
||||
shared_context 'with auth headers' do
|
||||
let(:headers_with_token) { header }
|
||||
let(:params_with_token) { {} }
|
||||
end
|
||||
|
||||
shared_context 'with auth params' do
|
||||
let(:headers_with_token) { {} }
|
||||
let(:params_with_token) { param }
|
||||
end
|
||||
|
||||
shared_context 'without auth' do
|
||||
let(:headers_with_token) { {} }
|
||||
let(:params_with_token) { {} }
|
||||
end
|
||||
|
||||
before do |example|
|
||||
unless example.metadata[:skip_before_request]
|
||||
get api('/job'), headers: headers_with_token, params: params_with_token
|
||||
end
|
||||
end
|
||||
|
||||
context 'with job token authentication header' do
|
||||
include_context 'with auth headers' do
|
||||
let(:header) { { API::Helpers::Runner::JOB_TOKEN_HEADER => running_job.token } }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns common job data' do
|
||||
let(:jobx) { running_job }
|
||||
end
|
||||
|
||||
it 'returns specific job data' do
|
||||
expect(json_response['finished_at']).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'returns common pipeline data' do
|
||||
let(:jobx) { running_job }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with job token authentication params' do
|
||||
include_context 'with auth params' do
|
||||
let(:param) { { job_token: running_job.token } }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns common job data' do
|
||||
let(:jobx) { running_job }
|
||||
end
|
||||
|
||||
it 'returns specific job data' do
|
||||
expect(json_response['finished_at']).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'returns common pipeline data' do
|
||||
let(:jobx) { running_job }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non running job' do
|
||||
include_context 'with auth headers' do
|
||||
let(:header) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns unauthorized'
|
||||
end
|
||||
|
||||
context 'with basic auth header' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:token) { personal_access_token.token}
|
||||
|
||||
include_context 'with auth headers' do
|
||||
let(:header) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => token } }
|
||||
end
|
||||
|
||||
it 'does not return a job' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without authentication' do
|
||||
include_context 'without auth'
|
||||
|
||||
it_behaves_like 'returns unauthorized'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/jobs' do
|
||||
let(:query) { {} }
|
||||
|
||||
|
@ -150,39 +283,21 @@ RSpec.describe API::Jobs do
|
|||
end
|
||||
|
||||
context 'authorized user' do
|
||||
it_behaves_like 'returns common job data' do
|
||||
let(:jobx) { job }
|
||||
end
|
||||
|
||||
it 'returns specific job data' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['id']).to eq(job.id)
|
||||
expect(json_response['status']).to eq(job.status)
|
||||
expect(json_response['stage']).to eq(job.stage)
|
||||
expect(json_response['name']).to eq(job.name)
|
||||
expect(json_response['ref']).to eq(job.ref)
|
||||
expect(json_response['tag']).to eq(job.tag)
|
||||
expect(json_response['coverage']).to eq(job.coverage)
|
||||
expect(json_response['allow_failure']).to eq(job.allow_failure)
|
||||
expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at)
|
||||
expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
|
||||
expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
|
||||
expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
|
||||
expect(json_response['artifacts_file']).to be_nil
|
||||
expect(json_response['artifacts']).to be_an Array
|
||||
expect(json_response['artifacts']).to be_empty
|
||||
expect(json_response['duration']).to eq(job.duration)
|
||||
expect(json_response['web_url']).to be_present
|
||||
end
|
||||
|
||||
it_behaves_like 'a job with artifacts and trace', result_is_array: false do
|
||||
let(:api_endpoint) { "/projects/#{project.id}/jobs/#{second_job.id}" }
|
||||
end
|
||||
|
||||
it 'returns pipeline data' do
|
||||
json_job = json_response
|
||||
|
||||
expect(json_job['pipeline']).not_to be_empty
|
||||
expect(json_job['pipeline']['id']).to eq job.pipeline.id
|
||||
expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
|
||||
expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
|
||||
expect(json_job['pipeline']['status']).to eq job.pipeline.status
|
||||
it_behaves_like 'returns common pipeline data' do
|
||||
let(:jobx) { job }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -243,11 +243,12 @@ RSpec.describe NotificationService, :mailer do
|
|||
describe 'AccessToken' do
|
||||
describe '#access_token_about_to_expire' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:pat) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
|
||||
|
||||
subject { notification.access_token_about_to_expire(user) }
|
||||
subject { notification.access_token_about_to_expire(user, [pat.name]) }
|
||||
|
||||
it 'sends email to the token owner' do
|
||||
expect { subject }.to have_enqueued_email(user, mail: "access_token_about_to_expire_email")
|
||||
expect { subject }.to have_enqueued_email(user, [pat.name], mail: "access_token_about_to_expire_email")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -279,6 +279,10 @@ module GpgHelpers
|
|||
KEY
|
||||
end
|
||||
|
||||
def primary_keyid2
|
||||
fingerprint2[-16..-1]
|
||||
end
|
||||
|
||||
def fingerprint2
|
||||
'C447A6F6BFD9CEF8FB371785571625A930241179'
|
||||
end
|
||||
|
|
|
@ -7,18 +7,23 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
|
||||
describe '#perform' do
|
||||
context 'when a token needs to be notified' do
|
||||
let_it_be(:pat) { create(:personal_access_token, expires_at: 5.days.from_now) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:expiring_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
|
||||
let_it_be(:expiring_token2) { create(:personal_access_token, user: user, expires_at: 3.days.from_now) }
|
||||
let_it_be(:notified_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now, expire_notification_delivered: true) }
|
||||
let_it_be(:not_expiring_token) { create(:personal_access_token, user: user, expires_at: 1.month.from_now) }
|
||||
let_it_be(:impersonation_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now, impersonation: true) }
|
||||
|
||||
it 'uses notification service to send the email' do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
expect(notification_service).to receive(:access_token_about_to_expire).with(pat.user)
|
||||
expect(notification_service).to receive(:access_token_about_to_expire).with(user, match_array([expiring_token.name, expiring_token2.name]))
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
it 'marks the notification as delivered' do
|
||||
expect { worker.perform }.to change { pat.reload.expire_notification_delivered }.from(false).to(true)
|
||||
expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,7 +32,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
|
||||
it "doesn't use notification service to send the email" do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user)
|
||||
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user, [pat.name])
|
||||
end
|
||||
|
||||
worker.perform
|
||||
|
@ -43,7 +48,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
|
||||
it "doesn't use notification service to send the email" do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user)
|
||||
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user, [pat.name])
|
||||
end
|
||||
|
||||
worker.perform
|
||||
|
|
51
yarn.lock
51
yarn.lock
|
@ -889,29 +889,29 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/favicon-overlay/-/favicon-overlay-2.0.0.tgz#2f32d0b6a4d5b8ac44e2927083d9ab478a78c984"
|
||||
integrity sha512-GNcORxXJ98LVGzOT9dDYKfbheqH6lNgPDD72lyXRnQIH7CjgGyos8i17aSBPq1f4s3zF3PyedFiAR4YEZbva2Q==
|
||||
|
||||
"@gitlab/stylelint-config@2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/stylelint-config/-/stylelint-config-2.2.0.tgz#f0139c8bd29525b51ee9f16d26b66283bd2be5bb"
|
||||
integrity sha512-yLBwRu/geN7nGzoOtF6VV2Fbjhcu2w3PwVnJ5/6wX3MILLO7Wh8zzurIjjSnls8124WUoD7n51Catrjl0hyqDw==
|
||||
"@gitlab/stylelint-config@2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/stylelint-config/-/stylelint-config-2.3.0.tgz#b27e8544ff52a4c5e23ff7a104c7efff1f7078f0"
|
||||
integrity sha512-8aGdBjNO05xadeGb8GSCyRdr1QcRDaDTUTTjKlD2CfFqvAmaQpCk6NdFMMSHlWQpHHMxS5hQeWiC47sIWON4iQ==
|
||||
dependencies:
|
||||
stylelint "^13.2.1"
|
||||
stylelint-declaration-strict-value "^1.7.7"
|
||||
stylelint-scss "^3.18.0"
|
||||
stylelint "13.9.0"
|
||||
stylelint-declaration-strict-value "1.7.7"
|
||||
stylelint-scss "3.18.0"
|
||||
|
||||
"@gitlab/svgs@1.182.0":
|
||||
version "1.182.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.182.0.tgz#600cb577c598ff63325c3f6f049e3254abdbb580"
|
||||
integrity sha512-hV6Hkd92bNyzGm2awdwtyXVjT71QZkfAMVV9qHg9fhu5swA4qLrxzXpYBBfeY3/e2KJIByVfROP8tSFws0R6Kw==
|
||||
"@gitlab/svgs@1.183.0":
|
||||
version "1.183.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.183.0.tgz#58a3f49d2355531bfc073f35cbd71855b4a0f9da"
|
||||
integrity sha512-cOcFlgmTkRhfljT+0arKFtkYMLFMXLk0q74WtMqb9YFRtuPmthqfH0zZd47XZ2yFTSvbEJfcyAXXrPIuX8f8Rg==
|
||||
|
||||
"@gitlab/tributejs@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@27.7.1":
|
||||
version "27.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-27.7.1.tgz#e62a9a8d96a5f0e61f09fdbbe1771027caaa7aa4"
|
||||
integrity sha512-LRmP2SD73NT9ntvKw1pJShPP6DmjCBmu+mBSZiAj535AGpA+GoQcyi+PTXY12nUjnsPwulaFOOhvFPo8IeFm2Q==
|
||||
"@gitlab/ui@27.14.0":
|
||||
version "27.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-27.14.0.tgz#f80c11df3650380f66aa763867afa32d1901d407"
|
||||
integrity sha512-cQaMN4pjcV0lCic3w1eiZUru9kV8XHwI3ZRT3LOWoBRQEbK0r3qv71SNKzEGDOtVPYq+WBl0/PRDlThk9H+CAg==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
@ -919,7 +919,7 @@
|
|||
copy-to-clipboard "^3.0.8"
|
||||
dompurify "^2.2.6"
|
||||
echarts "^4.9.0"
|
||||
highlight.js "^9.13.1"
|
||||
highlight.js "^10.6.0"
|
||||
js-beautify "^1.8.8"
|
||||
lodash "^4.17.20"
|
||||
portal-vue "^2.1.6"
|
||||
|
@ -5923,7 +5923,12 @@ he@^1.1.0, he@^1.1.1, he@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
highlight.js@^9.13.1, highlight.js@~9.13.0:
|
||||
highlight.js@^10.6.0:
|
||||
version "10.6.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.6.0.tgz#0073aa71d566906965ba6e1b7be7b2682f5e18b6"
|
||||
integrity sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==
|
||||
|
||||
highlight.js@~9.13.0:
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
|
||||
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
|
||||
|
@ -11278,7 +11283,7 @@ style-search@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
|
||||
integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
|
||||
|
||||
stylelint-declaration-strict-value@^1.7.7:
|
||||
stylelint-declaration-strict-value@1.7.7:
|
||||
version "1.7.7"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.7.7.tgz#d2f0aabc7f3e701a8988207f27d9696bd1d1ed0d"
|
||||
integrity sha512-Gr5RZYMIS7af6N6PGRD3vmCJM/NlKY4D/jWdidQqxcXkhBtsmV6C99GjQOB0nfdYtjfJEQZLMlTNBztY4tRGfA==
|
||||
|
@ -11286,10 +11291,10 @@ stylelint-declaration-strict-value@^1.7.7:
|
|||
css-values "^0.1.0"
|
||||
shortcss "^0.1.3"
|
||||
|
||||
stylelint-scss@^3.18.0:
|
||||
version "3.19.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.19.0.tgz#528006d5a4c5a0f1f4d709b02fd3f626ed66d742"
|
||||
integrity sha512-Ic5bsmpS4wVucOw44doC1Yi9f5qbeVL4wPFiEOaUElgsOuLEN6Ofn/krKI8BeNL2gAn53Zu+IcVV4E345r6rBw==
|
||||
stylelint-scss@3.18.0:
|
||||
version "3.18.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.18.0.tgz#8f06371c223909bf3f62e839548af1badeed31e9"
|
||||
integrity sha512-LD7+hv/6/ApNGt7+nR/50ft7cezKP2HM5rI8avIdGaUWre3xlHfV4jKO/DRZhscfuN+Ewy9FMhcTq0CcS0C/SA==
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
postcss-media-query-parser "^0.2.3"
|
||||
|
@ -11297,7 +11302,7 @@ stylelint-scss@^3.18.0:
|
|||
postcss-selector-parser "^6.0.2"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
stylelint@^13.2.1:
|
||||
stylelint@13.9.0:
|
||||
version "13.9.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.9.0.tgz#93921ee6e11d4556b9f31131f485dc813b68e32a"
|
||||
integrity sha512-VVWH2oixOAxpWL1vH+V42ReCzBjW2AeqskSAbi8+3OjV1Xg3VZkmTcAqBZfRRvJeF4BvYuDLXebW3tIHxgZDEg==
|
||||
|
|
Loading…
Reference in New Issue