Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4d922922a9
commit
1ca6880aac
|
|
@ -9,6 +9,9 @@
|
|||
.rspec-base:
|
||||
extends: .rails-job-base
|
||||
stage: test
|
||||
variables:
|
||||
RUBY_GC_MALLOC_LIMIT: 67108864
|
||||
RUBY_GC_MALLOC_LIMIT_MAX: 134217728
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"]
|
||||
script:
|
||||
# Only install knapsack after bundle install! Otherwise oddly some native
|
||||
|
|
|
|||
|
|
@ -81,10 +81,9 @@ review-deploy:
|
|||
# Run seed-dast-test-data.sh only when DAST_RUN is set to true. This is to pupulate review app with data for DAST scan.
|
||||
# Set DAST_RUN to true when jobs are manually scheduled.
|
||||
- if [ "$DAST_RUN" == "true" ]; then source scripts/review_apps/seed-dast-test-data.sh; TRACE=1 trigger_proj_user_creation; fi
|
||||
|
||||
artifacts:
|
||||
paths: [environment_url.txt]
|
||||
expire_in: 2 days
|
||||
expire_in: 7 days
|
||||
when: always
|
||||
|
||||
.review-stop-base:
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@
|
|||
|
||||
.db-patterns: &db-patterns
|
||||
- "{,ee/}{,spec/}{db,migrations}/**/*"
|
||||
- "{,ee/}{,spec/}lib/{,ee/}gitlab/database/**/*"
|
||||
- "{,ee/}{,spec/}lib/{,ee/}gitlab/database{,_spec}.rb"
|
||||
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration/**/*"
|
||||
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration{,_spec}.rb"
|
||||
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
|
||||
- "{,ee/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
65cd98f93c072f3a536021462c56e686cb2f8c7b
|
||||
0fc40ef439ae4bbf91da2a5b454dfad5cb815a17
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -111,7 +111,7 @@ gem 'hamlit', '~> 2.11.0'
|
|||
|
||||
# Files attachments
|
||||
gem 'carrierwave', '~> 1.3'
|
||||
gem 'mini_magick'
|
||||
gem 'mini_magick', '~> 4.10.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 3.5'
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ GEM
|
|||
mime-types-data (3.2020.0512)
|
||||
mimemagic (0.3.5)
|
||||
mini_histogram (0.1.3)
|
||||
mini_magick (4.9.5)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
|
|
@ -1384,7 +1384,7 @@ DEPENDENCIES
|
|||
memory_profiler (~> 0.9)
|
||||
method_source (~> 1.0)
|
||||
mimemagic (~> 0.3.2)
|
||||
mini_magick
|
||||
mini_magick (~> 4.10.1)
|
||||
minitest (~> 5.11.0)
|
||||
multi_json (~> 1.14.1)
|
||||
nakayoshi_fork (~> 0.0.4)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Autosize from 'autosize';
|
||||
import { waitForCSSLoaded } from '../helpers/startup_css_helper';
|
||||
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
waitForCSSLoaded(() => {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default {
|
|||
<stop-environment-modal :environment="environmentInStopModal" />
|
||||
<delete-environment-modal :environment="environmentInDeleteModal" />
|
||||
|
||||
<h4 class="js-folder-name environments-folder-name">
|
||||
<h4 class="gl-font-weight-normal" data-testid="folder-name">
|
||||
{{ s__('Environments|Environments') }} /
|
||||
<b>{{ folderName }}</b>
|
||||
</h4>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const initGroupMembersApp = (el, tableFields) => {
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const { members, groupId } = el.dataset;
|
||||
const { members, groupId, memberPath } = el.dataset;
|
||||
|
||||
const store = new Vuex.Store({
|
||||
...membersModule({
|
||||
|
|
@ -19,6 +19,7 @@ export const initGroupMembersApp = (el, tableFields) => {
|
|||
sourceId: parseInt(groupId, 10),
|
||||
currentUserId: gon.current_user_id || null,
|
||||
tableFields,
|
||||
memberPath,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -77,13 +77,19 @@ export default class Members {
|
|||
|
||||
$expiresInText.text(sprintf(__('Expires in %{expires_at}'), { expires_at: expiresIn }));
|
||||
|
||||
const { expires_soon: expiresSoon } = data;
|
||||
const { expires_soon: expiresSoon, expires_at_formatted: expiresAtFormatted } = data;
|
||||
|
||||
if (expiresSoon) {
|
||||
$expiresInText.addClass('text-warning');
|
||||
} else {
|
||||
$expiresInText.removeClass('text-warning');
|
||||
}
|
||||
|
||||
// Update tooltip
|
||||
if (expiresAtFormatted) {
|
||||
$expiresInText.attr('title', expiresAtFormatted);
|
||||
$expiresInText.attr('data-original-title', expiresAtFormatted);
|
||||
}
|
||||
} else {
|
||||
$expiresIn.addClass('gl-display-none');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default {
|
|||
<div class="discussion-with-resolve-btn clearfix">
|
||||
<reply-placeholder
|
||||
data-qa-selector="discussion_reply_tab"
|
||||
:button-text="s__('MergeRequests|Reply...')"
|
||||
:button-text="s__('MergeRequests|Reply')"
|
||||
@onClick="$emit('showReplyForm')"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'ReplyPlaceholder',
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
buttonText: {
|
||||
type: String,
|
||||
|
|
@ -11,13 +16,13 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
ref="button"
|
||||
type="button"
|
||||
class="js-vue-discussion-reply btn btn-text-field"
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="success"
|
||||
class="js-vue-discussion-reply"
|
||||
:title="s__('MergeRequests|Add a reply')"
|
||||
@click="$emit('onClick')"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
<script>
|
||||
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { n__, s__, sprintf } from '~/locale';
|
||||
import { __, n__, s__, sprintf } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeprecatedModal,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
|
|
@ -115,20 +114,24 @@ Once deleted, it cannot be undone or recovered.`),
|
|||
});
|
||||
},
|
||||
},
|
||||
primaryProps: {
|
||||
text: s__('Milestones|Delete milestone'),
|
||||
attributes: [{ variant: 'danger' }, { category: 'primary' }],
|
||||
},
|
||||
cancelProps: {
|
||||
text: __('Cancel'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<deprecated-modal
|
||||
id="delete-milestone-modal"
|
||||
<gl-modal
|
||||
modal-id="delete-milestone-modal"
|
||||
:title="title"
|
||||
:text="text"
|
||||
:primary-button-label="s__('Milestones|Delete milestone')"
|
||||
kind="danger"
|
||||
@submit="onSubmit"
|
||||
:action-primary="$options.primaryProps"
|
||||
:action-cancel="$options.cancelProps"
|
||||
@primary="onSubmit"
|
||||
>
|
||||
<template #body="props">
|
||||
<p v-safe-html="props.text"></p>
|
||||
</template>
|
||||
</deprecated-modal>
|
||||
<p v-safe-html="text"></p>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
|
||||
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
|
||||
import eventHub from './event_hub';
|
||||
|
||||
export default () => {
|
||||
|
|
@ -18,6 +18,8 @@ export default () => {
|
|||
button.querySelector('.js-loading-icon').classList.add('hidden');
|
||||
};
|
||||
|
||||
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
|
||||
|
||||
const onRequestStarted = milestoneUrl => {
|
||||
const button = document.querySelector(
|
||||
`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
|
||||
|
|
@ -27,35 +29,8 @@ export default () => {
|
|||
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
|
||||
};
|
||||
|
||||
const onDeleteButtonClick = event => {
|
||||
const button = event.currentTarget;
|
||||
const modalProps = {
|
||||
milestoneId: parseInt(button.dataset.milestoneId, 10),
|
||||
milestoneTitle: button.dataset.milestoneTitle,
|
||||
milestoneUrl: button.dataset.milestoneUrl,
|
||||
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
|
||||
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
|
||||
};
|
||||
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
|
||||
eventHub.$emit('deleteMilestoneModal.props', modalProps);
|
||||
};
|
||||
|
||||
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
|
||||
deleteMilestoneButtons.forEach(button => {
|
||||
button.addEventListener('click', onDeleteButtonClick);
|
||||
});
|
||||
|
||||
eventHub.$once('deleteMilestoneModal.mounted', () => {
|
||||
deleteMilestoneButtons.forEach(button => {
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: '#delete-milestone-modal',
|
||||
components: {
|
||||
deleteMilestoneModal,
|
||||
},
|
||||
el: '#js-delete-milestone-modal',
|
||||
data() {
|
||||
return {
|
||||
modalProps: {
|
||||
|
|
@ -69,10 +44,21 @@ export default () => {
|
|||
},
|
||||
mounted() {
|
||||
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
|
||||
eventHub.$emit('deleteMilestoneModal.mounted');
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
|
||||
deleteMilestoneButtons.forEach(button => {
|
||||
button.removeAttribute('disabled');
|
||||
button.addEventListener('click', () => {
|
||||
this.$root.$emit('bv::show::modal', 'delete-milestone-modal');
|
||||
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
|
||||
|
||||
this.setModalProps({
|
||||
milestoneId: parseInt(button.dataset.milestoneId, 10),
|
||||
milestoneTitle: button.dataset.milestoneTitle,
|
||||
milestoneUrl: button.dataset.milestoneUrl,
|
||||
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
|
||||
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
setModalProps(modalProps) {
|
||||
|
|
@ -80,7 +66,7 @@ export default () => {
|
|||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(deleteMilestoneModal, {
|
||||
return createElement(DeleteMilestoneModal, {
|
||||
props: this.modalProps,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import { GlColumnChart } from '@gitlab/ui/dist/charts';
|
||||
import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper';
|
||||
import Vue from 'vue';
|
||||
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
|
||||
import { __ } from '~/locale';
|
||||
import CodeCoverage from '../components/code_coverage.vue';
|
||||
import SeriesDataMixin from './series_data_mixin';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
<script>
|
||||
import Visibility from 'visibilityjs';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import ciIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import CommitPipelineService from '../services/commit_pipeline_service';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
ciIcon,
|
||||
|
|
@ -97,7 +96,7 @@ export default {
|
|||
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
|
||||
<a v-else :href="ciStatus.details_path">
|
||||
<ci-icon
|
||||
v-tooltip
|
||||
v-gl-tooltip
|
||||
:title="statusTitle"
|
||||
:aria-label="statusTitle"
|
||||
:status="ciStatus"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import SnippetBlobEdit from './snippet_blob_edit.vue';
|
||||
import { SNIPPET_MAX_BLOBS } from '../constants';
|
||||
import { createBlob, decorateBlob, diffAll } from '../utils/blob';
|
||||
|
|
@ -12,7 +11,6 @@ export default {
|
|||
SnippetBlobEdit,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
initBlobs: {
|
||||
type: Array,
|
||||
|
|
@ -52,12 +50,6 @@ export default {
|
|||
canAdd() {
|
||||
return this.count < SNIPPET_MAX_BLOBS;
|
||||
},
|
||||
hasMultiFilesEnabled() {
|
||||
return this.glFeatures.snippetMultipleFiles;
|
||||
},
|
||||
filesLabel() {
|
||||
return this.hasMultiFilesEnabled ? s__('Snippets|Files') : s__('Snippets|File');
|
||||
},
|
||||
firstInputId() {
|
||||
const blobId = this.blobIds[0];
|
||||
|
||||
|
|
@ -132,19 +124,17 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="form-group file-editor">
|
||||
<label :for="firstInputId">{{ filesLabel }}</label>
|
||||
<label :for="firstInputId">{{ s__('Snippets|Files') }}</label>
|
||||
<snippet-blob-edit
|
||||
v-for="(blobId, index) in blobIds"
|
||||
:key="blobId"
|
||||
:class="{ 'gl-mt-3': index > 0 }"
|
||||
:blob="blobs[blobId]"
|
||||
:can-delete="canDelete"
|
||||
:show-delete="hasMultiFilesEnabled"
|
||||
@blob-updated="updateBlob(blobId, $event)"
|
||||
@delete="deleteBlob(blobId)"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="hasMultiFilesEnabled"
|
||||
:disabled="!canAdd"
|
||||
data-testid="add_button"
|
||||
class="gl-my-3"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default {
|
|||
showDelete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import ActionButtonGroup from './action_button_group.vue';
|
||||
import RemoveMemberButton from './remove_member_button.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'AccessRequestActionButtons',
|
||||
components: { ActionButtonGroup, RemoveMemberButton },
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
permissions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCurrentUser: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
const { user, source } = this.member;
|
||||
|
||||
if (this.isCurrentUser) {
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to withdraw your access request for "%{source}"'),
|
||||
{ source: source.name },
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to deny %{usersName}\'s request to join "%{source}"'),
|
||||
{ usersName: user.name, source: source.name },
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-button-group>
|
||||
<!-- Approve button will go here -->
|
||||
<div v-if="permissions.canRemove" class="gl-px-1">
|
||||
<remove-member-button
|
||||
:member-id="member.id"
|
||||
:message="message"
|
||||
:title="s__('Member|Deny access')"
|
||||
:is-access-request="true"
|
||||
icon="close"
|
||||
/>
|
||||
</div>
|
||||
</action-button-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'ActionButtonGroup',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-flex-align-items-center gl-justify-content-end gl-mx-n1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'GroupActionButtons',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<!-- Temporarily empty -->
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import ActionButtonGroup from './action_button_group.vue';
|
||||
import RemoveMemberButton from './remove_member_button.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'InviteActionButtons',
|
||||
components: { ActionButtonGroup, RemoveMemberButton },
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
permissions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
const { invite, source } = this.member;
|
||||
|
||||
return sprintf(
|
||||
s__(
|
||||
'Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join "%{source}"',
|
||||
),
|
||||
{ inviteEmail: invite.email, source: source.name },
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-button-group>
|
||||
<!-- Resend button will go here -->
|
||||
<div v-if="permissions.canRemove" class="gl-px-1">
|
||||
<remove-member-button
|
||||
:member-id="member.id"
|
||||
:message="message"
|
||||
:title="s__('Member|Revoke invite')"
|
||||
/>
|
||||
</div>
|
||||
</action-button-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'RemoveMemberButton',
|
||||
components: { GlButton },
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
memberId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'remove',
|
||||
},
|
||||
isAccessRequest: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['memberPath']),
|
||||
computedMemberPath() {
|
||||
return this.memberPath.replace(':id', this.memberId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button
|
||||
v-gl-tooltip.hover
|
||||
class="js-remove-member-button"
|
||||
variant="danger"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
:icon="icon"
|
||||
:data-member-path="computedMemberPath"
|
||||
:data-is-access-request="isAccessRequest"
|
||||
:data-message="message"
|
||||
data-qa-selector="delete_member_button"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import ActionButtonGroup from './action_button_group.vue';
|
||||
import RemoveMemberButton from './remove_member_button.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'UserActionButtons',
|
||||
components: { ActionButtonGroup, RemoveMemberButton },
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCurrentUser: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
permissions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
const { user, source } = this.member;
|
||||
|
||||
if (user) {
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'),
|
||||
{
|
||||
usersName: user.name,
|
||||
source: source.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'),
|
||||
{
|
||||
source: source.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-button-group>
|
||||
<div v-if="permissions.canRemove" class="gl-px-1">
|
||||
<template v-if="isCurrentUser">
|
||||
<!-- Leave button will go here -->
|
||||
</template>
|
||||
<remove-member-button
|
||||
v-else
|
||||
:member-id="member.id"
|
||||
:message="message"
|
||||
:title="s__('Member|Remove member')"
|
||||
/>
|
||||
</div>
|
||||
</action-button-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import UserActionButtons from '../action_buttons/user_action_buttons.vue';
|
||||
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
|
||||
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
|
||||
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
|
||||
import { MEMBER_TYPES } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'MemberActionButtons',
|
||||
components: {
|
||||
UserActionButtons,
|
||||
GroupActionButtons,
|
||||
InviteActionButtons,
|
||||
AccessRequestActionButtons,
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
memberType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
permissions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCurrentUser: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
actionButtonComponent() {
|
||||
const dictionary = {
|
||||
[MEMBER_TYPES.user]: 'user-action-buttons',
|
||||
[MEMBER_TYPES.group]: 'group-action-buttons',
|
||||
[MEMBER_TYPES.invite]: 'invite-action-buttons',
|
||||
[MEMBER_TYPES.accessRequest]: 'access-request-action-buttons',
|
||||
};
|
||||
|
||||
return dictionary[this.memberType];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="actionButtonComponent"
|
||||
v-if="actionButtonComponent"
|
||||
:member="member"
|
||||
:permissions="permissions"
|
||||
:is-current-user="isCurrentUser"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -7,6 +7,7 @@ import MemberAvatar from './member_avatar.vue';
|
|||
import MemberSource from './member_source.vue';
|
||||
import CreatedAt from './created_at.vue';
|
||||
import ExpiresAt from './expires_at.vue';
|
||||
import MemberActionButtons from './member_action_buttons.vue';
|
||||
import MembersTableCell from './members_table_cell.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -18,6 +19,7 @@ export default {
|
|||
ExpiresAt,
|
||||
MembersTableCell,
|
||||
MemberSource,
|
||||
MemberActionButtons,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['members', 'tableFields']),
|
||||
|
|
@ -75,6 +77,17 @@ export default {
|
|||
<expires-at :date="expiresAt" />
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ item: member }">
|
||||
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
|
||||
<member-action-buttons
|
||||
:member-type="memberType"
|
||||
:is-current-user="isCurrentUser"
|
||||
:permissions="permissions"
|
||||
:member="member"
|
||||
/>
|
||||
</members-table-cell>
|
||||
</template>
|
||||
|
||||
<template #head(actions)="{ label }">
|
||||
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -38,12 +38,18 @@ export default {
|
|||
isCurrentUser() {
|
||||
return this.member.user?.id === this.currentUserId;
|
||||
},
|
||||
canRemove() {
|
||||
return this.isDirectMember && this.member.canRemove;
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.$scopedSlots.default({
|
||||
memberType: this.memberType,
|
||||
isDirectMember: this.isDirectMember,
|
||||
isCurrentUser: this.isCurrentUser,
|
||||
permissions: {
|
||||
canRemove: this.canRemove,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export default ({ members, sourceId, currentUserId, tableFields }) => ({
|
||||
export default ({ members, sourceId, currentUserId, tableFields, memberPath }) => ({
|
||||
members,
|
||||
sourceId,
|
||||
currentUserId,
|
||||
tableFields,
|
||||
memberPath,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
@import './pages/diff';
|
||||
@import './pages/editor';
|
||||
@import './pages/environment_logs';
|
||||
@import './pages/environments';
|
||||
@import './pages/error_details';
|
||||
@import './pages/error_list';
|
||||
@import './pages/error_tracking_list';
|
||||
|
|
|
|||
|
|
@ -244,20 +244,15 @@
|
|||
}
|
||||
|
||||
&.btn-text-field {
|
||||
color: $gray-500;
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 6px 16px;
|
||||
border-color: $border-color;
|
||||
color: $gray-darkest;
|
||||
background-color: $white;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: text;
|
||||
box-shadow: none;
|
||||
border-color: lighten($blue-300, 20%);
|
||||
color: $gray-darkest;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
@include media-breakpoint-down(md) {
|
||||
.deployments-container {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.environments-folder-name {
|
||||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
.environments-container {
|
||||
.ci-table {
|
||||
|
|
@ -22,10 +22,14 @@ module MembershipActions
|
|||
.new(current_user, update_params)
|
||||
.execute(member)
|
||||
|
||||
member = present_members([member]).first
|
||||
|
||||
respond_to do |format|
|
||||
format.js { render 'shared/members/update', locals: { member: member } }
|
||||
if member.expires?
|
||||
render json: {
|
||||
expires_in: helpers.distance_of_time_in_words_to_now(member.expires_at),
|
||||
expires_soon: member.expires_soon?,
|
||||
expires_at_formatted: member.expires_at.to_time.in_time_zone.to_s(:medium)
|
||||
}
|
||||
else
|
||||
render json: {}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
before_action :authorize_update_snippet!, only: [:edit, :update]
|
||||
before_action :authorize_admin_snippet!, only: [:destroy]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:snippet_multiple_files, current_user)
|
||||
end
|
||||
|
||||
def index
|
||||
@snippet_counts = ::Snippets::CountService
|
||||
.new(current_user, project: @project)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ class SnippetsController < Snippets::ApplicationController
|
|||
|
||||
layout 'snippets'
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:snippet_multiple_files, current_user)
|
||||
end
|
||||
|
||||
def index
|
||||
if params[:username].present?
|
||||
@user = UserFinder.new(params[:username]).find_by_username!
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class Snippet < ApplicationRecord
|
|||
extend ::Gitlab::Utils::Override
|
||||
|
||||
MAX_FILE_COUNT = 10
|
||||
MAX_SINGLE_FILE_COUNT = 1
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
|
|
@ -175,8 +174,8 @@ class Snippet < ApplicationRecord
|
|||
Snippet.find_by(id: id, project: project)
|
||||
end
|
||||
|
||||
def self.max_file_limit(user)
|
||||
Feature.enabled?(:snippet_multiple_files, user) ? MAX_FILE_COUNT : MAX_SINGLE_FILE_COUNT
|
||||
def self.max_file_limit
|
||||
MAX_FILE_COUNT
|
||||
end
|
||||
|
||||
def initialize(attributes = {})
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ class SnippetBlobPresenter < BlobPresenter
|
|||
|
||||
private
|
||||
|
||||
def snippet_multiple_files?
|
||||
blob.container.repository_exists? && Feature.enabled?(:snippet_multiple_files, current_user)
|
||||
end
|
||||
|
||||
def snippet
|
||||
blob.container
|
||||
end
|
||||
|
|
@ -52,8 +48,6 @@ class SnippetBlobPresenter < BlobPresenter
|
|||
end
|
||||
|
||||
def snippet_blob_raw_route(only_path: false)
|
||||
return gitlab_raw_snippet_blob_url(snippet, blob.path, only_path: only_path) if snippet_multiple_files?
|
||||
|
||||
gitlab_raw_snippet_url(snippet, only_path: only_path)
|
||||
gitlab_raw_snippet_blob_url(snippet, blob.path, only_path: only_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ module Snippets
|
|||
|
||||
def check_file_count!
|
||||
file_count = repository.ls_files(snippet.default_branch).size
|
||||
limit = Snippet.max_file_limit(current_user)
|
||||
limit = Snippet.max_file_limit
|
||||
|
||||
if file_count > limit
|
||||
raise RepositoryValidationError, _('Repository files count over the limit')
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
|
||||
= render 'shared/members/sort_dropdown'
|
||||
- if vue_members_list_enabled
|
||||
.js-group-members-list{ data: { members: members_data_json(@group, @members), **data_attributes } }
|
||||
.js-group-members-list{ data: { members: members_data_json(@group, @members), member_path: group_group_member_path(id: ':id'), **data_attributes } }
|
||||
- else
|
||||
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
|
||||
= render partial: 'shared/members/member', collection: @members, as: :member
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
|
||||
= render 'shared/members/search_field', name: 'search_invited'
|
||||
- if vue_members_list_enabled
|
||||
.js-group-invited-members-list{ data: { members: members_data_json(@group, @invited_members), **data_attributes } }
|
||||
.js-group-invited-members-list{ data: { members: members_data_json(@group, @invited_members), member_path: group_group_member_path(id: ':id'), **data_attributes } }
|
||||
- else
|
||||
%ul.content-list.members-list
|
||||
= render partial: 'shared/members/member', collection: @invited_members, as: :member
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
= render 'groups/group_members/tab_pane/title' do
|
||||
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
|
||||
- if vue_members_list_enabled
|
||||
.js-group-access-requests-list{ data: { members: members_data_json(@group, @requesters), **data_attributes } }
|
||||
.js-group-access-requests-list{ data: { members: members_data_json(@group, @requesters), member_path: group_group_member_path(id: ':id'), **data_attributes } }
|
||||
- else
|
||||
%ul.content-list.members-list
|
||||
= render partial: 'shared/members/member', collection: @requesters, as: :member
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
- page_title _("Edit"), @environment.name, _("Environments")
|
||||
- add_page_specific_style 'page_bundles/environments'
|
||||
|
||||
%h3.page-title
|
||||
= _('Edit environment')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- add_to_breadcrumbs _("Environments"), project_environments_path(@project)
|
||||
- breadcrumb_title _("Folder/%{name}") % { name: @folder }
|
||||
- page_title _("Environments in %{name}") % { name: @folder }
|
||||
- add_page_specific_style 'page_bundles/environments'
|
||||
|
||||
#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, project_path: @project.full_path } }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
- page_title _("Environments")
|
||||
- add_page_specific_style 'page_bundles/environments'
|
||||
|
||||
#environments-list-view{ data: { environments_data: environments_list_data,
|
||||
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- breadcrumb_title _("Environments")
|
||||
- page_title _("New Environment")
|
||||
- add_page_specific_style 'page_bundles/environments'
|
||||
|
||||
%h3.page-title
|
||||
= _("New environment")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
- breadcrumb_title @environment.name
|
||||
- page_title _("Environments")
|
||||
- add_page_specific_style 'page_bundles/xterm'
|
||||
- add_page_specific_style 'page_bundles/environments'
|
||||
|
||||
#environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} }
|
||||
- if @environment.available? && can?(current_user, :stop_environment, @environment)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- if current_user
|
||||
%button.csv_download_link.btn.has-tooltip{ title: _('Export as CSV'),
|
||||
%button.csv_download_link.btn.gl-button.has-tooltip{ title: _('Export as CSV'),
|
||||
data: { toggle: 'modal', target: '.issues-export-modal', qa_selector: 'export_as_csv_button' } }
|
||||
= sprite_icon('export')
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@
|
|||
.modal-text
|
||||
= html_escape(_('The CSV export will be created in the background. Once finished, it will be sent to %{strong_open}%{email}%{strong_close} in an attachment.')) % { email: @current_user.notification_email, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
|
||||
.modal-footer
|
||||
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
|
||||
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn gl-button btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
= _('New milestone')
|
||||
|
||||
.milestones
|
||||
#delete-milestone-modal
|
||||
#js-delete-milestone-modal
|
||||
#promote-milestone-modal
|
||||
|
||||
%ul.content-list
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@
|
|||
= _("Requested %{time_ago}").html_safe % { time_ago: time_ago_with_tooltip(member.requested_at) }
|
||||
- else
|
||||
= _("Given access %{time_ago}").html_safe % { time_ago: time_ago_with_tooltip(member.created_at) }
|
||||
- if member.expires?
|
||||
·
|
||||
%span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
|
||||
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) }
|
||||
%span.js-expires-in{ class: ('gl-display-none' unless member.expires?) }
|
||||
·
|
||||
%span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) }
|
||||
- if member.expires?
|
||||
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) }
|
||||
|
||||
- else
|
||||
= image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
- member = local_assigns.fetch(:member)
|
||||
|
||||
:plain
|
||||
var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}');
|
||||
$("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
|
||||
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}"));
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone)
|
||||
|
||||
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
|
||||
target: '#delete-milestone-modal',
|
||||
milestone_id: @milestone.id,
|
||||
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { milestone_id: @milestone.id,
|
||||
milestone_title: markdown_field(@milestone, :title),
|
||||
milestone_url: milestone_url,
|
||||
milestone_issue_count: @milestone.issues.count,
|
||||
|
|
@ -11,4 +9,4 @@
|
|||
= _('Delete')
|
||||
.spinner.js-loading-icon.hidden
|
||||
|
||||
#delete-milestone-modal
|
||||
#js-delete-milestone-modal
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
.row-content-block
|
||||
.float-right
|
||||
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
|
||||
= link_to(sherlock_transaction_path(@transaction), class: 'btn gl-button') do
|
||||
= sprite_icon('arrow-left')
|
||||
= t('sherlock.transaction')
|
||||
.oneline
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply gl-button class to projects/issues/export_csv directory
|
||||
merge_request: 44106
|
||||
author: Lakshit
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply GitLab UI button styles to buttons in app/views/sherlock/file_samples
|
||||
merge_request: 44109
|
||||
author: Lakshit
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable snippet multiple files
|
||||
merge_request: 43246
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Replacing deprecated Bootstrap button with GlButton and updating btn-text-field
|
||||
class to align with styles
|
||||
merge_request: 41430
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable Gitpod button on file tree view
|
||||
merge_request: 43961
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate DeprecatedModal to GitLab UI Modal
|
||||
merge_request: 42113
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Bump mini_magick gem version
|
||||
merge_request: 44450
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
require_relative 'boot'
|
||||
|
||||
# Based on https://github.com/rails/rails/blob/v6.0.1/railties/lib/rails/all.rb
|
||||
|
|
@ -188,6 +189,7 @@ module Gitlab
|
|||
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
|
||||
config.assets.precompile << "page_bundles/boards.css"
|
||||
config.assets.precompile << "page_bundles/cycle_analytics.css"
|
||||
config.assets.precompile << "page_bundles/environments.css"
|
||||
config.assets.precompile << "page_bundles/ide.css"
|
||||
config.assets.precompile << "page_bundles/issues_list.css"
|
||||
config.assets.precompile << "page_bundles/jira_connect.css"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitpod
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37985
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258206
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: snippet_multiple_files
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32416
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/217809
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
# Set default values for object_store settings
|
||||
class ObjectStoreSettings
|
||||
SUPPORTED_TYPES = %w(artifacts external_diffs lfs uploads packages dependency_proxy terraform_state).freeze
|
||||
SUPPORTED_TYPES = %w(artifacts external_diffs lfs uploads packages dependency_proxy terraform_state pages).freeze
|
||||
ALLOWED_OBJECT_STORE_OVERRIDES = %w(bucket enabled proxy_download).freeze
|
||||
|
||||
# pages may be enabled but use legacy disk storage
|
||||
# we don't need to raise an error in that case
|
||||
ALLOWED_INCOMPLETE_TYPES = %w(pages).freeze
|
||||
|
||||
attr_accessor :settings
|
||||
|
||||
# Legacy parser
|
||||
|
|
@ -115,7 +119,9 @@ class ObjectStoreSettings
|
|||
|
||||
next unless section
|
||||
|
||||
raise "Object storage for #{store_type} must have a bucket specified" if section['enabled'] && target_config['bucket'].blank?
|
||||
if section['enabled'] && target_config['bucket'].blank?
|
||||
missing_bucket_for(store_type)
|
||||
end
|
||||
|
||||
# Map bucket (external name) -> remote_directory (internal representation)
|
||||
target_config['remote_directory'] = target_config.delete('bucket')
|
||||
|
|
@ -152,4 +158,14 @@ class ObjectStoreSettings
|
|||
|
||||
true
|
||||
end
|
||||
|
||||
def missing_bucket_for(store_type)
|
||||
message = "Object storage for #{store_type} must have a bucket specified"
|
||||
|
||||
if ALLOWED_INCOMPLETE_TYPES.include?(store_type)
|
||||
warn "[WARNING] #{message}"
|
||||
else
|
||||
raise message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,77 +1,75 @@
|
|||
---
|
||||
stage: Verify
|
||||
group: Continuous Integration
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
# Environment variables
|
||||
|
||||
GitLab exposes certain environment variables which can be used to override
|
||||
their defaults values.
|
||||
|
||||
People usually configure GitLab via `/etc/gitlab/gitlab.rb` for Omnibus
|
||||
People usually configure GitLab with `/etc/gitlab/gitlab.rb` for Omnibus
|
||||
installations, or `gitlab.yml` for installations from source.
|
||||
|
||||
Below you will find the supported environment variables which you can use to
|
||||
override certain values.
|
||||
You can use the following environment variables to override certain values:
|
||||
|
||||
## Supported environment variables
|
||||
|
||||
Variable | Type | Description
|
||||
-------- | ---- | -----------
|
||||
`ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable)
|
||||
`GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (e.g. `//mycdnsubdomain.fictional-cdn.com`)
|
||||
`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
|
||||
`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
|
||||
`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
|
||||
`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
|
||||
`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
|
||||
`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
|
||||
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
|
||||
`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
|
||||
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
|
||||
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
|
||||
`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners
|
||||
`UNSTRUCTURED_RAILS_LOG` | string | Enables the unstructured log in addition to JSON logs (defaults to `true`)
|
||||
| Variable | Type | Description |
|
||||
|--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------|
|
||||
| `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. |
|
||||
| `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). |
|
||||
| `GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (for example, `//mycdnsubdomain.fictional-cdn.com`). |
|
||||
| `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the **From** field in emails sent by GitLab. |
|
||||
| `GITLAB_EMAIL_FROM` | string | The email address used in the **From** field in emails sent by GitLab. |
|
||||
| `GITLAB_EMAIL_REPLY_TO` | string | The email address used in the **Reply-To** field in emails sent by GitLab. |
|
||||
| `GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The email subject suffix used in emails sent by GitLab. |
|
||||
| `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`). |
|
||||
| `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation. |
|
||||
| `GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners. |
|
||||
| `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). |
|
||||
| `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). |
|
||||
| `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging`, or `test`. |
|
||||
| `UNSTRUCTURED_RAILS_LOG` | string | Enables the unstructured log in addition to JSON logs (defaults to `true`). |
|
||||
|
||||
## Complete database variables
|
||||
|
||||
The recommended way of specifying your database connection information is to set
|
||||
the `DATABASE_URL` environment variable. This variable only holds connection
|
||||
information (`adapter`, `database`, `username`, `password`, `host` and `port`),
|
||||
but not behavior information (`encoding`, `pool`). If you don't want to use
|
||||
`DATABASE_URL` and/or want to set database behavior information, you will have
|
||||
to either:
|
||||
The recommended method for specifying your database connection information is
|
||||
to set the `DATABASE_URL` environment variable. This variable contains
|
||||
connection information (`adapter`, `database`, `username`, `password`, `host`,
|
||||
and `port`), but no behavior information (`encoding` or `pool`). If you don't
|
||||
want to use `DATABASE_URL`, or want to set database behavior information,
|
||||
either:
|
||||
|
||||
- copy our template file: `cp config/database.yml.env config/database.yml`, or
|
||||
- set a value for some `GITLAB_DATABASE_XXX` variables
|
||||
- Copy the template file, `cp config/database.yml.env config/database.yml`.
|
||||
- Set a value for some `GITLAB_DATABASE_XXX` variables.
|
||||
|
||||
The list of `GITLAB_DATABASE_XXX` variables that you can set is:
|
||||
|
||||
Variable | Default value | Overridden by `DATABASE_URL`?
|
||||
-------- | ------------- | -----------------------------
|
||||
`GITLAB_DATABASE_ADAPTER` | `postgresql` | Yes
|
||||
`GITLAB_DATABASE_DATABASE` | `gitlab_#{ENV['RAILS_ENV']` | Yes
|
||||
`GITLAB_DATABASE_USERNAME` | `root` | Yes
|
||||
`GITLAB_DATABASE_PASSWORD` | None | Yes
|
||||
`GITLAB_DATABASE_HOST` | `localhost` | Yes
|
||||
`GITLAB_DATABASE_PORT` | `5432` | Yes
|
||||
`GITLAB_DATABASE_ENCODING` | `unicode` | No
|
||||
`GITLAB_DATABASE_POOL` | `10` | No
|
||||
| Variable | Default value | Overridden by `DATABASE_URL`? |
|
||||
|-----------------------------|--------------------------------|-------------------------------|
|
||||
| `GITLAB_DATABASE_ADAPTER` | `postgresql` | **{check-circle}** Yes |
|
||||
| `GITLAB_DATABASE_DATABASE` | `gitlab_#{ENV['RAILS_ENV']` | **{check-circle}** Yes |
|
||||
| `GITLAB_DATABASE_ENCODING` | `unicode` | **{dotted-circle}** No |
|
||||
| `GITLAB_DATABASE_HOST` | `localhost` | **{check-circle}** Yes |
|
||||
| `GITLAB_DATABASE_PASSWORD` | _none_ | **{check-circle}** Yes |
|
||||
| `GITLAB_DATABASE_POOL` | `10` | **{dotted-circle}** No |
|
||||
| `GITLAB_DATABASE_PORT` | `5432` | **{check-circle}** Yes |
|
||||
| `GITLAB_DATABASE_USERNAME` | `root` | **{check-circle}** Yes |
|
||||
|
||||
## Adding more variables
|
||||
|
||||
We welcome merge requests to make more settings configurable via variables.
|
||||
Please make changes in the `config/initializers/1_settings.rb` file and stick
|
||||
to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`.
|
||||
We welcome merge requests to make more settings configurable by using variables.
|
||||
Make changes to the `config/initializers/1_settings.rb` file, and use the
|
||||
naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`.
|
||||
|
||||
## Omnibus configuration
|
||||
|
||||
To set environment variables, follow [these
|
||||
instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
|
||||
To set environment variables, follow [these instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
|
||||
|
||||
It's possible to preconfigure the GitLab Docker image by adding the environment
|
||||
variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command.
|
||||
For more information see the [Pre-configure Docker container](https://docs.gitlab.com/omnibus/docker/#pre-configure-docker-container)
|
||||
section in the Omnibus documentation.
|
||||
For more information, see the [Pre-configure Docker container](https://docs.gitlab.com/omnibus/docker/#pre-configure-docker-container)
|
||||
section of the Omnibus GitLab documentation.
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ addresses and names, do use:
|
|||
- **Email addresses**: Use an email address ending in `example.com`.
|
||||
- **Names**: Use strings like `example_username`. Alternatively, use diverse or
|
||||
non-gendered names with common surnames, such as `Sidney Jones`, `Zhang Wei`,
|
||||
or `Maria Garcia`.
|
||||
or `Alex Garcia`.
|
||||
|
||||
### Fake URLs
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Snowplow is an enterprise-grade marketing and product analytics platform which h
|
|||
We have many definitions of Snowplow's schema. We have an active issue to [standardize this schema](https://gitlab.com/gitlab-org/gitlab/-/issues/207930) including the following definitions:
|
||||
|
||||
- Frontend and backend taxonomy as listed below
|
||||
- [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy)
|
||||
- [Structured event taxonomy](#structured-event-taxonomy)
|
||||
- [Self describing events](https://github.com/snowplow/snowplow/wiki/Custom-events#self-describing-events)
|
||||
- [Iglu schema](https://gitlab.com/gitlab-org/iglu/)
|
||||
- [Snowplow authored events](https://github.com/snowplow/snowplow/wiki/Snowplow-authored-events)
|
||||
|
|
@ -96,15 +96,29 @@ sequenceDiagram
|
|||
Snowflake DW->>Sisense Dashboards: Data available for querying
|
||||
```
|
||||
|
||||
## Structured event taxonomy
|
||||
|
||||
When adding new click events, we should add them in a way that's internally consistent. If we don't, it'll be very painful to perform analysis across features since each feature will be capturing events differently.
|
||||
|
||||
The current method provides several attributes that are sent on each click event. Please try to follow these guidelines when specifying events to capture:
|
||||
|
||||
| attribute | type | required | description |
|
||||
| --------- | ------- | -------- | ----------- |
|
||||
| category | text | true | The page or backend area of the application. Unless infeasible, please use the Rails page attribute by default in the frontend, and namespace + classname on the backend. |
|
||||
| action | text | true | The action the user is taking, or aspect that's being instrumented. The first word should always describe the action or aspect: clicks should be `click`, activations should be `activate`, creations should be `create`, etc. Use underscores to describe what was acted on; for example, activating a form field would be `activate_form_input`. An interface action like clicking on a dropdown would be `click_dropdown`, while a behavior like creating a project record from the backend would be `create_project` |
|
||||
| label | text | false | The specific element, or object that's being acted on. This is either the label of the element (e.g. a tab labeled 'Create from template' may be `create_from_template`) or a unique identifier if no text is available (e.g. closing the Groups dropdown in the top navbar might be `groups_dropdown_close`), or it could be the name or title attribute of a record being created. |
|
||||
| property | text | false | Any additional property of the element, or object being acted on. |
|
||||
| value | decimal | false | Describes a numeric value or something directly related to the event. This could be the value of an input (e.g. `10` when clicking `internal` visibility). |
|
||||
|
||||
## Implementing Snowplow JS (Frontend) tracking
|
||||
|
||||
GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to utilize tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy).
|
||||
GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to utilize tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy).
|
||||
|
||||
| field | type | default value | description |
|
||||
|:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `category` | string | document.body.dataset.page | Page or subsection of a page that events are being captured within. |
|
||||
| `action` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
|
||||
| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). |
|
||||
| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
|
||||
### Tracking in HAML (or Vue Templates)
|
||||
|
||||
|
|
@ -131,10 +145,10 @@ Below is a list of supported `data-track-*` attributes:
|
|||
| attribute | required | description |
|
||||
|:----------------------|:---------|:------------|
|
||||
| `data-track-event` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field would be `activate_form_input` and clicking a button would be `click_button`. |
|
||||
| `data-track-label` | false | The `label` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). |
|
||||
| `data-track-property` | false | The `property` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). |
|
||||
| `data-track-value` | false | The `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` when unchecked. |
|
||||
| `data-track-context` | false | The `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). |
|
||||
| `data-track-label` | false | The `label` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
| `data-track-property` | false | The `property` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
| `data-track-value` | false | The `value` as described in our [Structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` when unchecked. |
|
||||
| `data-track-context` | false | The `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
|
||||
### Tracking within Vue components
|
||||
|
||||
|
|
@ -278,7 +292,7 @@ Custom event tracking and instrumentation can be added by directly calling the `
|
|||
|:-----------|:-------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `category` | string | 'application' | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. |
|
||||
| `action` | string | 'generic' | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. |
|
||||
| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described in [Instrumentation at GitLab](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). These are set as empty strings if you don't provide them. |
|
||||
| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described in [Structured event taxonomy](#structured-event-taxonomy). These are set as empty strings if you don't provide them. |
|
||||
|
||||
Tracking can be viewed as either tracking user behavior, or can be utilized for instrumentation to monitor and visualize performance over time in an area or aspect of code.
|
||||
|
||||
|
|
|
|||
|
|
@ -484,17 +484,22 @@ This will result in only one `Project`, `User`, and `ProjectMember` created for
|
|||
is handled automatically using a transaction rollback.
|
||||
|
||||
Note that if you modify an object defined inside a `let_it_be` block,
|
||||
then you will need to reload the object as needed, or specify the `reload`
|
||||
option to reload for every example.
|
||||
then you must do one of the following:
|
||||
|
||||
- Reload the object as needed.
|
||||
- Use the `let_it_be_with_reload` alias.
|
||||
- Specify the `reload` option to reload for every example.
|
||||
|
||||
```ruby
|
||||
let_it_be_with_reload(:project) { create(:project) }
|
||||
let_it_be(:project, reload: true) { create(:project) }
|
||||
```
|
||||
|
||||
You can also specify the `refind` option as well to completely load a
|
||||
new object.
|
||||
You can also use the `let_it_be_with_refind` alias, or specify the `refind`
|
||||
option as well to completely load a new object.
|
||||
|
||||
```ruby
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
let_it_be(:project, refind: true) { create(:project) }
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ info: "To determine the technical writer assigned to the Stage/Group associated
|
|||
# Gitpod Integration
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228893) in GitLab 13.4.
|
||||
> - It's [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default.
|
||||
> - It was [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default.
|
||||
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/258206) in GitLab 13.5.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#configure-your-gitlab-instance-with-gitpod). **(CORE ONLY)**
|
||||
|
|
@ -57,19 +58,18 @@ and get your instance up and running.
|
|||
|
||||
## Enable or disable the Gitpod integration **(CORE ONLY)**
|
||||
|
||||
The Gitpod integration is under development and not ready for production use. It is deployed behind a
|
||||
feature flag that is **disabled by default**.
|
||||
The Gitpod integration is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:gitpod)
|
||||
```
|
||||
can enable or disable it.
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:gitpod)
|
||||
```
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:gitpod)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ The following table lists variables used to disable jobs.
|
|||
| `REVIEW_DISABLED` | From GitLab 11.0, used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs won't be created. |
|
||||
| `SAST_DISABLED` | From GitLab 11.0, used to disable the `sast` job. If the variable is present, the job won't be created. |
|
||||
| `TEST_DISABLED` | From GitLab 11.0, used to disable the `test` job. If the variable is present, the job won't be created. |
|
||||
| `SECRET_DETECTION_DISABLED` | From GitLab 13.1, used to disable the `secret_detection` job. If the variable is present, the job won't be created. |
|
||||
|
||||
### Application secret variables
|
||||
|
||||
|
|
|
|||
|
|
@ -1126,7 +1126,7 @@ to how much it can scale, and as it is a single instance deployment, you will ex
|
|||
when upgrading the Vault application.
|
||||
|
||||
To optimally use Vault in a production environment, it's ideal to have a good understanding
|
||||
of the internals of Vault and how to configure it. This can be done by reading the
|
||||
of the internals of Vault and how to configure it. This can be done by reading the [Vault Configuration guide](../../ci/secrets/#configure-your-vault-server),
|
||||
[the Vault documentation](https://www.vaultproject.io/docs/internals) as well as
|
||||
the Vault Helm chart [`values.yaml` file](https://github.com/hashicorp/vault-helm/blob/v0.3.3/values.yaml).
|
||||
|
||||
|
|
|
|||
|
|
@ -356,6 +356,13 @@ with your personal access token or deploy token):
|
|||
//gitlab.com/api/v4/projects/:_authToken=<your_token>
|
||||
```
|
||||
|
||||
You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically:
|
||||
|
||||
```shell
|
||||
yarn config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
|
||||
yarn config set '//gitlab.com/api/v4/packages/npm/:_authToken' "<your_token>"
|
||||
```
|
||||
|
||||
### `npm publish` targets default NPM registry (`registry.npmjs.org`)
|
||||
|
||||
Ensure that your package scope is set consistently in your `package.json` and `.npmrc` files.
|
||||
|
|
|
|||
|
|
@ -19,9 +19,12 @@ and learn how to spin up a Kubernetes cluster managed by Google Cloud Platform (
|
|||
in a few clicks.
|
||||
|
||||
TIP: **Tip:**
|
||||
Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
|
||||
and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
|
||||
Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form) and apply for credit.
|
||||
Every new Google Cloud Platform (GCP) account receives
|
||||
[$300 in credit upon sign up](https://console.cloud.google.com/freetrial).
|
||||
In partnership with Google, GitLab is able to offer an additional $200 for new GCP
|
||||
accounts to get started with GitLab's Google Kubernetes Engine Integration.
|
||||
[Follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form)
|
||||
to apply for credit.
|
||||
|
||||
## Before you begin
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ Before [adding a Kubernetes cluster](#create-new-cluster) using GitLab, you need
|
|||
- GitLab itself. Either:
|
||||
- A [GitLab.com account](https://about.gitlab.com/pricing/#gitlab-com).
|
||||
- A [self-managed installation](https://about.gitlab.com/pricing/#self-managed) with GitLab version
|
||||
12.5 or later. This will ensure the GitLab UI can be used for cluster creation.
|
||||
12.5 or later. This ensures the GitLab UI can be used for cluster creation.
|
||||
- The following GitLab access:
|
||||
- [Maintainer access to a project](../../permissions.md#project-members-permissions) for a
|
||||
project-level cluster.
|
||||
|
|
@ -41,14 +44,12 @@ Before [adding a Kubernetes cluster](#create-new-cluster) using GitLab, you need
|
|||
|
||||
## Access controls
|
||||
|
||||
When creating a cluster in GitLab, you will be asked if you would like to create either:
|
||||
When creating a cluster in GitLab, you are asked if you would like to create either:
|
||||
|
||||
- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) cluster.
|
||||
- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
cluster, which is the GitLab default and recommended option.
|
||||
- An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/reference/access-authn-authz/abac/) cluster.
|
||||
|
||||
NOTE: **Note:**
|
||||
[RBAC](#rbac-cluster-resources) is recommended and the GitLab default.
|
||||
|
||||
GitLab creates the necessary service accounts and privileges to install and run
|
||||
[GitLab managed applications](index.md#installing-applications). When GitLab creates the cluster,
|
||||
a `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace
|
||||
|
|
@ -59,10 +60,10 @@ Restricted service account for deployment was [introduced](https://gitlab.com/gi
|
|||
|
||||
The first time you install an application into your cluster, the `tiller` service
|
||||
account is created with `cluster-admin` privileges in the
|
||||
`gitlab-managed-apps` namespace. This service account will be used by Helm to
|
||||
`gitlab-managed-apps` namespace. This service account is used by Helm to
|
||||
install and run [GitLab managed applications](index.md#installing-applications).
|
||||
|
||||
Helm will also create additional service accounts and other resources for each
|
||||
Helm also creates additional service accounts and other resources for each
|
||||
installed application. Consult the documentation of the Helm charts for each application
|
||||
for details.
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ Note the following about access controls:
|
|||
|
||||
- Environment-specific resources are only created if your cluster is
|
||||
[managed by GitLab](index.md#gitlab-managed-clusters).
|
||||
- If your cluster was created before GitLab 12.2, it will use a single namespace for all project
|
||||
- If your cluster was created before GitLab 12.2, it uses a single namespace for all project
|
||||
environments.
|
||||
|
||||
### RBAC cluster resources
|
||||
|
|
@ -181,7 +182,7 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'
|
||||
```
|
||||
|
||||
1. **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We will use the certificate created by default.
|
||||
1. **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We use the certificate created by default.
|
||||
1. List the secrets with `kubectl get secrets`, and one should be named similar to
|
||||
`default-token-xxxxx`. Copy that token name for use below.
|
||||
1. Get the certificate by running this command:
|
||||
|
|
@ -193,17 +194,17 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
NOTE: **Note:**
|
||||
If the command returns the entire certificate chain, you must copy the Root CA
|
||||
certificate and any intermediate certificates at the bottom of the chain.
|
||||
A chain file has following structure:
|
||||
A chain file has following structure:
|
||||
|
||||
```plaintext
|
||||
-----BEGIN MY CERTIFICATE-----
|
||||
-----END MY CERTIFICATE-----
|
||||
-----BEGIN INTERMEDIATE CERTIFICATE-----
|
||||
-----END INTERMEDIATE CERTIFICATE-----
|
||||
-----BEGIN INTERMEDIATE CERTIFICATE-----
|
||||
-----END INTERMEDIATE CERTIFICATE-----
|
||||
-----BEGIN ROOT CERTIFICATE-----
|
||||
-----END ROOT CERTIFICATE-----
|
||||
-----BEGIN MY CERTIFICATE-----
|
||||
-----END MY CERTIFICATE-----
|
||||
-----BEGIN INTERMEDIATE CERTIFICATE-----
|
||||
-----END INTERMEDIATE CERTIFICATE-----
|
||||
-----BEGIN INTERMEDIATE CERTIFICATE-----
|
||||
-----END INTERMEDIATE CERTIFICATE-----
|
||||
-----BEGIN ROOT CERTIFICATE-----
|
||||
-----END ROOT CERTIFICATE-----
|
||||
```
|
||||
|
||||
1. **Token** -
|
||||
|
|
@ -241,10 +242,10 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
kubectl apply -f gitlab-admin-service-account.yaml
|
||||
```
|
||||
|
||||
You will need the `container.clusterRoleBindings.create` permission
|
||||
You need the `container.clusterRoleBindings.create` permission
|
||||
to create cluster-level roles. If you do not have this permission,
|
||||
you can alternatively enable Basic Authentication and then run the
|
||||
`kubectl apply` command as an admin:
|
||||
`kubectl apply` command as an administrator:
|
||||
|
||||
```shell
|
||||
kubectl apply -f gitlab-admin-service-account.yaml --username=admin --password=<password>
|
||||
|
|
@ -286,7 +287,7 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
For GKE clusters, you will need the
|
||||
For GKE clusters, you need the
|
||||
`container.clusterRoleBindings.create` permission to create a cluster
|
||||
role binding. You can follow the [Google Cloud
|
||||
documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
|
||||
|
|
@ -295,7 +296,7 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
1. **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster.
|
||||
See the [Managed clusters section](index.md#gitlab-managed-clusters) for more information.
|
||||
1. **Project namespace** (optional) - You don't have to fill it in; by leaving
|
||||
it blank, GitLab will create one for you. Also:
|
||||
it blank, GitLab creates one for you. Also:
|
||||
- Each project should have a unique namespace.
|
||||
- The project namespace is not necessarily the namespace of the secret, if
|
||||
you're using a secret with broader permissions, like the secret from `default`.
|
||||
|
|
@ -306,19 +307,19 @@ To add a Kubernetes cluster to your project, group, or instance:
|
|||
|
||||
1. Finally, click the **Create Kubernetes cluster** button.
|
||||
|
||||
After a couple of minutes, your cluster will be ready to go. You can now proceed
|
||||
After a couple of minutes, your cluster is ready. You can now proceed
|
||||
to install some [pre-defined applications](index.md#installing-applications).
|
||||
|
||||
#### Disable Role-Based Access Control (RBAC) (optional)
|
||||
|
||||
When connecting a cluster via GitLab integration, you may specify whether the
|
||||
cluster is RBAC-enabled or not. This will affect how GitLab interacts with the
|
||||
cluster is RBAC-enabled or not. This affects how GitLab interacts with the
|
||||
cluster for certain operations. If you did *not* check the **RBAC-enabled cluster**
|
||||
checkbox at creation time, GitLab will assume RBAC is disabled for your cluster
|
||||
checkbox at creation time, GitLab assumes RBAC is disabled for your cluster
|
||||
when interacting with it. If so, you must disable RBAC on your cluster for the
|
||||
integration to work properly.
|
||||
|
||||

|
||||

|
||||
|
||||
NOTE: **Note:**
|
||||
Disabling RBAC means that any application running in the cluster,
|
||||
|
|
@ -368,3 +369,12 @@ When removing the cluster integration, note:
|
|||
|
||||
To learn more on automatically deploying your applications,
|
||||
read about [Auto DevOps](../../../topics/autodevops/index.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid
|
||||
|
||||
If you encounter this error while adding a Kubernetes cluster, ensure you're
|
||||
properly pasting the service token. Some shells may add a line break to the
|
||||
service token, making it invalid. Ensure that there are no line breaks by
|
||||
pasting your token into an editor and removing any additional spaces.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module API
|
|||
expose :file_name do |snippet|
|
||||
snippet.file_name_on_repo || snippet.file_name
|
||||
end
|
||||
expose :files, if: ->(snippet, options) { snippet_multiple_files?(snippet, options[:current_user]) } do |snippet, options|
|
||||
expose :files do |snippet, options|
|
||||
snippet.list_files.map do |file|
|
||||
{
|
||||
path: file,
|
||||
|
|
@ -25,10 +25,6 @@ module API
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
def snippet_multiple_files?(snippet, current_user)
|
||||
::Feature.enabled?(:snippet_multiple_files, current_user) && snippet.repository_exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ module API
|
|||
def validate_params_for_multiple_files(snippet)
|
||||
return unless params[:content] || params[:file_name]
|
||||
|
||||
if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files?
|
||||
if snippet.multiple_files?
|
||||
render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ module Gitlab
|
|||
override :check_single_change_access
|
||||
def check_single_change_access(change, _skip_lfs_integrity_check: false)
|
||||
Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
|
||||
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
|
||||
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit, logger: logger).validate!
|
||||
rescue Checks::TimedLogger::TimeoutError
|
||||
raise TimeoutError, logger.full_message
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@
|
|||
module Gitlab
|
||||
class Gitpod
|
||||
class << self
|
||||
def feature_conditional?
|
||||
feature.conditional?
|
||||
end
|
||||
|
||||
def feature_available?
|
||||
# The gitpod_bundle feature could be conditionally applied, so check if `!off?`
|
||||
!feature.off?
|
||||
!feature.off? || feature_enabled?
|
||||
end
|
||||
|
||||
def feature_enabled?(actor = nil)
|
||||
feature.enabled?(actor)
|
||||
Feature.enabled?(:gitpod, actor, default_enabled: true)
|
||||
end
|
||||
|
||||
def feature_and_settings_enabled?(actor = nil)
|
||||
|
|
|
|||
|
|
@ -4338,6 +4338,9 @@ msgstr ""
|
|||
msgid "Burndown chart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Burndown charts are now fixed. This means that removing issues from a milestone after it has expired won't affect the chart. You can view the old chart using the %{strongStart}Legacy burndown chart%{strongEnd} button."
|
||||
msgstr ""
|
||||
|
||||
msgid "BurndownChartLabel|Open issue weight"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11347,6 +11350,9 @@ msgstr ""
|
|||
msgid "First seen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fixed burndown chart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fixed date"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15013,6 +15019,9 @@ msgstr ""
|
|||
msgid "Leave zen mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Legacy burndown chart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Let's Encrypt does not accept emails on example.com"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15840,6 +15849,21 @@ msgstr ""
|
|||
msgid "Members|%{time} by %{user}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to deny %{usersName}'s request to join \"%{source}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Expired"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15849,6 +15873,15 @@ msgstr ""
|
|||
msgid "Members|in %{time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Member|Deny access"
|
||||
msgstr ""
|
||||
|
||||
msgid "Member|Remove member"
|
||||
msgstr ""
|
||||
|
||||
msgid "Member|Revoke invite"
|
||||
msgstr ""
|
||||
|
||||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16008,7 +16041,7 @@ msgstr ""
|
|||
msgid "MergeRequests|Jump to next unresolved thread"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|Reply..."
|
||||
msgid "MergeRequests|Reply"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|Resolve this thread in a new issue"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create', :requires_admin do
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Multiple file snippet' do
|
||||
before do
|
||||
Runtime::Feature.enable('snippet_multiple_files')
|
||||
end
|
||||
|
||||
after do
|
||||
Runtime::Feature.disable('snippet_multiple_files')
|
||||
end
|
||||
|
||||
it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/842' do
|
||||
Flow::Login.sign_in
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ RSpec.describe ObjectStoreSettings do
|
|||
'lfs' => { 'enabled' => true },
|
||||
'artifacts' => { 'enabled' => true },
|
||||
'external_diffs' => { 'enabled' => false },
|
||||
'pages' => { 'enabled' => true },
|
||||
'object_store' => {
|
||||
'enabled' => true,
|
||||
'connection' => connection,
|
||||
|
|
@ -39,6 +40,9 @@ RSpec.describe ObjectStoreSettings do
|
|||
'external_diffs' => {
|
||||
'bucket' => 'external_diffs',
|
||||
'enabled' => false
|
||||
},
|
||||
'pages' => {
|
||||
'bucket' => 'pages'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +68,11 @@ RSpec.describe ObjectStoreSettings do
|
|||
expect(settings.lfs['object_store']['proxy_download']).to be true
|
||||
expect(settings.lfs['object_store']['remote_directory']).to eq('lfs-objects')
|
||||
|
||||
expect(settings.pages['enabled']).to be true
|
||||
expect(settings.pages['object_store']['enabled']).to be true
|
||||
expect(settings.pages['object_store']['connection']).to eq(connection)
|
||||
expect(settings.pages['object_store']['remote_directory']).to eq('pages')
|
||||
|
||||
expect(settings.external_diffs['enabled']).to be false
|
||||
expect(settings.external_diffs['object_store']['enabled']).to be false
|
||||
expect(settings.external_diffs['object_store']['remote_directory']).to eq('external_diffs')
|
||||
|
|
@ -75,6 +84,12 @@ RSpec.describe ObjectStoreSettings do
|
|||
expect { subject }.to raise_error(/Object storage for lfs must have a bucket specified/)
|
||||
end
|
||||
|
||||
it 'does not raise error if pages bucket is missing' do
|
||||
config['object_store']['objects']['pages'].delete('bucket')
|
||||
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'with legacy config' do
|
||||
let(:legacy_settings) do
|
||||
{
|
||||
|
|
|
|||
|
|
@ -233,6 +233,42 @@ RSpec.describe Groups::GroupMembersController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'expiration date' do
|
||||
let(:expiry_date) { 1.month.from_now.to_date }
|
||||
|
||||
before do
|
||||
travel_to Time.now.utc.beginning_of_day
|
||||
|
||||
put(
|
||||
:update,
|
||||
params: {
|
||||
group_member: { expires_at: expiry_date },
|
||||
group_id: group,
|
||||
id: requester
|
||||
},
|
||||
format: :json
|
||||
)
|
||||
end
|
||||
|
||||
context 'when `expires_at` is set' do
|
||||
it 'returns correct json response' do
|
||||
expect(json_response).to eq({
|
||||
"expires_in" => "about 1 month",
|
||||
"expires_soon" => false,
|
||||
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `expires_at` is not set' do
|
||||
let(:expiry_date) { nil }
|
||||
|
||||
it 'returns empty json response' do
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
|
|
@ -441,7 +477,7 @@ RSpec.describe Groups::GroupMembersController do
|
|||
group_id: group,
|
||||
id: membership
|
||||
},
|
||||
format: :js
|
||||
format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -228,6 +228,43 @@ RSpec.describe Projects::ProjectMembersController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'expiration date' do
|
||||
let(:expiry_date) { 1.month.from_now.to_date }
|
||||
|
||||
before do
|
||||
travel_to Time.now.utc.beginning_of_day
|
||||
|
||||
put(
|
||||
:update,
|
||||
params: {
|
||||
project_member: { expires_at: expiry_date },
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: requester
|
||||
},
|
||||
format: :json
|
||||
)
|
||||
end
|
||||
|
||||
context 'when `expires_at` is set' do
|
||||
it 'returns correct json response' do
|
||||
expect(json_response).to eq({
|
||||
"expires_in" => "about 1 month",
|
||||
"expires_soon" => false,
|
||||
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `expires_at` is not set' do
|
||||
let(:expiry_date) { nil }
|
||||
|
||||
it 'returns empty json response' do
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
|
|
|
|||
|
|
@ -6,65 +6,66 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js
|
|||
include Select2Helper
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
let(:user1) { create(:user, name: 'John Doe') }
|
||||
let!(:new_member) { create(:user, name: 'Mary Jane') }
|
||||
let(:group) { create(:group) }
|
||||
let_it_be(:user1) { create(:user, name: 'John Doe') }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let(:new_member) { create(:user, name: 'Mary Jane') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vue_group_members_list: false)
|
||||
|
||||
travel_to Time.now.utc.beginning_of_day
|
||||
|
||||
group.add_owner(user1)
|
||||
sign_in(user1)
|
||||
end
|
||||
|
||||
it 'expiration date is displayed in the members list' do
|
||||
travel_to Time.zone.parse('2016-08-06 08:00') do
|
||||
date = 4.days.from_now
|
||||
visit group_group_members_path(group)
|
||||
visit group_group_members_path(group)
|
||||
|
||||
page.within '.invite-users-form' do
|
||||
select2(new_member.id, from: '#user_ids', multiple: true)
|
||||
fill_in 'expires_at', with: date.to_s(:medium) + "\n"
|
||||
click_on 'Invite'
|
||||
end
|
||||
page.within '.invite-users-form' do
|
||||
select2(new_member.id, from: '#user_ids', multiple: true)
|
||||
|
||||
page.within "#group_member_#{group_member_id(new_member)}" do
|
||||
expect(page).to have_content('Expires in 4 days')
|
||||
end
|
||||
fill_in 'expires_at', with: 3.days.from_now.to_date
|
||||
find_field('expires_at').native.send_keys :enter
|
||||
|
||||
click_on 'Invite'
|
||||
end
|
||||
|
||||
page.within "#group_member_#{group_member_id}" do
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
end
|
||||
|
||||
it 'change expiration date' do
|
||||
travel_to Time.zone.parse('2016-08-06 08:00') do
|
||||
date = 3.days.from_now
|
||||
group.add_developer(new_member)
|
||||
it 'changes expiration date' do
|
||||
group.add_developer(new_member)
|
||||
visit group_group_members_path(group)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
page.within "#group_member_#{group_member_id}" do
|
||||
fill_in 'Expiration date', with: 3.days.from_now.to_date
|
||||
find_field('Expiration date').native.send_keys :enter
|
||||
|
||||
page.within "#group_member_#{group_member_id(new_member)}" do
|
||||
find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
|
||||
wait_for_requests
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
end
|
||||
|
||||
it 'remove expiration date' do
|
||||
travel_to Time.zone.parse('2016-08-06 08:00') do
|
||||
date = 3.days.from_now
|
||||
group_member = create(:group_member, :developer, user: new_member, group: group, expires_at: date.to_s(:medium))
|
||||
it 'clears expiration date' do
|
||||
create(:group_member, :developer, user: new_member, group: group, expires_at: 3.days.from_now.to_date)
|
||||
visit group_group_members_path(group)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
page.within "#group_member_#{group_member_id}" do
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
|
||||
page.within "#group_member_#{group_member.id}" do
|
||||
find('.js-clear-input').click
|
||||
wait_for_requests
|
||||
expect(page).not_to have_content('Expires in 3 days')
|
||||
end
|
||||
find('.js-clear-input').click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content('Expires in')
|
||||
end
|
||||
end
|
||||
|
||||
def group_member_id(user)
|
||||
def group_member_id
|
||||
group.members.find_by(user_id: new_member).id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -834,7 +834,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
end
|
||||
|
||||
def start_and_cancel_discussion
|
||||
click_button('Reply...')
|
||||
click_button('Reply')
|
||||
|
||||
fill_in('note_note', with: 'Whoops!')
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ end
|
|||
|
||||
def write_reply_to_discussion(button_text: 'Start a review', text: 'Line is wrong', resolve: false, unresolve: false)
|
||||
page.within(first('.diff-files-holder .discussion-reply-holder')) do
|
||||
click_button('Reply...')
|
||||
click_button('Reply')
|
||||
|
||||
fill_in('note_note', with: text)
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
it 'adds as discussion' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
|
||||
expect(page).to have_css('.notes_holder .note.note-discussion', count: 1)
|
||||
expect(page).to have_button('Reply...')
|
||||
expect(page).to have_button('Reply')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
|
||||
it 'allows user to comment' do
|
||||
page.within '.diff-content' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
find(".js-unresolve-checkbox").set false
|
||||
find('.js-note-text').set 'testing'
|
||||
|
|
@ -176,7 +176,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
|
||||
it 'allows user to comment & unresolve thread' do
|
||||
page.within '.diff-content' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
find('.js-note-text').set 'testing'
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
|
||||
it 'allows user to comment & resolve thread' do
|
||||
page.within '.diff-content' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
find('.js-note-text').set 'testing'
|
||||
|
||||
|
|
@ -438,7 +438,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
|
||||
it 'allows user to comment & resolve thread' do
|
||||
page.within '.diff-content' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
find('.js-note-text').set 'testing'
|
||||
|
||||
|
|
@ -457,7 +457,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
page.within '.diff-content' do
|
||||
click_button 'Resolve thread'
|
||||
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
find('.js-note-text').set 'testing'
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
end
|
||||
|
||||
it 'does not render avatars after commenting on discussion tab' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
find('.note-textarea').native.send_keys('Test comment')
|
||||
|
|
@ -132,7 +132,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
end
|
||||
|
||||
it 'adds avatar when commenting' do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
page.within '.js-discussion-note-form' do
|
||||
find('.js-note-text').native.send_keys('Test')
|
||||
|
|
@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
|
||||
it 'adds multiple comments' do
|
||||
3.times do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
|
||||
page.within '.js-discussion-note-form' do
|
||||
find('.js-note-text').native.send_keys('Test')
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe 'Merge request > User sees threads', :js do
|
|||
|
||||
it 'can be replied to' do
|
||||
within(".discussion[data-discussion-id='#{discussion_id}']") do
|
||||
click_button 'Reply...'
|
||||
click_button 'Reply'
|
||||
fill_in 'note[note]', with: 'Test!'
|
||||
click_button 'Comment'
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User sees notes from forked project', :js do
|
|||
expect(page).to have_content('A commit comment')
|
||||
|
||||
page.within('.discussion-notes') do
|
||||
find('.btn-text-field').click
|
||||
find('.js-vue-discussion-reply').click
|
||||
scroll_to(page.find('#note_note', visible: false))
|
||||
find('#note_note').send_keys('A reply comment')
|
||||
find('.js-comment-button').click
|
||||
|
|
|
|||
|
|
@ -6,43 +6,64 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
|
|||
include Select2Helper
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
let(:maintainer) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let!(:new_member) { create(:user) }
|
||||
let_it_be(:maintainer) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let(:new_member) { create(:user) }
|
||||
|
||||
before do
|
||||
travel_to Time.now.utc.beginning_of_day
|
||||
|
||||
project.add_maintainer(maintainer)
|
||||
sign_in(maintainer)
|
||||
end
|
||||
|
||||
it 'expiration date is displayed in the members list' do
|
||||
travel_to Time.zone.parse('2016-08-06 08:00') do
|
||||
date = 4.days.from_now
|
||||
visit project_project_members_path(project)
|
||||
visit project_project_members_path(project)
|
||||
|
||||
page.within '.invite-users-form' do
|
||||
select2(new_member.id, from: '#user_ids', multiple: true)
|
||||
fill_in 'expires_at', with: date.to_s(:medium) + "\n"
|
||||
click_on 'Invite'
|
||||
end
|
||||
page.within '.invite-users-form' do
|
||||
select2(new_member.id, from: '#user_ids', multiple: true)
|
||||
|
||||
page.within "#project_member_#{new_member.project_members.first.id}" do
|
||||
expect(page).to have_content('Expires in 4 days')
|
||||
end
|
||||
fill_in 'expires_at', with: 3.days.from_now.to_date
|
||||
find_field('expires_at').native.send_keys :enter
|
||||
|
||||
click_on 'Invite'
|
||||
end
|
||||
|
||||
page.within "#project_member_#{project_member_id}" do
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
end
|
||||
|
||||
it 'change expiration date' do
|
||||
travel_to Time.zone.parse('2016-08-06 08:00') do
|
||||
date = 3.days.from_now
|
||||
project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium))
|
||||
visit project_project_members_path(project)
|
||||
it 'changes expiration date' do
|
||||
project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_date)
|
||||
visit project_project_members_path(project)
|
||||
|
||||
page.within "#project_member_#{new_member.project_members.first.id}" do
|
||||
find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
|
||||
wait_for_requests
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
page.within "#project_member_#{project_member_id}" do
|
||||
fill_in 'Expiration date', with: 3.days.from_now.to_date
|
||||
find_field('Expiration date').native.send_keys :enter
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
end
|
||||
end
|
||||
|
||||
it 'clears expiration date' do
|
||||
project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
|
||||
visit project_project_members_path(project)
|
||||
|
||||
page.within "#project_member_#{project_member_id}" do
|
||||
expect(page).to have_content('Expires in 3 days')
|
||||
|
||||
find('.js-clear-input').click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content('Expires in')
|
||||
end
|
||||
end
|
||||
|
||||
def project_member_id
|
||||
project.members.find_by(user_id: new_member).id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ describe('Commit pipeline status component', () => {
|
|||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: pending');
|
||||
expect(findCiIcon().attributes('title')).toEqual('Pipeline: pending');
|
||||
expect(findCiIcon().props('status')).toEqual(mockCiStatus);
|
||||
});
|
||||
});
|
||||
|
|
@ -161,7 +161,7 @@ describe('Commit pipeline status component', () => {
|
|||
});
|
||||
|
||||
it('renders not found CI icon', () => {
|
||||
expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: not found');
|
||||
expect(findCiIcon().attributes('title')).toEqual('Pipeline: not found');
|
||||
expect(findCiIcon().props('status')).toEqual({
|
||||
text: 'not found',
|
||||
icon: 'status_notfound',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { GlPagination } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { removeBreakLine, removeWhitespace } from 'helpers/text_helper';
|
||||
import { GlPagination } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
|
||||
import EnvironmentTable from '~/environments/components/environments_table.vue';
|
||||
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { environmentsList } from '../mock_data';
|
||||
|
||||
describe('Environments Folder View', () => {
|
||||
|
|
@ -89,9 +89,9 @@ describe('Environments Folder View', () => {
|
|||
});
|
||||
|
||||
it('should render parent folder name', () => {
|
||||
expect(removeBreakLine(removeWhitespace(wrapper.find('.js-folder-name').text()))).toContain(
|
||||
'Environments / review',
|
||||
);
|
||||
expect(
|
||||
removeBreakLine(removeWhitespace(wrapper.find('[data-testid="folder-name"]').text())),
|
||||
).toContain('Environments / review');
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ describe('initGroupMembersApp', () => {
|
|||
el = document.createElement('div');
|
||||
el.setAttribute('data-members', membersJsonString);
|
||||
el.setAttribute('data-group-id', '234');
|
||||
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
|
||||
|
||||
window.gon = { current_user_id: 123 };
|
||||
|
||||
|
|
@ -69,4 +70,10 @@ describe('initGroupMembersApp', () => {
|
|||
|
||||
expect(vm.$store.state.tableFields).toEqual(['account']);
|
||||
});
|
||||
|
||||
it('sets `memberPath` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state.memberPath).toBe('/groups/foo-bar/-/group_members/:id');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { waitForCSSLoaded } from '../../../app/assets/javascripts/helpers/startup_css_helper';
|
||||
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
|
||||
|
||||
describe('waitForCSSLoaded', () => {
|
||||
let mockedCallback;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
|
||||
const buttonText = 'Test Button Text';
|
||||
|
|
@ -6,7 +7,7 @@ const buttonText = 'Test Button Text';
|
|||
describe('ReplyPlaceholder', () => {
|
||||
let wrapper;
|
||||
|
||||
const findButton = () => wrapper.find({ ref: 'button' });
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(ReplyPlaceholder, {
|
||||
|
|
@ -20,8 +21,8 @@ describe('ReplyPlaceholder', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('emits onClick event on button click', () => {
|
||||
findButton().trigger('click');
|
||||
it('should emit a onClick event on button click', () => {
|
||||
findButton().vm.$emit('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
|
|||
candelete="true"
|
||||
data-qa-selector="file_name_field"
|
||||
id="blob_local_7_file_path"
|
||||
showdelete="true"
|
||||
value="foo/bar/test.md"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,17 +19,12 @@ const TEST_BLOBS_UNLOADED = TEST_BLOBS.map(blob => ({ ...blob, content: '', isLo
|
|||
describe('snippets/components/snippet_blob_actions_edit', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}, snippetMultipleFiles = true) => {
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(SnippetBlobActionsEdit, {
|
||||
propsData: {
|
||||
initBlobs: TEST_BLOBS,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
snippetMultipleFiles,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -69,28 +64,24 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
describe.each`
|
||||
featureFlag | label | showDelete | showAdd
|
||||
${true} | ${'Files'} | ${true} | ${true}
|
||||
${false} | ${'File'} | ${false} | ${false}
|
||||
`('with feature flag = $featureFlag', ({ featureFlag, label, showDelete, showAdd }) => {
|
||||
describe('multi-file snippets rendering', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, featureFlag);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders label', () => {
|
||||
expect(findLabel().text()).toBe(label);
|
||||
expect(findLabel().text()).toBe('Files');
|
||||
});
|
||||
|
||||
it(`renders delete button (show=${showDelete})`, () => {
|
||||
it(`renders delete button (show=true)`, () => {
|
||||
expect(findFirstBlobEdit().props()).toMatchObject({
|
||||
showDelete,
|
||||
showDelete: true,
|
||||
canDelete: true,
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders add button (show=${showAdd})`, () => {
|
||||
expect(findAddButton().exists()).toBe(showAdd);
|
||||
it(`renders add button (show=true)`, () => {
|
||||
expect(findAddButton().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ describe('Snippet Blob Edit component', () => {
|
|||
});
|
||||
|
||||
it('shows blob header', () => {
|
||||
const { canDelete = true, showDelete = false } = props;
|
||||
const { canDelete = true, showDelete = true } = props;
|
||||
|
||||
expect(findHeader().props()).toMatchObject({
|
||||
canDelete,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue';
|
||||
import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
|
||||
import { accessRequest as member } from '../mock_data';
|
||||
|
||||
describe('AccessRequestActionButtons', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = shallowMount(AccessRequestActionButtons, {
|
||||
propsData: {
|
||||
member,
|
||||
isCurrentUser: true,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when user has `canRemove` permissions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remove member button', () => {
|
||||
expect(findRemoveMemberButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets props correctly', () => {
|
||||
expect(findRemoveMemberButton().props()).toMatchObject({
|
||||
memberId: member.id,
|
||||
title: 'Deny access',
|
||||
isAccessRequest: true,
|
||||
icon: 'close',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when member is the current user', () => {
|
||||
it('sets `message` prop correctly', () => {
|
||||
expect(findRemoveMemberButton().props('message')).toBe(
|
||||
`Are you sure you want to withdraw your access request for "${member.source.name}"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when member is not the current user', () => {
|
||||
it('sets `message` prop correctly', () => {
|
||||
createComponent({
|
||||
isCurrentUser: false,
|
||||
permissions: {
|
||||
canRemove: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRemoveMemberButton().props('message')).toBe(
|
||||
`Are you sure you want to deny ${member.user.name}'s request to join "${member.source.name}"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user does not have `canRemove` permissions', () => {
|
||||
it('does not render remove member button', () => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRemoveMemberButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue';
|
||||
import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
|
||||
import { invite as member } from '../mock_data';
|
||||
|
||||
describe('InviteActionButtons', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = shallowMount(InviteActionButtons, {
|
||||
propsData: {
|
||||
member,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when user has `canRemove` permissions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remove member button', () => {
|
||||
expect(findRemoveMemberButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets props correctly', () => {
|
||||
expect(findRemoveMemberButton().props()).toEqual({
|
||||
memberId: member.id,
|
||||
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.name}"`,
|
||||
title: 'Revoke invite',
|
||||
isAccessRequest: false,
|
||||
icon: 'remove',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user does not have `canRemove` permissions', () => {
|
||||
it('does not render remove member button', () => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRemoveMemberButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('RemoveMemberButton', () => {
|
||||
let wrapper;
|
||||
|
||||
const createStore = (state = {}) => {
|
||||
return new Vuex.Store({
|
||||
state: {
|
||||
memberPath: '/groups/foo-bar/-/group_members/:id',
|
||||
...state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (propsData = {}, state) => {
|
||||
wrapper = shallowMount(RemoveMemberButton, {
|
||||
localVue,
|
||||
store: createStore(state),
|
||||
propsData: {
|
||||
memberId: 1,
|
||||
message: 'Are you sure you want to remove John Smith?',
|
||||
title: 'Remove member',
|
||||
isAccessRequest: true,
|
||||
...propsData,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('sets attributes on button', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.attributes()).toMatchObject({
|
||||
'data-member-path': '/groups/foo-bar/-/group_members/1',
|
||||
'data-message': 'Are you sure you want to remove John Smith?',
|
||||
'data-is-access-request': 'true',
|
||||
'aria-label': 'Remove member',
|
||||
title: 'Remove member',
|
||||
icon: 'remove',
|
||||
});
|
||||
});
|
||||
|
||||
it('displays `title` prop as a tooltip', () => {
|
||||
createComponent();
|
||||
|
||||
expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('has CSS class used by `remove_member_modal.vue`', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.classes()).toContain('js-remove-member-button');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
|
||||
import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
|
||||
import { member, orphanedMember } from '../mock_data';
|
||||
|
||||
describe('UserActionButtons', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = shallowMount(UserActionButtons, {
|
||||
propsData: {
|
||||
member,
|
||||
isCurrentUser: false,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when user has `canRemove` permissions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remove member button', () => {
|
||||
expect(findRemoveMemberButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets props correctly', () => {
|
||||
expect(findRemoveMemberButton().props()).toEqual({
|
||||
memberId: member.id,
|
||||
message: `Are you sure you want to remove ${member.user.name} from "${member.source.name}"`,
|
||||
title: 'Remove member',
|
||||
isAccessRequest: false,
|
||||
icon: 'remove',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when member is orphaned', () => {
|
||||
it('sets `message` prop correctly', () => {
|
||||
createComponent({
|
||||
member: orphanedMember,
|
||||
permissions: {
|
||||
canRemove: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRemoveMemberButton().props('message')).toBe(
|
||||
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.name}"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user does not have `canRemove` permissions', () => {
|
||||
it('does not render remove member button', () => {
|
||||
createComponent({
|
||||
permissions: {
|
||||
canRemove: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRemoveMemberButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue