Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-06 18:08:49 +00:00
parent 4d922922a9
commit 1ca6880aac
116 changed files with 1315 additions and 520 deletions

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
65cd98f93c072f3a536021462c56e686cb2f8c7b
0fc40ef439ae4bbf91da2a5b454dfad5cb815a17

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export default {
showDelete: {
type: Boolean,
required: false,
default: false,
default: true,
},
},
computed: {

View File

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

View File

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

View File

@ -0,0 +1,11 @@
<script>
export default {
name: 'GroupActionButtons',
};
</script>
<template>
<span>
<!-- Temporarily empty -->
</span>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
export default ({ members, sourceId, currentUserId, tableFields }) => ({
export default ({ members, sourceId, currentUserId, tableFields, memberPath }) => ({
members,
sourceId,
currentUserId,
tableFields,
memberPath,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
- page_title _("Edit"), @environment.name, _("Environments")
- add_page_specific_style 'page_bundles/environments'
%h3.page-title
= _('Edit environment')

View File

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

View File

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

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("Environments")
- page_title _("New Environment")
- add_page_specific_style 'page_bundles/environments'
%h3.page-title
= _("New environment")

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@
= _('New milestone')
.milestones
#delete-milestone-modal
#js-delete-milestone-modal
#promote-milestone-modal
%ul.content-list

View File

@ -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?) }
&middot;
%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: ''

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Apply gl-button class to projects/issues/export_csv directory
merge_request: 44106
author: Lakshit
type: other

View File

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

View File

@ -0,0 +1,5 @@
---
title: Enable snippet multiple files
merge_request: 43246
author:
type: added

View File

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

View File

@ -0,0 +1,5 @@
---
title: Enable Gitpod button on file tree view
merge_request: 43961
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Migrate DeprecatedModal to GitLab UI Modal
merge_request: 42113
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Bump mini_magick gem version
merge_request: 44450
author:
type: other

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
![rbac](img/rbac_v13_1.png)
![RBAC](img/rbac_v13_1.png)
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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