Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
db5097a28b
commit
8fea353b90
|
@ -75,6 +75,7 @@ stages:
|
||||||
TEST_LICENSE_MODE: $QA_TEST_LICENSE_MODE
|
TEST_LICENSE_MODE: $QA_TEST_LICENSE_MODE
|
||||||
EE_LICENSE: $QA_EE_LICENSE
|
EE_LICENSE: $QA_EE_LICENSE
|
||||||
GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN
|
GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN
|
||||||
|
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Prepare stage
|
# Prepare stage
|
||||||
|
@ -369,6 +370,9 @@ ee:registry:
|
||||||
|
|
||||||
ee:registry-with-cdn:
|
ee:registry-with-cdn:
|
||||||
extends: .qa
|
extends: .qa
|
||||||
|
before_script:
|
||||||
|
- unset GITLAB_QA_ADMIN_ACCESS_TOKEN
|
||||||
|
- !reference [.gitlab-qa-install, before_script]
|
||||||
variables:
|
variables:
|
||||||
QA_SCENARIO: Test::Integration::RegistryWithCDN
|
QA_SCENARIO: Test::Integration::RegistryWithCDN
|
||||||
GCS_CDN_BUCKET_NAME: $QA_GCS_CDN_BUCKET_NAME
|
GCS_CDN_BUCKET_NAME: $QA_GCS_CDN_BUCKET_NAME
|
||||||
|
@ -440,6 +444,17 @@ ee:packages:
|
||||||
- !reference [.rules:test:qa, rules]
|
- !reference [.rules:test:qa, rules]
|
||||||
- if: $QA_SUITES =~ /Test::Instance::Packages/
|
- if: $QA_SUITES =~ /Test::Instance::Packages/
|
||||||
|
|
||||||
|
ee:elasticsearch:
|
||||||
|
extends: .qa
|
||||||
|
variables:
|
||||||
|
QA_SCENARIO: "Test::Integration::Elasticsearch"
|
||||||
|
script:
|
||||||
|
- unset ELASTIC_URL # unset url which is globally defined in .gitlab-ci.yml
|
||||||
|
- !reference [.qa, script]
|
||||||
|
rules:
|
||||||
|
- !reference [.rules:test:qa, rules]
|
||||||
|
- if: $QA_SUITES =~ /Test::Integration::Elasticsearch/
|
||||||
|
|
||||||
ee:object-storage:
|
ee:object-storage:
|
||||||
extends: .qa
|
extends: .qa
|
||||||
variables:
|
variables:
|
||||||
|
|
|
@ -17,26 +17,6 @@ Layout/FirstArrayElementIndentation:
|
||||||
- 'app/finders/user_groups_counter.rb'
|
- 'app/finders/user_groups_counter.rb'
|
||||||
- 'app/helpers/diff_helper.rb'
|
- 'app/helpers/diff_helper.rb'
|
||||||
- 'app/helpers/search_helper.rb'
|
- 'app/helpers/search_helper.rb'
|
||||||
- 'app/models/ci/job_token/scope.rb'
|
|
||||||
- 'app/models/container_repository.rb'
|
|
||||||
- 'app/models/customer_relations/contact.rb'
|
|
||||||
- 'app/models/customer_relations/organization.rb'
|
|
||||||
- 'app/models/group.rb'
|
|
||||||
- 'app/models/integration.rb'
|
|
||||||
- 'app/models/internal_id.rb'
|
|
||||||
- 'app/models/issue.rb'
|
|
||||||
- 'app/models/member.rb'
|
|
||||||
- 'app/models/merge_request.rb'
|
|
||||||
- 'app/models/namespace.rb'
|
|
||||||
- 'app/models/packages/package.rb'
|
|
||||||
- 'app/models/project.rb'
|
|
||||||
- 'app/models/projects/topic.rb'
|
|
||||||
- 'app/models/todo.rb'
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
- 'app/services/ci/delete_objects_service.rb'
|
|
||||||
- 'app/services/labels/transfer_service.rb'
|
|
||||||
- 'app/services/milestones/transfer_service.rb'
|
|
||||||
- 'app/workers/ssh_keys/expired_notification_worker.rb'
|
|
||||||
- 'config/initializers/postgres_partitioning.rb'
|
- 'config/initializers/postgres_partitioning.rb'
|
||||||
- 'db/post_migrate/20210812013042_remove_duplicate_project_authorizations.rb'
|
- 'db/post_migrate/20210812013042_remove_duplicate_project_authorizations.rb'
|
||||||
- 'ee/app/controllers/groups/settings/reporting_controller.rb'
|
- 'ee/app/controllers/groups/settings/reporting_controller.rb'
|
||||||
|
|
4
Gemfile
4
Gemfile
|
@ -199,8 +199,8 @@ gem 'state_machines-activerecord', '~> 0.8.0'
|
||||||
gem 'acts-as-taggable-on', '~> 9.0'
|
gem 'acts-as-taggable-on', '~> 9.0'
|
||||||
|
|
||||||
# Background jobs
|
# Background jobs
|
||||||
gem 'sidekiq', '~> 6.4'
|
gem 'sidekiq', '~> 6.4.0'
|
||||||
gem 'sidekiq-cron', '~> 1.2'
|
gem 'sidekiq-cron', '~> 1.4.0'
|
||||||
gem 'redis-namespace', '~> 1.8.1'
|
gem 'redis-namespace', '~> 1.8.1'
|
||||||
gem 'gitlab-sidekiq-fetcher', '0.8.0', require: 'sidekiq-reliable-fetch'
|
gem 'gitlab-sidekiq-fetcher', '0.8.0', require: 'sidekiq-reliable-fetch'
|
||||||
|
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -400,7 +400,7 @@ GEM
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubi (1.9.0)
|
erubi (1.9.0)
|
||||||
escape_utils (1.2.1)
|
escape_utils (1.2.1)
|
||||||
et-orbi (1.2.1)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
ethon (0.15.0)
|
ethon (0.15.0)
|
||||||
ffi (>= 1.15.0)
|
ffi (>= 1.15.0)
|
||||||
|
@ -509,7 +509,7 @@ GEM
|
||||||
fog-core
|
fog-core
|
||||||
nokogiri (>= 1.5.11, < 2.0.0)
|
nokogiri (>= 1.5.11, < 2.0.0)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.2.1)
|
fugit (1.2.3)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.1)
|
raabro (~> 1.1)
|
||||||
fuubar (2.2.0)
|
fuubar (2.2.0)
|
||||||
|
@ -1037,7 +1037,7 @@ GEM
|
||||||
get_process_mem (~> 0.2)
|
get_process_mem (~> 0.2)
|
||||||
puma (>= 2.7)
|
puma (>= 2.7)
|
||||||
pyu-ruby-sasl (0.0.3.3)
|
pyu-ruby-sasl (0.0.3.3)
|
||||||
raabro (1.1.6)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.4)
|
rack (2.2.4)
|
||||||
rack-accept (0.4.5)
|
rack-accept (0.4.5)
|
||||||
|
@ -1285,8 +1285,8 @@ GEM
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
sidekiq-cron (1.2.0)
|
sidekiq-cron (1.4.0)
|
||||||
fugit (~> 1.1)
|
fugit (~> 1)
|
||||||
sidekiq (>= 4.2.1)
|
sidekiq (>= 4.2.1)
|
||||||
sigdump (0.2.4)
|
sigdump (0.2.4)
|
||||||
signet (0.17.0)
|
signet (0.17.0)
|
||||||
|
@ -1747,8 +1747,8 @@ DEPENDENCIES
|
||||||
sentry-sidekiq (~> 5.1.1)
|
sentry-sidekiq (~> 5.1.1)
|
||||||
settingslogic (~> 2.0.9)
|
settingslogic (~> 2.0.9)
|
||||||
shoulda-matchers (~> 5.1.0)
|
shoulda-matchers (~> 5.1.0)
|
||||||
sidekiq (~> 6.4)
|
sidekiq (~> 6.4.0)
|
||||||
sidekiq-cron (~> 1.2)
|
sidekiq-cron (~> 1.4.0)
|
||||||
sigdump (~> 0.2.4)
|
sigdump (~> 0.2.4)
|
||||||
simple_po_parser (~> 1.1.6)
|
simple_po_parser (~> 1.1.6)
|
||||||
simplecov (~> 0.21)
|
simplecov (~> 0.21)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { truncate } from '~/lib/utils/text_utility';
|
||||||
import { n__ } from '~/locale';
|
import { n__ } from '~/locale';
|
||||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||||
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
|
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
|
||||||
|
import { HIDE_COMMENTS } from '../i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -55,6 +56,9 @@ export default {
|
||||||
return `${noteData.author.name}: ${note}`;
|
return `${noteData.author.name}: ${note}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
HIDE_COMMENTS,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -62,8 +66,10 @@ export default {
|
||||||
<div class="diff-comment-avatar-holders">
|
<div class="diff-comment-avatar-holders">
|
||||||
<button
|
<button
|
||||||
v-if="discussionsExpanded"
|
v-if="discussionsExpanded"
|
||||||
|
v-gl-tooltip
|
||||||
|
:title="$options.i18n.HIDE_COMMENTS"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="__('Show comments')"
|
:aria-label="$options.i18n.HIDE_COMMENTS"
|
||||||
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
|
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
|
||||||
@click="$emit('toggleLineDiscussions')"
|
@click="$emit('toggleLineDiscussions')"
|
||||||
>
|
>
|
||||||
|
|
|
@ -47,3 +47,5 @@ export const CONFLICT_TEXT = {
|
||||||
'Conflict: This file was added both in the source and target branches, but with different contents.',
|
'Conflict: This file was added both in the source and target branches, but with different contents.',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const HIDE_COMMENTS = __('Hide comments');
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlTooltipDirective,
|
GlTooltipDirective,
|
||||||
|
GlPopover,
|
||||||
|
GlButton,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import { kebabCase, snakeCase } from 'lodash';
|
import { kebabCase, snakeCase } from 'lodash';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
|
@ -17,6 +19,7 @@ import { IssuableType } from '~/issues/constants';
|
||||||
import { timeFor } from '~/lib/utils/datetime_utility';
|
import { timeFor } from '~/lib/utils/datetime_utility';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||||
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import {
|
import {
|
||||||
dropdowni18nText,
|
dropdowni18nText,
|
||||||
Tracking,
|
Tracking,
|
||||||
|
@ -47,7 +50,10 @@ export default {
|
||||||
GlSearchBoxByType,
|
GlSearchBoxByType,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
|
GlPopover,
|
||||||
|
GlButton,
|
||||||
},
|
},
|
||||||
|
mixins: [glFeatureFlagMixin()],
|
||||||
inject: {
|
inject: {
|
||||||
isClassicSidebar: {
|
isClassicSidebar: {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -66,6 +72,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
issuableAttribute: {
|
issuableAttribute: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -111,6 +118,10 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
update(data) {
|
update(data) {
|
||||||
|
if (this.glFeatures?.epicWidgetEditConfirmation && this.isEpic) {
|
||||||
|
this.hasCurrentAttribute = data?.workspace?.issuable.hasEpic;
|
||||||
|
}
|
||||||
|
|
||||||
return data?.workspace?.issuable.attribute;
|
return data?.workspace?.issuable.attribute;
|
||||||
},
|
},
|
||||||
error(error) {
|
error(error) {
|
||||||
|
@ -179,6 +190,8 @@ export default {
|
||||||
updating: false,
|
updating: false,
|
||||||
selectedTitle: null,
|
selectedTitle: null,
|
||||||
currentAttribute: null,
|
currentAttribute: null,
|
||||||
|
hasCurrentAttribute: false,
|
||||||
|
editConfirmation: false,
|
||||||
attributesList: [],
|
attributesList: [],
|
||||||
tracking: {
|
tracking: {
|
||||||
event: Tracking.editEvent,
|
event: Tracking.editEvent,
|
||||||
|
@ -228,6 +241,15 @@ export default {
|
||||||
snake: snakeCase(this.issuableAttribute),
|
snake: snakeCase(this.issuableAttribute),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
shouldShowConfirmationPopover() {
|
||||||
|
if (!this.glFeatures?.epicWidgetEditConfirmation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isEpic && this.currentAttribute === null && this.hasCurrentAttribute
|
||||||
|
? !this.editConfirmation
|
||||||
|
: false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateAttribute(attributeId) {
|
updateAttribute(attributeId) {
|
||||||
|
@ -299,6 +321,17 @@ export default {
|
||||||
setFocus() {
|
setFocus() {
|
||||||
this.$refs.search.focusInput();
|
this.$refs.search.focusInput();
|
||||||
},
|
},
|
||||||
|
handlePopoverClose() {
|
||||||
|
this.$refs.popover.$emit('close');
|
||||||
|
},
|
||||||
|
handlePopoverConfirm(cb) {
|
||||||
|
this.editConfirmation = true;
|
||||||
|
this.handlePopoverClose();
|
||||||
|
setTimeout(cb, 0);
|
||||||
|
},
|
||||||
|
handleEditConfirmation() {
|
||||||
|
this.$refs.popover.$emit('open');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -308,10 +341,13 @@ export default {
|
||||||
ref="editable"
|
ref="editable"
|
||||||
:title="attributeTypeTitle"
|
:title="attributeTypeTitle"
|
||||||
:data-testid="`${formatIssuableAttribute.kebab}-edit`"
|
:data-testid="`${formatIssuableAttribute.kebab}-edit`"
|
||||||
|
:button-id="`${formatIssuableAttribute.kebab}-edit`"
|
||||||
:tracking="tracking"
|
:tracking="tracking"
|
||||||
|
:should-show-confirmation-popover="shouldShowConfirmationPopover"
|
||||||
:loading="updating || loading"
|
:loading="updating || loading"
|
||||||
@open="handleOpen"
|
@open="handleOpen"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
|
@edit-confirm="handleEditConfirmation"
|
||||||
>
|
>
|
||||||
<template #collapsed>
|
<template #collapsed>
|
||||||
<slot name="value-collapsed" :current-attribute="currentAttribute">
|
<slot name="value-collapsed" :current-attribute="currentAttribute">
|
||||||
|
@ -332,6 +368,10 @@ export default {
|
||||||
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
|
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
|
||||||
>
|
>
|
||||||
<span v-if="updating">{{ selectedTitle }}</span>
|
<span v-if="updating">{{ selectedTitle }}</span>
|
||||||
|
<template v-else-if="!currentAttribute && hasCurrentAttribute">
|
||||||
|
<gl-icon name="warning" class="gl-text-orange-500" />
|
||||||
|
<span class="gl-text-gray-500">{{ i18n.noPermissionToView }}</span>
|
||||||
|
</template>
|
||||||
<span v-else-if="!currentAttribute" class="gl-text-gray-500">
|
<span v-else-if="!currentAttribute" class="gl-text-gray-500">
|
||||||
{{ $options.i18n.none }}
|
{{ $options.i18n.none }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -354,7 +394,40 @@ export default {
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template v-if="shouldShowConfirmationPopover" #default="{ toggle }">
|
||||||
|
<gl-popover
|
||||||
|
ref="popover"
|
||||||
|
:target="`${formatIssuableAttribute.kebab}-edit`"
|
||||||
|
placement="bottomleft"
|
||||||
|
boundary="viewport"
|
||||||
|
triggers="click"
|
||||||
|
>
|
||||||
|
<div class="gl-mb-4 gl-font-base">
|
||||||
|
{{ i18n.editConfirmation }}
|
||||||
|
</div>
|
||||||
|
<div class="gl-display-flex gl-align-items-center">
|
||||||
|
<gl-button
|
||||||
|
size="small"
|
||||||
|
variant="confirm"
|
||||||
|
category="primary"
|
||||||
|
data-testid="confirm-edit-cta"
|
||||||
|
@click.prevent="() => handlePopoverConfirm(toggle)"
|
||||||
|
>{{ i18n.editConfirmationCta }}</gl-button
|
||||||
|
>
|
||||||
|
<gl-button
|
||||||
|
class="gl-ml-auto"
|
||||||
|
size="small"
|
||||||
|
name="cancel"
|
||||||
|
variant="default"
|
||||||
|
category="primary"
|
||||||
|
data-testid="confirm-edit-cancel"
|
||||||
|
@click.prevent="handlePopoverClose"
|
||||||
|
>{{ i18n.editConfirmationCancel }}</gl-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</gl-popover>
|
||||||
|
</template>
|
||||||
|
<template v-else #default>
|
||||||
<gl-dropdown
|
<gl-dropdown
|
||||||
ref="newDropdown"
|
ref="newDropdown"
|
||||||
lazy
|
lazy
|
||||||
|
|
|
@ -14,6 +14,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
buttonId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -48,6 +53,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
shouldShowConfirmationPopover: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -97,6 +107,11 @@ export default {
|
||||||
window.removeEventListener('keyup', this.collapseOnEscape);
|
window.removeEventListener('keyup', this.collapseOnEscape);
|
||||||
},
|
},
|
||||||
toggle({ emitEvent = true } = {}) {
|
toggle({ emitEvent = true } = {}) {
|
||||||
|
if (this.shouldShowConfirmationPopover) {
|
||||||
|
this.$emit('edit-confirm');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.edit) {
|
if (this.edit) {
|
||||||
this.collapse({ emitEvent });
|
this.collapse({ emitEvent });
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,6 +147,7 @@ export default {
|
||||||
<slot name="collapsed-right"></slot>
|
<slot name="collapsed-right"></slot>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="canUpdate && !initialLoading && canEdit"
|
v-if="canUpdate && !initialLoading && canEdit"
|
||||||
|
:id="buttonId"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
size="small"
|
size="small"
|
||||||
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
|
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
|
||||||
|
@ -151,7 +167,7 @@ export default {
|
||||||
<slot name="collapsed">{{ __('None') }}</slot>
|
<slot name="collapsed">{{ __('None') }}</slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }">
|
<div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }">
|
||||||
<slot :edit="edit"></slot>
|
<slot :edit="edit" :toggle="toggle"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlIcon, GlLink, GlModal, GlButton, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
|
import {
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
GlModal,
|
||||||
|
GlButton,
|
||||||
|
GlModalDirective,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlTooltipDirective,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import { IssuableType } from '~/issues/constants';
|
import { IssuableType } from '~/issues/constants';
|
||||||
import { s__, __ } from '~/locale';
|
import { s__, __ } from '~/locale';
|
||||||
import { timeTrackingQueries } from '~/sidebar/constants';
|
import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '~/sidebar/constants';
|
||||||
|
|
||||||
import eventHub from '../../event_hub';
|
import eventHub from '../../event_hub';
|
||||||
import TimeTrackingCollapsedState from './collapsed_state.vue';
|
import TimeTrackingCollapsedState from './collapsed_state.vue';
|
||||||
|
@ -31,6 +39,7 @@ export default {
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlModal: GlModalDirective,
|
GlModal: GlModalDirective,
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
issuableType: {
|
issuableType: {
|
||||||
|
@ -162,6 +171,12 @@ export default {
|
||||||
this.issuableId
|
this.issuableId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
timeTrackingIconTitle() {
|
||||||
|
return this.showHelpState ? '' : HOW_TO_TRACK_TIME;
|
||||||
|
},
|
||||||
|
timeTrackingIconName() {
|
||||||
|
return this.showHelpState ? 'close' : 'question-o';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
/**
|
/**
|
||||||
|
@ -212,7 +227,12 @@ export default {
|
||||||
class="gl-ml-auto"
|
class="gl-ml-auto"
|
||||||
@click="toggleHelpState(!showHelpState)"
|
@click="toggleHelpState(!showHelpState)"
|
||||||
>
|
>
|
||||||
<gl-icon :name="showHelpState ? 'close' : 'question-o'" class="gl-text-gray-900!" />
|
<gl-icon
|
||||||
|
v-gl-tooltip.left
|
||||||
|
:title="timeTrackingIconTitle"
|
||||||
|
:name="timeTrackingIconName"
|
||||||
|
class="gl-text-gray-900!"
|
||||||
|
/>
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
|
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { s__, sprintf } from '~/locale';
|
import { s__, __, sprintf } from '~/locale';
|
||||||
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
|
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
|
||||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||||
import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
|
import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
|
||||||
|
@ -313,8 +313,26 @@ export function dropdowni18nText(issuableAttribute, issuableType) {
|
||||||
),
|
),
|
||||||
{ issuableAttribute, issuableType },
|
{ issuableAttribute, issuableType },
|
||||||
),
|
),
|
||||||
|
noPermissionToView: sprintf(
|
||||||
|
s__("DropdownWidget|You don't have permission to view this %{issuableAttribute}."),
|
||||||
|
{ issuableAttribute },
|
||||||
|
),
|
||||||
|
editConfirmation: sprintf(
|
||||||
|
s__(
|
||||||
|
'DropdownWidget|You do not have permission to view the currently assigned %{issuableAttribute} and will not be able to choose it again if you reassign it.',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
issuableAttribute,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
editConfirmationCta: sprintf(s__('DropdownWidget|Edit %{issuableAttribute}'), {
|
||||||
|
issuableAttribute,
|
||||||
|
}),
|
||||||
|
editConfirmationCancel: s__('DropdownWidget|Cancel'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const escalationStatusQuery = getEscalationStatusQuery;
|
export const escalationStatusQuery = getEscalationStatusQuery;
|
||||||
export const escalationStatusMutation = updateEscalationStatusMutation;
|
export const escalationStatusMutation = updateEscalationStatusMutation;
|
||||||
|
|
||||||
|
export const HOW_TO_TRACK_TIME = __('How to track time');
|
||||||
|
|
|
@ -80,6 +80,7 @@ export default {
|
||||||
v-if="!showDropdownContentsCreateView"
|
v-if="!showDropdownContentsCreateView"
|
||||||
ref="searchInput"
|
ref="searchInput"
|
||||||
:value="searchKey"
|
:value="searchKey"
|
||||||
|
:placeholder="__('Search labels')"
|
||||||
:disabled="labelsFetchInProgress"
|
:disabled="labelsFetchInProgress"
|
||||||
data-qa-selector="dropdown_input_field"
|
data-qa-selector="dropdown_input_field"
|
||||||
data-testid="dropdown-input-field"
|
data-testid="dropdown-input-field"
|
||||||
|
|
|
@ -7,5 +7,7 @@
|
||||||
class JiraConnect::OauthCallbacksController < ApplicationController
|
class JiraConnect::OauthCallbacksController < ApplicationController
|
||||||
feature_category :integrations
|
feature_category :integrations
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,6 +53,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
push_frontend_feature_flag(:realtime_labels, project)
|
push_frontend_feature_flag(:realtime_labels, project)
|
||||||
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_hierarchy, project)
|
push_frontend_feature_flag(:work_items_hierarchy, project)
|
||||||
|
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
|
||||||
push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,17 @@
|
||||||
module Types
|
module Types
|
||||||
module Ci
|
module Ci
|
||||||
class RunnerMembershipFilterEnum < BaseEnum
|
class RunnerMembershipFilterEnum < BaseEnum
|
||||||
graphql_name 'RunnerMembershipFilter'
|
graphql_name 'CiRunnerMembershipFilter'
|
||||||
description 'Values for filtering runners in namespaces.'
|
description 'Values for filtering runners in namespaces. ' \
|
||||||
|
'The previous type name `RunnerMembershipFilter` was deprecated in 15.4.'
|
||||||
|
|
||||||
value 'DIRECT',
|
value 'DIRECT',
|
||||||
description: "Include runners that have a direct relationship.",
|
description: "Include runners that have a direct relationship.",
|
||||||
value: :direct
|
value: :direct
|
||||||
|
|
||||||
value 'DESCENDANTS',
|
value 'DESCENDANTS',
|
||||||
description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).",
|
description: "Include runners that have either a direct or inherited relationship. " \
|
||||||
|
"These runners can be specific to a project or a group.",
|
||||||
value: :descendants
|
value: :descendants
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ module ApplicationSettingsHelper
|
||||||
:gravatar_enabled?,
|
:gravatar_enabled?,
|
||||||
:password_authentication_enabled_for_web?,
|
:password_authentication_enabled_for_web?,
|
||||||
:akismet_enabled?,
|
:akismet_enabled?,
|
||||||
|
:spam_check_endpoint_enabled?,
|
||||||
to: :'Gitlab::CurrentSettings.current_application_settings'
|
to: :'Gitlab::CurrentSettings.current_application_settings'
|
||||||
|
|
||||||
def user_oauth_applications?
|
def user_oauth_applications?
|
||||||
|
@ -60,6 +61,10 @@ module ApplicationSettingsHelper
|
||||||
all_protocols_enabled? || Gitlab::CurrentSettings.enabled_git_access_protocol == 'http'
|
all_protocols_enabled? || Gitlab::CurrentSettings.enabled_git_access_protocol == 'http'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def anti_spam_service_enabled?
|
||||||
|
akismet_enabled? || spam_check_endpoint_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
def enabled_protocol_button(container, protocol)
|
def enabled_protocol_button(container, protocol)
|
||||||
case protocol
|
case protocol
|
||||||
when 'ssh'
|
when 'ssh'
|
||||||
|
|
|
@ -30,10 +30,7 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_projects
|
def all_projects
|
||||||
Project.from_union([
|
Project.from_union(target_projects, remove_duplicates: false)
|
||||||
Project.id_in(source_project),
|
|
||||||
Project.id_in(target_project_ids)
|
|
||||||
], remove_duplicates: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -41,6 +38,13 @@ module Ci
|
||||||
def target_project_ids
|
def target_project_ids
|
||||||
Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
|
Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def target_projects
|
||||||
|
[
|
||||||
|
Project.id_in(source_project),
|
||||||
|
Project.id_in(target_project_ids)
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -263,10 +263,10 @@ class ContainerRepository < ApplicationRecord
|
||||||
.with_migration_import_started_at_nil_or_before(before_timestamp)
|
.with_migration_import_started_at_nil_or_before(before_timestamp)
|
||||||
|
|
||||||
union = ::Gitlab::SQL::Union.new([
|
union = ::Gitlab::SQL::Union.new([
|
||||||
stale_pre_importing,
|
stale_pre_importing,
|
||||||
stale_pre_import_done,
|
stale_pre_import_done,
|
||||||
stale_importing
|
stale_importing
|
||||||
])
|
])
|
||||||
from("(#{union.to_sql}) #{ContainerRepository.table_name}")
|
from("(#{union.to_sql}) #{ContainerRepository.table_name}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -79,22 +79,23 @@ class CustomerRelations::Contact < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.sort_by_name
|
def self.sort_by_name
|
||||||
order(Gitlab::Pagination::Keyset::Order.build([
|
order(Gitlab::Pagination::Keyset::Order.build(
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: 'last_name',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table[:last_name].asc,
|
attribute_name: 'last_name',
|
||||||
distinct: false
|
order_expression: arel_table[:last_name].asc,
|
||||||
),
|
distinct: false
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'first_name',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table[:first_name].asc,
|
attribute_name: 'first_name',
|
||||||
distinct: false
|
order_expression: arel_table[:first_name].asc,
|
||||||
),
|
distinct: false
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'id',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table[:id].asc
|
attribute_name: 'id',
|
||||||
)
|
order_expression: arel_table[:id].asc
|
||||||
]))
|
)
|
||||||
|
]))
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_ids_by_emails(group, emails)
|
def self.find_ids_by_emails(group, emails)
|
||||||
|
@ -117,22 +118,14 @@ class CustomerRelations::Contact < ApplicationRecord
|
||||||
JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
||||||
WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
|
WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
|
||||||
SQL
|
SQL
|
||||||
connection.execute(sanitize_sql([
|
connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
|
||||||
update_query,
|
|
||||||
old_group_id: group.root_ancestor.id,
|
|
||||||
new_group_id: group.id
|
|
||||||
]))
|
|
||||||
|
|
||||||
dupes_query = <<~SQL
|
dupes_query = <<~SQL
|
||||||
DELETE FROM #{table_name} AS existing_contacts
|
DELETE FROM #{table_name} AS existing_contacts
|
||||||
USING #{table_name} AS new_contacts
|
USING #{table_name} AS new_contacts
|
||||||
WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
||||||
SQL
|
SQL
|
||||||
connection.execute(sanitize_sql([
|
connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
|
||||||
dupes_query,
|
|
||||||
old_group_id: group.root_ancestor.id,
|
|
||||||
new_group_id: group.id
|
|
||||||
]))
|
|
||||||
|
|
||||||
where(group: group).update_all(group_id: group.root_ancestor.id)
|
where(group: group).update_all(group_id: group.root_ancestor.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,22 +66,14 @@ class CustomerRelations::Organization < ApplicationRecord
|
||||||
JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
||||||
WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
|
WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
|
||||||
SQL
|
SQL
|
||||||
connection.execute(sanitize_sql([
|
connection.execute(sanitize_sql([update_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
|
||||||
update_query,
|
|
||||||
old_group_id: group.root_ancestor.id,
|
|
||||||
new_group_id: group.id
|
|
||||||
]))
|
|
||||||
|
|
||||||
dupes_query = <<~SQL
|
dupes_query = <<~SQL
|
||||||
DELETE FROM #{table_name} AS existing_organizations
|
DELETE FROM #{table_name} AS existing_organizations
|
||||||
USING #{table_name} AS new_organizations
|
USING #{table_name} AS new_organizations
|
||||||
WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
||||||
SQL
|
SQL
|
||||||
connection.execute(sanitize_sql([
|
connection.execute(sanitize_sql([dupes_query, old_group_id: group.root_ancestor.id, new_group_id: group.id]))
|
||||||
dupes_query,
|
|
||||||
old_group_id: group.root_ancestor.id,
|
|
||||||
new_group_id: group.id
|
|
||||||
]))
|
|
||||||
|
|
||||||
where(group: group).update_all(group_id: group.root_ancestor.id)
|
where(group: group).update_all(group_id: group.root_ancestor.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -635,11 +635,11 @@ class Group < Namespace
|
||||||
# 4. They belong to an ancestor group
|
# 4. They belong to an ancestor group
|
||||||
def direct_and_indirect_users
|
def direct_and_indirect_users
|
||||||
User.from_union([
|
User.from_union([
|
||||||
User
|
User
|
||||||
.where(id: direct_and_indirect_members.select(:user_id))
|
.where(id: direct_and_indirect_members.select(:user_id))
|
||||||
.reorder(nil),
|
.reorder(nil),
|
||||||
project_users_with_descendants
|
project_users_with_descendants
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns all users (also inactive) that are members of the group because:
|
# Returns all users (also inactive) that are members of the group because:
|
||||||
|
@ -649,11 +649,11 @@ class Group < Namespace
|
||||||
# 4. They belong to an ancestor group
|
# 4. They belong to an ancestor group
|
||||||
def direct_and_indirect_users_with_inactive
|
def direct_and_indirect_users_with_inactive
|
||||||
User.from_union([
|
User.from_union([
|
||||||
User
|
User
|
||||||
.where(id: direct_and_indirect_members_with_inactive.select(:user_id))
|
.where(id: direct_and_indirect_members_with_inactive.select(:user_id))
|
||||||
.reorder(nil),
|
.reorder(nil),
|
||||||
project_users_with_descendants
|
project_users_with_descendants
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def users_count
|
def users_count
|
||||||
|
|
|
@ -401,9 +401,9 @@ class Integration < ApplicationRecord
|
||||||
.or(where(type: integration.type, instance: true)).select(:id)
|
.or(where(type: integration.type, instance: true)).select(:id)
|
||||||
|
|
||||||
from_union([
|
from_union([
|
||||||
where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
|
where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
|
||||||
where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
|
where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def activated?
|
def activated?
|
||||||
|
|
|
@ -143,10 +143,7 @@ class InternalId < ApplicationRecord
|
||||||
def track_greatest(new_value)
|
def track_greatest(new_value)
|
||||||
InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
|
InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
|
||||||
|
|
||||||
function = Arel::Nodes::NamedFunction.new('GREATEST', [
|
function = Arel::Nodes::NamedFunction.new('GREATEST', [arel_table[:last_value], new_value.to_i])
|
||||||
arel_table[:last_value],
|
|
||||||
new_value.to_i
|
|
||||||
])
|
|
||||||
|
|
||||||
next_iid = update_record!(subject, scope, usage, function)
|
next_iid = update_record!(subject, scope, usage, function)
|
||||||
return next_iid if next_iid
|
return next_iid if next_iid
|
||||||
|
|
|
@ -258,22 +258,23 @@ class Issue < ApplicationRecord
|
||||||
reversed_direction = direction == :asc ? :desc : :asc
|
reversed_direction = direction == :asc ? :desc : :asc
|
||||||
|
|
||||||
# rubocop: disable GitlabSecurity/PublicSend
|
# rubocop: disable GitlabSecurity/PublicSend
|
||||||
order = ::Gitlab::Pagination::Keyset::Order.build([
|
order = ::Gitlab::Pagination::Keyset::Order.build(
|
||||||
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: attribute_name,
|
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
column_expression: column,
|
attribute_name: attribute_name,
|
||||||
order_expression: column.send(direction).send(nullable),
|
column_expression: column,
|
||||||
reversed_order_expression: column.send(reversed_direction).send(nullable),
|
order_expression: column.send(direction).send(nullable),
|
||||||
order_direction: direction,
|
reversed_order_expression: column.send(reversed_direction).send(nullable),
|
||||||
distinct: false,
|
order_direction: direction,
|
||||||
add_to_projections: true,
|
distinct: false,
|
||||||
nullable: nullable
|
add_to_projections: true,
|
||||||
),
|
nullable: nullable
|
||||||
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'id',
|
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table['id'].desc
|
attribute_name: 'id',
|
||||||
)
|
order_expression: arel_table['id'].desc
|
||||||
])
|
)
|
||||||
|
])
|
||||||
# rubocop: enable GitlabSecurity/PublicSend
|
# rubocop: enable GitlabSecurity/PublicSend
|
||||||
|
|
||||||
order.apply_cursor_conditions(scope).order(order)
|
order.apply_cursor_conditions(scope).order(order)
|
||||||
|
|
|
@ -74,10 +74,7 @@ class Member < ApplicationRecord
|
||||||
projects = source.root_ancestor.all_projects
|
projects = source.root_ancestor.all_projects
|
||||||
project_members = Member.default_scoped.where(source: projects).select(*Member.cached_column_list)
|
project_members = Member.default_scoped.where(source: projects).select(*Member.cached_column_list)
|
||||||
|
|
||||||
Member.default_scoped.from_union([
|
Member.default_scoped.from_union([group_members, project_members]).merge(self)
|
||||||
group_members,
|
|
||||||
project_members
|
|
||||||
]).merge(self)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :excluding_users, ->(user_ids) do
|
scope :excluding_users, ->(user_ids) do
|
||||||
|
|
|
@ -343,23 +343,24 @@ class MergeRequest < ApplicationRecord
|
||||||
column_expression = MergeRequest::Metrics.arel_table[metric]
|
column_expression = MergeRequest::Metrics.arel_table[metric]
|
||||||
column_expression_with_direction = direction == 'ASC' ? column_expression.asc : column_expression.desc
|
column_expression_with_direction = direction == 'ASC' ? column_expression.asc : column_expression.desc
|
||||||
|
|
||||||
order = Gitlab::Pagination::Keyset::Order.build([
|
order = Gitlab::Pagination::Keyset::Order.build(
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: "merge_request_metrics_#{metric}",
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
column_expression: column_expression,
|
attribute_name: "merge_request_metrics_#{metric}",
|
||||||
order_expression: column_expression_with_direction.nulls_last,
|
column_expression: column_expression,
|
||||||
reversed_order_expression: column_expression_with_direction.reverse.nulls_first,
|
order_expression: column_expression_with_direction.nulls_last,
|
||||||
order_direction: direction,
|
reversed_order_expression: column_expression_with_direction.reverse.nulls_first,
|
||||||
nullable: :nulls_last,
|
order_direction: direction,
|
||||||
distinct: false,
|
nullable: :nulls_last,
|
||||||
add_to_projections: true
|
distinct: false,
|
||||||
),
|
add_to_projections: true
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'merge_request_metrics_id',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: MergeRequest::Metrics.arel_table[:id].desc,
|
attribute_name: 'merge_request_metrics_id',
|
||||||
add_to_projections: true
|
order_expression: MergeRequest::Metrics.arel_table[:id].desc,
|
||||||
)
|
add_to_projections: true
|
||||||
])
|
)
|
||||||
|
])
|
||||||
|
|
||||||
order.apply_cursor_conditions(join_metrics).order(order)
|
order.apply_cursor_conditions(join_metrics).order(order)
|
||||||
end
|
end
|
||||||
|
|
|
@ -176,10 +176,12 @@ class Namespace < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :sorted_by_similarity_and_parent_id_desc, -> (search) do
|
scope :sorted_by_similarity_and_parent_id_desc, -> (search) do
|
||||||
order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
|
order_expression = Gitlab::Database::SimilarityScore.build_expression(
|
||||||
{ column: arel_table["path"], multiplier: 1 },
|
search: search,
|
||||||
{ column: arel_table["name"], multiplier: 0.7 }
|
rules: [
|
||||||
])
|
{ column: arel_table["path"], multiplier: 1 },
|
||||||
|
{ column: arel_table["name"], multiplier: 0.7 }
|
||||||
|
])
|
||||||
reorder(order_expression.desc, Namespace.arel_table['parent_id'].desc.nulls_last, Namespace.arel_table['id'].desc)
|
reorder(order_expression.desc, Namespace.arel_table['parent_id'].desc.nulls_last, Namespace.arel_table['id'].desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -242,22 +242,23 @@ class Packages::Package < ApplicationRecord
|
||||||
reverse_order_direction = direction == :asc ? desc_order_expression : asc_order_expression
|
reverse_order_direction = direction == :asc ? desc_order_expression : asc_order_expression
|
||||||
arel_order_classes = ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::AREL_ORDER_CLASSES.invert
|
arel_order_classes = ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::AREL_ORDER_CLASSES.invert
|
||||||
|
|
||||||
::Gitlab::Pagination::Keyset::Order.build([
|
::Gitlab::Pagination::Keyset::Order.build(
|
||||||
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: "#{join_table}_#{column_name}",
|
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
column_expression: join_class.arel_table[column_name],
|
attribute_name: "#{join_table}_#{column_name}",
|
||||||
order_expression: order_direction,
|
column_expression: join_class.arel_table[column_name],
|
||||||
reversed_order_expression: reverse_order_direction,
|
order_expression: order_direction,
|
||||||
order_direction: direction,
|
reversed_order_expression: reverse_order_direction,
|
||||||
distinct: false,
|
order_direction: direction,
|
||||||
add_to_projections: true
|
distinct: false,
|
||||||
),
|
add_to_projections: true
|
||||||
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'id',
|
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]),
|
attribute_name: 'id',
|
||||||
add_to_projections: true
|
order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]),
|
||||||
)
|
add_to_projections: true
|
||||||
])
|
)
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def versions
|
def versions
|
||||||
|
|
|
@ -569,26 +569,29 @@ class Project < ApplicationRecord
|
||||||
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
|
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
|
||||||
|
|
||||||
scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do
|
scope :sorted_by_similarity_desc, -> (search, include_in_select: false) do
|
||||||
order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
|
order_expression = Gitlab::Database::SimilarityScore.build_expression(
|
||||||
{ column: arel_table["path"], multiplier: 1 },
|
search: search,
|
||||||
{ column: arel_table["name"], multiplier: 0.7 },
|
rules: [
|
||||||
{ column: arel_table["description"], multiplier: 0.2 }
|
{ column: arel_table["path"], multiplier: 1 },
|
||||||
])
|
{ column: arel_table["name"], multiplier: 0.7 },
|
||||||
|
{ column: arel_table["description"], multiplier: 0.2 }
|
||||||
|
])
|
||||||
|
|
||||||
order = Gitlab::Pagination::Keyset::Order.build([
|
order = Gitlab::Pagination::Keyset::Order.build(
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: 'similarity',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
column_expression: order_expression,
|
attribute_name: 'similarity',
|
||||||
order_expression: order_expression.desc,
|
column_expression: order_expression,
|
||||||
order_direction: :desc,
|
order_expression: order_expression.desc,
|
||||||
distinct: false,
|
order_direction: :desc,
|
||||||
add_to_projections: true
|
distinct: false,
|
||||||
),
|
add_to_projections: true
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'id',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: Project.arel_table[:id].desc
|
attribute_name: 'id',
|
||||||
)
|
order_expression: Project.arel_table[:id].desc
|
||||||
])
|
)
|
||||||
|
])
|
||||||
|
|
||||||
order.apply_cursor_conditions(reorder(order))
|
order.apply_cursor_conditions(reorder(order))
|
||||||
end
|
end
|
||||||
|
@ -2562,10 +2565,7 @@ class Project < ApplicationRecord
|
||||||
def badges
|
def badges
|
||||||
return project_badges unless group
|
return project_badges unless group
|
||||||
|
|
||||||
Badge.from_union([
|
Badge.from_union([project_badges, GroupBadge.where(group: group.self_and_ancestors)])
|
||||||
project_badges,
|
|
||||||
GroupBadge.where(group: group.self_and_ancestors)
|
|
||||||
])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_requests_allowing_push_to_user(user)
|
def merge_requests_allowing_push_to_user(user)
|
||||||
|
|
|
@ -18,9 +18,11 @@ module Projects
|
||||||
scope :without_assigned_projects, -> { where(total_projects_count: 0) }
|
scope :without_assigned_projects, -> { where(total_projects_count: 0) }
|
||||||
scope :order_by_non_private_projects_count, -> { order(non_private_projects_count: :desc).order(id: :asc) }
|
scope :order_by_non_private_projects_count, -> { order(non_private_projects_count: :desc).order(id: :asc) }
|
||||||
scope :reorder_by_similarity, -> (search) do
|
scope :reorder_by_similarity, -> (search) do
|
||||||
order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
|
order_expression = Gitlab::Database::SimilarityScore.build_expression(
|
||||||
{ column: arel_table['name'] }
|
search: search,
|
||||||
])
|
rules: [
|
||||||
|
{ column: arel_table['name'] }
|
||||||
|
])
|
||||||
reorder(order_expression.desc, arel_table['non_private_projects_count'].desc, arel_table['id'])
|
reorder(order_expression.desc, arel_table['non_private_projects_count'].desc, arel_table['id'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -96,10 +96,11 @@ class Todo < ApplicationRecord
|
||||||
def for_group_ids_and_descendants(group_ids)
|
def for_group_ids_and_descendants(group_ids)
|
||||||
groups = Group.groups_including_descendants_by(group_ids)
|
groups = Group.groups_including_descendants_by(group_ids)
|
||||||
|
|
||||||
from_union([
|
from_union(
|
||||||
for_project(Project.for_group(groups)),
|
[
|
||||||
for_group(groups)
|
for_project(Project.for_group(groups)),
|
||||||
])
|
for_group(groups)
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns `true` if the current user has any todos for the given target with the optional given state.
|
# Returns `true` if the current user has any todos for the given target with the optional given state.
|
||||||
|
|
|
@ -696,28 +696,29 @@ class User < ApplicationRecord
|
||||||
scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query)
|
scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query)
|
||||||
scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit]))
|
scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit]))
|
||||||
|
|
||||||
order = Gitlab::Pagination::Keyset::Order.build([
|
order = Gitlab::Pagination::Keyset::Order.build(
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: 'users_match_priority',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: sanitized_order_sql.asc,
|
attribute_name: 'users_match_priority',
|
||||||
add_to_projections: true,
|
order_expression: sanitized_order_sql.asc,
|
||||||
distinct: false
|
add_to_projections: true,
|
||||||
),
|
distinct: false
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'users_name',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table[:name].asc,
|
attribute_name: 'users_name',
|
||||||
add_to_projections: true,
|
order_expression: arel_table[:name].asc,
|
||||||
nullable: :not_nullable,
|
add_to_projections: true,
|
||||||
distinct: false
|
nullable: :not_nullable,
|
||||||
),
|
distinct: false
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'users_id',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: arel_table[:id].asc,
|
attribute_name: 'users_id',
|
||||||
add_to_projections: true,
|
order_expression: arel_table[:id].asc,
|
||||||
nullable: :not_nullable,
|
add_to_projections: true,
|
||||||
distinct: true
|
nullable: :not_nullable,
|
||||||
)
|
distinct: true
|
||||||
])
|
)
|
||||||
|
])
|
||||||
scope.reorder(order)
|
scope.reorder(order)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1357,10 +1358,11 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def accessible_deploy_keys
|
def accessible_deploy_keys
|
||||||
DeployKey.from_union([
|
DeployKey.from_union(
|
||||||
DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
|
[
|
||||||
DeployKey.are_public
|
DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
|
||||||
])
|
DeployKey.are_public
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def created_by
|
def created_by
|
||||||
|
@ -1661,10 +1663,11 @@ class User < ApplicationRecord
|
||||||
strong_memoize(:forkable_namespaces) do
|
strong_memoize(:forkable_namespaces) do
|
||||||
personal_namespace = Namespace.where(id: namespace_id)
|
personal_namespace = Namespace.where(id: namespace_id)
|
||||||
|
|
||||||
Namespace.from_union([
|
Namespace.from_union(
|
||||||
manageable_groups(include_groups_with_developer_maintainer_access: true),
|
[
|
||||||
personal_namespace
|
manageable_groups(include_groups_with_developer_maintainer_access: true),
|
||||||
])
|
personal_namespace
|
||||||
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2243,10 +2246,11 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_groups_without_shared_membership
|
def authorized_groups_without_shared_membership
|
||||||
Group.from_union([
|
Group.from_union(
|
||||||
groups.select(*Namespace.cached_column_list),
|
[
|
||||||
authorized_projects.joins(:namespace).select(*Namespace.cached_column_list)
|
groups.select(*Namespace.cached_column_list),
|
||||||
])
|
authorized_projects.joins(:namespace).select(*Namespace.cached_column_list)
|
||||||
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_groups_with_shared_membership
|
def authorized_groups_with_shared_membership
|
||||||
|
@ -2256,10 +2260,10 @@ class User < ApplicationRecord
|
||||||
Group
|
Group
|
||||||
.with(cte.to_arel)
|
.with(cte.to_arel)
|
||||||
.from_union([
|
.from_union([
|
||||||
Group.from(cte_alias),
|
Group.from(cte_alias),
|
||||||
Group.joins(:shared_with_group_links)
|
Group.joins(:shared_with_group_links)
|
||||||
.where(group_group_links: { shared_with_group_id: Group.from(cte_alias) })
|
.where(group_group_links: { shared_with_group_id: Group.from(cte_alias) })
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_private_profile_to_false
|
def default_private_profile_to_false
|
||||||
|
|
|
@ -27,9 +27,7 @@ module Ci
|
||||||
# `find_by_sql` performs a write in this case and we need to wrap it in
|
# `find_by_sql` performs a write in this case and we need to wrap it in
|
||||||
# a transaction to stick to the primary database.
|
# a transaction to stick to the primary database.
|
||||||
Ci::DeletedObject.transaction do
|
Ci::DeletedObject.transaction do
|
||||||
Ci::DeletedObject.find_by_sql([
|
Ci::DeletedObject.find_by_sql([next_batch_sql, new_pick_up_at: RETRY_IN.from_now])
|
||||||
next_batch_sql, new_pick_up_at: RETRY_IN.from_now
|
|
||||||
])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
|
@ -40,9 +40,9 @@ module Labels
|
||||||
def labels_to_transfer
|
def labels_to_transfer
|
||||||
Label
|
Label
|
||||||
.from_union([
|
.from_union([
|
||||||
group_labels_applied_to_issues,
|
group_labels_applied_to_issues,
|
||||||
group_labels_applied_to_merge_requests
|
group_labels_applied_to_merge_requests
|
||||||
])
|
])
|
||||||
.reorder(nil)
|
.reorder(nil)
|
||||||
.distinct
|
.distinct
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,10 +35,7 @@ module Milestones
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def milestones_to_transfer
|
def milestones_to_transfer
|
||||||
Milestone.from_union([
|
Milestone.from_union([group_milestones_applied_to_issues, group_milestones_applied_to_merge_requests])
|
||||||
group_milestones_applied_to_issues,
|
|
||||||
group_milestones_applied_to_merge_requests
|
|
||||||
])
|
|
||||||
.reorder(nil)
|
.reorder(nil)
|
||||||
.distinct
|
.distinct
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
- prometheus_help_link_url = help_page_path('administration/monitoring/prometheus/gitlab_metrics')
|
- prometheus_help_link_url = help_page_path('administration/monitoring/prometheus/gitlab_metrics')
|
||||||
- prometheus_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: prometheus_help_link_url }
|
- prometheus_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: prometheus_help_link_url }
|
||||||
= f.gitlab_ui_checkbox_component :prometheus_metrics_enabled,
|
= f.gitlab_ui_checkbox_component :prometheus_metrics_enabled,
|
||||||
_('Enable health and performance metrics endpoint'),
|
_('Enable GitLab Prometheus metrics endpoint'),
|
||||||
help_text: s_('AdminSettings|Enable a Prometheus endpoint that exposes health and performance statistics. The Health Check menu item appears in the Monitoring section of the Admin Area. Restart required. %{link_start}Learn more.%{link_end}').html_safe % { link_start: prometheus_help_link_start, link_end: '</a>'.html_safe }
|
help_text: s_('AdminSettings|Enable collection of application metrics. Restart required. %{link_start}Learn how to export metrics to Prometheus%{link_end}.').html_safe % { link_start: prometheus_help_link_start, link_end: '</a>'.html_safe }
|
||||||
.form-text.gl-text-gray-500.gl-pl-6
|
.form-text.gl-text-gray-500.gl-pl-6
|
||||||
- unless Gitlab::Metrics.metrics_folder_present?
|
- unless Gitlab::Metrics.metrics_folder_present?
|
||||||
- icon_link = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory'), target: '_blank', rel: 'noopener noreferrer'
|
- icon_link = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory'), target: '_blank', rel: 'noopener noreferrer'
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||||
%p
|
%p
|
||||||
= _('Monitor the health and performance of GitLab with Prometheus.')
|
= _('Monitor GitLab with Prometheus.')
|
||||||
.settings-content
|
.settings-content
|
||||||
= render 'prometheus'
|
= render 'prometheus'
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,13 @@
|
||||||
= link_to _('Remove user'), admin_spam_log_path(spam_log, remove_user: true),
|
= link_to _('Remove user'), admin_spam_log_path(spam_log, remove_user: true),
|
||||||
data: { confirm: _("USER %{user_name} WILL BE REMOVED! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') }, method: :delete, class: "gl-button btn btn-sm btn-danger"
|
data: { confirm: _("USER %{user_name} WILL BE REMOVED! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') }, method: :delete, class: "gl-button btn btn-sm btn-danger"
|
||||||
%td
|
%td
|
||||||
- if spam_log.submitted_as_ham?
|
-# TODO: Remove conditonal once spamcheck supports this https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/spam/spamcheck/-/issues/190
|
||||||
.gl-button.btn.btn-default.btn-sm.disabled.gl-mb-3
|
- if akismet_enabled?
|
||||||
= _("Submitted as ham")
|
- if spam_log.submitted_as_ham?
|
||||||
- else
|
.gl-button.btn.btn-default.btn-sm.disabled.gl-mb-3
|
||||||
= link_to _('Submit as ham'), mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'gl-button btn btn-default btn-sm gl-mb-3'
|
= _("Submitted as ham")
|
||||||
|
- else
|
||||||
|
= link_to _('Submit as ham'), mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'gl-button btn btn-default btn-sm gl-mb-3'
|
||||||
- if user && !user.blocked?
|
- if user && !user.blocked?
|
||||||
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "gl-button btn btn-default btn-sm gl-mb-3"
|
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "gl-button btn btn-default btn-sm gl-mb-3"
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -175,7 +175,7 @@
|
||||||
%strong.fly-out-top-item-name
|
%strong.fly-out-top-item-name
|
||||||
= _('Kubernetes')
|
= _('Kubernetes')
|
||||||
|
|
||||||
- if akismet_enabled?
|
- if anti_spam_service_enabled?
|
||||||
= nav_link(controller: :spam_logs) do
|
= nav_link(controller: :spam_logs) do
|
||||||
= link_to admin_spam_logs_path do
|
= link_to admin_spam_logs_path do
|
||||||
.nav-icon-container
|
.nav-icon-container
|
||||||
|
|
|
@ -15,19 +15,20 @@ module SshKeys
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def perform
|
def perform
|
||||||
order = Gitlab::Pagination::Keyset::Order.build([
|
order = Gitlab::Pagination::Keyset::Order.build(
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
[
|
||||||
attribute_name: 'expires_at_utc',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: Arel.sql("date(expires_at AT TIME ZONE 'UTC')").asc,
|
attribute_name: 'expires_at_utc',
|
||||||
nullable: :not_nullable,
|
order_expression: Arel.sql("date(expires_at AT TIME ZONE 'UTC')").asc,
|
||||||
distinct: false,
|
nullable: :not_nullable,
|
||||||
add_to_projections: true
|
distinct: false,
|
||||||
),
|
add_to_projections: true
|
||||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
),
|
||||||
attribute_name: 'id',
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||||
order_expression: Key.arel_table[:id].asc
|
attribute_name: 'id',
|
||||||
)
|
order_expression: Key.arel_table[:id].asc
|
||||||
])
|
)
|
||||||
|
])
|
||||||
|
|
||||||
scope = Key.expired_today_and_not_notified.order(order)
|
scope = Key.expired_today_and_not_notified.order(order)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: epic_widget_edit_confirmation
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96872
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372429
|
||||||
|
milestone: '15.4'
|
||||||
|
type: development
|
||||||
|
group: group::product planning
|
||||||
|
default_enabled: false
|
|
@ -2,7 +2,7 @@
|
||||||
name: use_pipeline_wizard_for_pages
|
name: use_pipeline_wizard_for_pages
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78276
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78276
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349095
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349095
|
||||||
milestone: '15.3'
|
milestone: '15.4'
|
||||||
type: development
|
type: development
|
||||||
group: group::incubation
|
group: group::incubation
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
|
@ -151,6 +151,7 @@ options:
|
||||||
- p_ci_templates_implicit_jobs_browser_performance_testing_latest
|
- p_ci_templates_implicit_jobs_browser_performance_testing_latest
|
||||||
- p_ci_templates_implicit_jobs_cf_provision
|
- p_ci_templates_implicit_jobs_cf_provision
|
||||||
- p_ci_templates_implicit_jobs_build_latest
|
- p_ci_templates_implicit_jobs_build_latest
|
||||||
|
- p_ci_templates_implicit_jobs_sast_iac
|
||||||
- p_ci_templates_implicit_security_sast
|
- p_ci_templates_implicit_security_sast
|
||||||
- p_ci_templates_implicit_security_dast_runner_validation
|
- p_ci_templates_implicit_security_dast_runner_validation
|
||||||
- p_ci_templates_implicit_security_dast_on_demand_scan
|
- p_ci_templates_implicit_security_dast_on_demand_scan
|
||||||
|
@ -167,6 +168,7 @@ options:
|
||||||
- p_ci_templates_implicit_security_api_fuzzing
|
- p_ci_templates_implicit_security_api_fuzzing
|
||||||
- p_ci_templates_implicit_security_dast
|
- p_ci_templates_implicit_security_dast
|
||||||
- p_ci_templates_implicit_security_cluster_image_scanning
|
- p_ci_templates_implicit_security_cluster_image_scanning
|
||||||
|
- p_ci_templates_implicit_security_sast_iac
|
||||||
- p_ci_templates_kaniko
|
- p_ci_templates_kaniko
|
||||||
- p_ci_templates_qualys_iac_security
|
- p_ci_templates_qualys_iac_security
|
||||||
- p_ci_templates_liquibase
|
- p_ci_templates_liquibase
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_security_sast_iac_monthly
|
||||||
|
description: Count of pipelines with implicit SAST runs using the stable SAST IaC template
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: "static_analysis"
|
||||||
|
product_category: SAST
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "15.4"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86275
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- p_ci_templates_implicit_jobs_sast_iac
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_sast_iac_monthly
|
||||||
|
description: Count of pipelines with implicit SAST jobs using the stable SAST IaC template
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: "static_analysis"
|
||||||
|
product_category: SAST
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "15.4"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86275
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- p_ci_templates_implicit_jobs_sast_iac
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_security_sast_iac_weekly
|
||||||
|
description: Count of pipelines with implicit SAST runs using the stable SAST IaC template
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: "static_analysis"
|
||||||
|
product_category: SAST
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "15.4"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86275
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- p_ci_templates_implicit_jobs_sast_iac
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_sast_iac_weekly
|
||||||
|
description: Count of pipelines with implicit SAST jobs using the stable SAST IaC template
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: "static_analysis"
|
||||||
|
product_category: SAST
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: "15.4"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86275
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: optional
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
performance_indicator_type: []
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- p_ci_templates_implicit_jobs_sast_iac
|
|
@ -12972,7 +12972,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="grouprunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
|
| <a id="grouprunnersactive"></a>`active` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 14.8. This was renamed. Use: `paused`. |
|
||||||
| <a id="grouprunnersmembership"></a>`membership` | [`RunnerMembershipFilter`](#runnermembershipfilter) | Control which runners to include in the results. |
|
| <a id="grouprunnersmembership"></a>`membership` | [`CiRunnerMembershipFilter`](#cirunnermembershipfilter) | Control which runners to include in the results. |
|
||||||
| <a id="grouprunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
|
| <a id="grouprunnerspaused"></a>`paused` | [`Boolean`](#boolean) | Filter runners by `paused` (true) or `active` (false) status. |
|
||||||
| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
|
| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
|
||||||
| <a id="grouprunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
|
| <a id="grouprunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
|
||||||
|
@ -19513,6 +19513,15 @@ Values for YAML processor result.
|
||||||
| <a id="cirunneraccesslevelnot_protected"></a>`NOT_PROTECTED` | A runner that is not protected. |
|
| <a id="cirunneraccesslevelnot_protected"></a>`NOT_PROTECTED` | A runner that is not protected. |
|
||||||
| <a id="cirunneraccesslevelref_protected"></a>`REF_PROTECTED` | A runner that is ref protected. |
|
| <a id="cirunneraccesslevelref_protected"></a>`REF_PROTECTED` | A runner that is ref protected. |
|
||||||
|
|
||||||
|
### `CiRunnerMembershipFilter`
|
||||||
|
|
||||||
|
Values for filtering runners in namespaces. The previous type name `RunnerMembershipFilter` was deprecated in 15.4.
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| <a id="cirunnermembershipfilterdescendants"></a>`DESCENDANTS` | Include runners that have either a direct or inherited relationship. These runners can be specific to a project or a group. |
|
||||||
|
| <a id="cirunnermembershipfilterdirect"></a>`DIRECT` | Include runners that have a direct relationship. |
|
||||||
|
|
||||||
### `CiRunnerSort`
|
### `CiRunnerSort`
|
||||||
|
|
||||||
Values for sorting runners.
|
Values for sorting runners.
|
||||||
|
@ -20718,15 +20727,6 @@ Status of a requirement based on last test report.
|
||||||
| <a id="requirementstatusfiltermissing"></a>`MISSING` | Requirements without any test report. |
|
| <a id="requirementstatusfiltermissing"></a>`MISSING` | Requirements without any test report. |
|
||||||
| <a id="requirementstatusfilterpassed"></a>`PASSED` | Passed test report. |
|
| <a id="requirementstatusfilterpassed"></a>`PASSED` | Passed test report. |
|
||||||
|
|
||||||
### `RunnerMembershipFilter`
|
|
||||||
|
|
||||||
Values for filtering runners in namespaces.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
| ----- | ----------- |
|
|
||||||
| <a id="runnermembershipfilterdescendants"></a>`DESCENDANTS` | Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried). |
|
|
||||||
| <a id="runnermembershipfilterdirect"></a>`DIRECT` | Include runners that have a direct relationship. |
|
|
||||||
|
|
||||||
### `SastUiComponentSize`
|
### `SastUiComponentSize`
|
||||||
|
|
||||||
Size of UI component in SAST configuration page.
|
Size of UI component in SAST configuration page.
|
||||||
|
|
|
@ -0,0 +1,354 @@
|
||||||
|
---
|
||||||
|
stage: none
|
||||||
|
group: unassigned
|
||||||
|
comments: false
|
||||||
|
description: 'Next Rate Limiting Architecture'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Next Rate Limiting Architecture
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Introducing reasonable application limits is a very important step in any SaaS
|
||||||
|
platform scaling strategy. The more users a SaaS platform has, the more
|
||||||
|
important it is to introduce sensible rate limiting and policies enforcement
|
||||||
|
that will help to achieve availability goals, reduce the problem of noisy
|
||||||
|
neighbours for users and ensure that they can keep using a platform
|
||||||
|
successfully.
|
||||||
|
|
||||||
|
This is especially true for GitLab.com. Our goal is to have a reasonable and
|
||||||
|
transparent strategy for enforcing application limits, which will become a
|
||||||
|
definition of a responsible usage, to help us with keeping our availability and
|
||||||
|
user satisfaction at a desired level.
|
||||||
|
|
||||||
|
We've been introducing various application limits for many years already, but
|
||||||
|
we've never had a consistent strategy for doing it. What we want to build now is
|
||||||
|
a consistent framework used by engineers and product managers, across entire
|
||||||
|
application stack, to define, expose and enforce limits and policies.
|
||||||
|
|
||||||
|
Lack of consistency in defining limits, not being able to expose them to our
|
||||||
|
users, support engineers and satellite services, has negative impact on our
|
||||||
|
productivity, makes it difficult to introduce new limits and eventually
|
||||||
|
prevents us from enforcing responsible usage on all layers of our application
|
||||||
|
stack.
|
||||||
|
|
||||||
|
This blueprint has been written to consolidate our limits and to describe the
|
||||||
|
vision of our next rate limiting and policies enforcement architecture.
|
||||||
|
|
||||||
|
_Disclaimer: The following contains information related to upcoming products,
|
||||||
|
features, and functionality._
|
||||||
|
|
||||||
|
_It is important to note that the information presented is for informational
|
||||||
|
purposes only. Please do not rely on this information for purchasing or
|
||||||
|
planning purposes._
|
||||||
|
|
||||||
|
_As with all projects, the items mentioned in this document and linked pages are
|
||||||
|
subject to change or delay. The development, release and timing of any
|
||||||
|
products, features, or functionality remain at the sole discretion of GitLab
|
||||||
|
Inc._
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
**Implement a next architecture for rate limiting and policies definition.**
|
||||||
|
|
||||||
|
## Challenges
|
||||||
|
|
||||||
|
- We have many ways to define application limits, in many different places.
|
||||||
|
- It is difficult to understand what limits have been applied to a request.
|
||||||
|
- It is difficult to introduce new limits, even more to define policies.
|
||||||
|
- Finding what limits are defined requires performing a codebase audit.
|
||||||
|
- We don't have a good way to expose limits to satellite services like Registry.
|
||||||
|
- We enforce a number of different policies via opaque external systems
|
||||||
|
(Pipeline Validation Service, Bouncer, Watchtower, Cloudflare, Haproxy).
|
||||||
|
- There is not standardized way to define policies in a way consistent with defining limits.
|
||||||
|
- It is difficult to understand when a user is approaching a limit threshold.
|
||||||
|
- There is no way to automatically notify a user when they are approaching thresholds.
|
||||||
|
- There is no single way to change limits for a namespace / project / user / customer.
|
||||||
|
- There is no single way to monitor limits through real-time metrics.
|
||||||
|
- There is no framework for hierarchical limit configuration (instance / namespace / sub-group / project).
|
||||||
|
- We allow disabling rate-limiting for some marquee SaaS customers, but this
|
||||||
|
increases a risk for those same customers. We should instead be able to set
|
||||||
|
higher limits.
|
||||||
|
|
||||||
|
## Opportunity
|
||||||
|
|
||||||
|
We want to build a new framework, making it easier to define limits, quotas and
|
||||||
|
policies, and to enforce / adjust them in a controlled way, through robust
|
||||||
|
monitoring capabilities.
|
||||||
|
|
||||||
|
<!-- markdownlint-disable MD029 -->
|
||||||
|
|
||||||
|
1. Build a framework to define and enforce limits in GitLab Rails.
|
||||||
|
2. Build an API to consume limits in satellite service and expose them to users.
|
||||||
|
3. Extract parts of this framework into a dedicated GitLab Limits Service.
|
||||||
|
|
||||||
|
<!-- markdownlint-enable MD029 -->
|
||||||
|
|
||||||
|
The most important opportunity here is consolidation happening on multiple
|
||||||
|
levels:
|
||||||
|
|
||||||
|
1. Consolidate on the application limits tooling used in GitLab Rails.
|
||||||
|
1. Consolidate on the process of adding and managing application limits.
|
||||||
|
1. Consolidate on the behavior of hierarchical cascade of limits and overrides.
|
||||||
|
1. Consolidate on the application limits tooling used across entire application stack.
|
||||||
|
1. Consolidate on the policies enforcement tooling used across entire company.
|
||||||
|
|
||||||
|
Once we do that we will unlock another opportunity: to ship the new framework /
|
||||||
|
tooling as a GitLab feature to unlock these consolidation benefits for our
|
||||||
|
users, customers and entire wider community audience.
|
||||||
|
|
||||||
|
### Limits, quotas and policies
|
||||||
|
|
||||||
|
This document aims to describe our technical vision for building the next rate
|
||||||
|
limiting architecture for GitLab.com. We refer to this architectural evolution
|
||||||
|
as "the next rate limiting architecture", but this is a mental shortcut,
|
||||||
|
because we actually want to build a better framework that will make it easier
|
||||||
|
for us to manage not only rate limits, but also quotas and policies.
|
||||||
|
|
||||||
|
Below you can find a short definition of what we understand by a limit, by a
|
||||||
|
quota and by a policy.
|
||||||
|
|
||||||
|
- **Limit:** A constraint on application usage, typically used to mitigate
|
||||||
|
risks to performance, stability, and security.
|
||||||
|
- _Example:_ API calls per second for a given IP address
|
||||||
|
- _Example:_ `git clone` events per minute for a given user
|
||||||
|
- _Example:_ maximum artifact upload size of 1GB
|
||||||
|
- **Quota:** A global constraint in application usage that is aggregated across an
|
||||||
|
entire namespace over the duration of their billing cycle.
|
||||||
|
- _Example:_ 400 CI/CD minutes per namespace per month
|
||||||
|
- _Example:_ 10GB transfer per namespace per month
|
||||||
|
- **Policy:** A representation of business logic that is decoupled from application
|
||||||
|
code. Decoupled policy definitions allow logic to be shared across multiple services
|
||||||
|
and/or "hot-loaded" at runtime without releasing a new version of the application.
|
||||||
|
- _Example:_ decode and verify a JWT, determine whether the user has access to the
|
||||||
|
given resource based on the JWT's scopes and claims
|
||||||
|
- _Example:_ deny access based on group-level constraints
|
||||||
|
(such as IP allowlist, SSO, and 2FA) across all services
|
||||||
|
|
||||||
|
Technically, all of these are limits, because rate limiting is still
|
||||||
|
"limiting", quota is usually a business limit, and policy limits what you can
|
||||||
|
do with the application to enforce specific rules. By referring to a "limit" in
|
||||||
|
this document we mean a limit that is defined to protect business, availability
|
||||||
|
and security.
|
||||||
|
|
||||||
|
### Framework to define and enforce limits
|
||||||
|
|
||||||
|
First we want to build a new framework that will allow us to define and enforce
|
||||||
|
application limits, in the GitLab Rails project context, in a more consistent
|
||||||
|
and established way. In order to do that, we will need to build a new
|
||||||
|
abstraction that will tell engineers how to define a limit in a structured way
|
||||||
|
(presumably using YAML or Cue format) and then how to consume the limit in the
|
||||||
|
application itself.
|
||||||
|
|
||||||
|
We already do have many limits defined in the application, we can use them to
|
||||||
|
triangulate to find a reasonable abstraction that will consolidate how we
|
||||||
|
define, use and enforce limits.
|
||||||
|
|
||||||
|
We envision building a simple Ruby library here (we can add it to LabKit) that
|
||||||
|
will make it trivial for engineers to check if a certain limit has been
|
||||||
|
exceeded or not.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: my_limit_name
|
||||||
|
actors: user
|
||||||
|
context: project, group, pipeline
|
||||||
|
type: rate / second
|
||||||
|
group: pipeline::execution
|
||||||
|
limits:
|
||||||
|
warn: 2B / day
|
||||||
|
soft: 100k / s
|
||||||
|
hard: 500k / s
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Gitlab::Limits::RateThreshold.enforce(:my_limit_name) do |threshold|
|
||||||
|
actor = current_user
|
||||||
|
context = current_project
|
||||||
|
|
||||||
|
threshold.available do |limit|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
|
||||||
|
threshold.approaching do |limit|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
|
||||||
|
threshold.exceeded do |limit|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, when `my_limit_name` is defined in YAML, engineers will
|
||||||
|
be check the current state and execute appropriate code block depending on the
|
||||||
|
past usage / resource consumption.
|
||||||
|
|
||||||
|
Things we want to build and support by default:
|
||||||
|
|
||||||
|
1. Comprehensive dashboards showing how often limits are being hit.
|
||||||
|
1. Notifications about the risk of hitting limits.
|
||||||
|
1. Automation checking if limits definitions are being enforced properly.
|
||||||
|
1. Different types of limits - time bound / number per resource etc.
|
||||||
|
1. A panel that makes it easy to override limits per plan / namespace.
|
||||||
|
1. Logging that will expose limits applied in Kibana.
|
||||||
|
1. An automatically generated documentation page describing all the limits.
|
||||||
|
|
||||||
|
### API to expose limits and policies
|
||||||
|
|
||||||
|
Once we have an established a consistent way to define application limits we
|
||||||
|
can build a few API endpoints that will allow us to expose them to our users,
|
||||||
|
customers and other satellite services that may want to consume them.
|
||||||
|
|
||||||
|
Users will be able to ask the API about the limits / thresholds that have been
|
||||||
|
set for them, how often they are hitting them, and what impact those might have
|
||||||
|
on their business. This kind of transparency can help them with communicating
|
||||||
|
their needs to customer success team at GitLab, and we will be able to
|
||||||
|
communicate how the responsible usage is defined at a given moment.
|
||||||
|
|
||||||
|
Because of how GitLab architecture has been built, GitLab Rails application, in
|
||||||
|
most cases, behaves as a central enterprise service bus (ESB) and there are a
|
||||||
|
few satellite services communicating with it. Services like Container Registry,
|
||||||
|
GitLab Runners, Gitaly, Workhorse, KAS could use the API to receive a set of
|
||||||
|
application limits those are supposed to enforce. This will still allow us to
|
||||||
|
define all of them in a single place.
|
||||||
|
|
||||||
|
### GitLab Policy Service
|
||||||
|
|
||||||
|
_Disclaimer_: Extracting a GitLab Policy Service might be out of scope of the
|
||||||
|
current workstream organized around implementing this blueprint.
|
||||||
|
|
||||||
|
Not all limits can be easily described in YAML. There are some more complex
|
||||||
|
policies that require a bit more sophisticated approach and a declarative
|
||||||
|
programming language used to enforce them. One example of such a language might be
|
||||||
|
[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) language.
|
||||||
|
It is a standardized way to define policies in
|
||||||
|
[OPA - Open Policy Agent](https://www.openpolicyagent.org/). At GitLab we are
|
||||||
|
already using OPA in some departments. We envision the need to additional
|
||||||
|
consolidation to not only consolidate on the tooling we are using internally at
|
||||||
|
GitLab, but to also transform the Next Rate Limiting Architecture into
|
||||||
|
something we can make a part of the product itself.
|
||||||
|
|
||||||
|
Today, we already do have a policy service we are using to decide whether a
|
||||||
|
pipeline can be created or not. There are many policies defined in
|
||||||
|
[Pipeline Validation Service](https://gitlab.com/gitlab-org/modelops/anti-abuse/pipeline-validation-service).
|
||||||
|
There is a significant opportunity here in transforming Pipeline Validation
|
||||||
|
Service into a general purpose GitLab Policy Service / GitLab Policy Agent that
|
||||||
|
will be well integrated into the GitLab product itself.
|
||||||
|
|
||||||
|
Generalizing Pipeline Validation Service into GitLab Policy Service can bring a
|
||||||
|
few interesting benefits:
|
||||||
|
|
||||||
|
1. Consolidate on our tooling across the company to improve efficiency.
|
||||||
|
1. Integrate our GitLab Rails limits framework to resolve policies using the policy service.
|
||||||
|
1. Do not struggle to define complex policies in YAML and hack evaluating them in Ruby.
|
||||||
|
1. Build a policy for GraphQL queries limiting using query execution cost estimation.
|
||||||
|
1. Make it easier to resolve policies that do not need "hierarchical limits" structure.
|
||||||
|
1. Make GitLab Policy Service part of the product and integrate it into the single application.
|
||||||
|
|
||||||
|
We envision using GitLab Policy Service to be place to define policies that do
|
||||||
|
not require knowing anything about the hierarchical structure of the limits.
|
||||||
|
There are limits that do not need this, like IP addresses allow-list, spam
|
||||||
|
checks, configuration validation etc.
|
||||||
|
|
||||||
|
We defined "Policy" as a stateless, functional-style, limit. It takes input
|
||||||
|
arguments and evaluates to either true or false. It should not require a global
|
||||||
|
counter or any other volatile global state to get evaluated. It may still
|
||||||
|
require to have a globally defined rules / configuration, but this state is not
|
||||||
|
volatile in a same way a rate limiting counter may be, or a megabytes consumed
|
||||||
|
to evaluate quota limit.
|
||||||
|
|
||||||
|
## Hierarchical limits
|
||||||
|
|
||||||
|
GitLab application aggregates users, projects, groups and namespaces in a
|
||||||
|
hierarchical way. This hierarchical structure has been designed to make it
|
||||||
|
easier to manage permissions, streamline workflows, and allow users and
|
||||||
|
customers to store related projects, repositories, and other artifacts,
|
||||||
|
together.
|
||||||
|
|
||||||
|
It is important to design the new rate limiting framework in a way that it
|
||||||
|
built on top of this hierarchical structure and engineers, customers, SREs and
|
||||||
|
other stakeholders can understand how limits are being applied, enforced and
|
||||||
|
overridden within the hierarchy of namespaces, groups and projects.
|
||||||
|
|
||||||
|
We want to reduce the cognitive load required to understand how limits are
|
||||||
|
being managed within the existing permissions structure. We might need to build
|
||||||
|
a simple and easy-to-understand formula for how our application decides which
|
||||||
|
limits and thresholds to apply for a given request and a given actor:
|
||||||
|
|
||||||
|
> GitLab will read default limits for every operation, all overrides configured
|
||||||
|
> and will choose a limit with the highest precedence configured. A limit
|
||||||
|
> precedence needs to be explicitly configured for every override, a default
|
||||||
|
> limit has precedence 100.
|
||||||
|
|
||||||
|
One way in which we can simplify limits management in general is to:
|
||||||
|
|
||||||
|
1. Have default limits / thresholds defined in YAML files with a default precedence 100.
|
||||||
|
1. Allow limits to be overridden through the API, store overrides in the database.
|
||||||
|
1. Every limit / threshold override needs to have an integer precedence value provided.
|
||||||
|
1. Build an API that will take an actor and expose limits applicable for it.
|
||||||
|
1. Build a dashboard showing actors with non-standard limits / overrides.
|
||||||
|
1. Build a observability around this showing in Kibana when non-standard limits are being used.
|
||||||
|
|
||||||
|
The points above represent an idea to use precedence score (or Z-Index for
|
||||||
|
limits), but there may be better solutions, like just defining a direction of
|
||||||
|
overrides - a lower limit might always override a limit defined higher in the
|
||||||
|
hierarchy. Choosing a proper solution will require a thoughtful research.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. Try to avoid building rate limiting framework in a tightly coupled way.
|
||||||
|
1. Build application limits API in a way that it can be easily extracted to a separate service.
|
||||||
|
1. Build application limits definition in a way that is independent from the Rails application.
|
||||||
|
1. Build tooling that produce consistent behavior and results across programming languages.
|
||||||
|
1. Build the new framework in a way that we can extend to allow self-managed admins to customize limits.
|
||||||
|
1. Maintain consistent features and behavior across SaaS and self-managed codebase.
|
||||||
|
1. Be mindful about a cognitive load added by the hierarchical limits, aim to reduce it.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Request For Comments.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
- 2022-04-27: [Rate Limit Architecture Working Group](https://about.gitlab.com/company/team/structure/working-groups/rate-limit-architecture/) started.
|
||||||
|
- 2022-06-07: Working Group members [started submitting technical proposals](https://gitlab.com/gitlab-org/gitlab/-/issues/364524) for the next rate limiting architecture.
|
||||||
|
- 2022-06-15: We started [scoring proposals](https://docs.google.com/spreadsheets/d/1DFHU1kSdTnpydwM5P2RK8NhVBNWgEHvzT72eOhB8F9E) submitted by Working Group members.
|
||||||
|
- 2022-07-06: A fourth, [consolidated proposal](https://gitlab.com/gitlab-org/gitlab/-/issues/364524#note_1017640650), has been submitted.
|
||||||
|
- 2022-07-12: Started working on the design document following [Architecture Evolution Workflow](https://about.gitlab.com/handbook/engineering/architecture/workflow/).
|
||||||
|
- 2022-09-08: The initial version of the blueprint has been merged.
|
||||||
|
|
||||||
|
## Who
|
||||||
|
|
||||||
|
Proposal:
|
||||||
|
|
||||||
|
<!-- vale gitlab.Spelling = NO -->
|
||||||
|
|
||||||
|
| Role | Who
|
||||||
|
|------------------------------|-------------------------|
|
||||||
|
| Author | Grzegorz Bizon |
|
||||||
|
| Author | Fabio Pitino |
|
||||||
|
| Author | Marshall Cottrell |
|
||||||
|
| Author | Hayley Swimelar |
|
||||||
|
| Engineering Leader | Sam Goldstein |
|
||||||
|
| Product Manager | |
|
||||||
|
| Architecture Evolution Coach | |
|
||||||
|
| Recommender | |
|
||||||
|
| Recommender | |
|
||||||
|
| Recommender | |
|
||||||
|
| Recommender | |
|
||||||
|
|
||||||
|
DRIs:
|
||||||
|
|
||||||
|
| Role | Who
|
||||||
|
|------------------------------|------------------------|
|
||||||
|
| Leadership | |
|
||||||
|
| Product | |
|
||||||
|
| Engineering | |
|
||||||
|
|
||||||
|
Domain experts:
|
||||||
|
|
||||||
|
| Area | Who
|
||||||
|
|------------------------------|------------------------|
|
||||||
|
| | |
|
||||||
|
|
||||||
|
<!-- vale gitlab.Spelling = YES -->
|
|
@ -25,6 +25,7 @@ to be emitted from the rails application:
|
||||||
## Existing SLIs
|
## Existing SLIs
|
||||||
|
|
||||||
1. [`rails_request_apdex`](rails_request_apdex.md)
|
1. [`rails_request_apdex`](rails_request_apdex.md)
|
||||||
|
1. `global_search_apdex`
|
||||||
|
|
||||||
## Defining a new SLI
|
## Defining a new SLI
|
||||||
|
|
||||||
|
@ -135,10 +136,7 @@ After that, add the following information:
|
||||||
into the error budgets for stage groups.
|
into the error budgets for stage groups.
|
||||||
- `description`: a Markdown string explaining the SLI. It will
|
- `description`: a Markdown string explaining the SLI. It will
|
||||||
be shown on dashboards and alerts.
|
be shown on dashboards and alerts.
|
||||||
- `kind`: the kind of indicator. Only `sliDefinition.apdexKind` is supported at the moment.
|
- `kind`: the kind of indicator. For example `sliDefinition.apdexKind`.
|
||||||
Reach out in
|
|
||||||
[this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1395)
|
|
||||||
if you want to implement an SLI for success or error rates.
|
|
||||||
|
|
||||||
When done, run `make generate` to generate recording rules for
|
When done, run `make generate` to generate recording rules for
|
||||||
the new SLI. This command creates recordings for all services
|
the new SLI. This command creates recordings for all services
|
||||||
|
@ -152,9 +150,9 @@ When these changes are merged, and the aggregations in
|
||||||
the success ratio of the new aggregated metrics. For example:
|
the success ratio of the new aggregated metrics. For example:
|
||||||
|
|
||||||
```prometheus
|
```prometheus
|
||||||
sum by (environment, stage, type)(gitlab_sli_aggregation:rails_request_apdex:apdex:success:rate_1h)
|
sum by (environment, stage, type)(application_sli_aggregation:rails_request:apdex:success:rate_1h)
|
||||||
/
|
/
|
||||||
sum by (environment, stage, type)(gitlab_sli_aggregation:rails_request_apdex:apdex:weight:rate_1h)
|
sum by (environment, stage, type)(application_sli_aggregation:rails_request:apdex:weight:score_1h)
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows the success ratio, which can guide you to set an
|
This shows the success ratio, which can guide you to set an
|
||||||
|
|
|
@ -54,8 +54,7 @@ module API
|
||||||
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
|
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
|
||||||
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
|
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
|
||||||
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
|
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
|
||||||
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
|
optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user'
|
||||||
optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads
|
|
||||||
optional :theme_id, type: Integer, desc: 'The GitLab theme for the user'
|
optional :theme_id, type: Integer, desc: 'The GitLab theme for the user'
|
||||||
optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer'
|
optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer'
|
||||||
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
|
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
|
||||||
|
|
|
@ -14,6 +14,9 @@ module Gitlab
|
||||||
DEPRECATIONS = [
|
DEPRECATIONS = [
|
||||||
Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(
|
Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(
|
||||||
old_name: 'CiRunnerUpgradeStatusType', new_name: 'CiRunnerUpgradeStatus', milestone: '15.3'
|
old_name: 'CiRunnerUpgradeStatusType', new_name: 'CiRunnerUpgradeStatus', milestone: '15.3'
|
||||||
|
),
|
||||||
|
Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(
|
||||||
|
old_name: 'RunnerMembershipFilter', new_name: 'CiRunnerMembershipFilter', milestone: '15.4'
|
||||||
)
|
)
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
|
|
@ -2289,6 +2289,9 @@ msgstr ""
|
||||||
msgid "Add label(s)"
|
msgid "Add label(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add license"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Add list"
|
msgid "Add list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2721,7 +2724,7 @@ msgstr ""
|
||||||
msgid "AdminSettings|Enable Service Ping"
|
msgid "AdminSettings|Enable Service Ping"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AdminSettings|Enable a Prometheus endpoint that exposes health and performance statistics. The Health Check menu item appears in the Monitoring section of the Admin Area. Restart required. %{link_start}Learn more.%{link_end}"
|
msgid "AdminSettings|Enable collection of application metrics. Restart required. %{link_start}Learn how to export metrics to Prometheus%{link_end}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AdminSettings|Enable kuromoji custom analyzer: Indexing"
|
msgid "AdminSettings|Enable kuromoji custom analyzer: Indexing"
|
||||||
|
@ -14028,6 +14031,12 @@ msgstr ""
|
||||||
msgid "DropdownWidget|Assign %{issuableAttribute}"
|
msgid "DropdownWidget|Assign %{issuableAttribute}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DropdownWidget|Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DropdownWidget|Edit %{issuableAttribute}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again."
|
msgid "DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14043,6 +14052,12 @@ msgstr ""
|
||||||
msgid "DropdownWidget|No open %{issuableAttribute} found"
|
msgid "DropdownWidget|No open %{issuableAttribute} found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DropdownWidget|You do not have permission to view the currently assigned %{issuableAttribute} and will not be able to choose it again if you reassign it."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DropdownWidget|You don't have permission to view this %{issuableAttribute}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Due Date"
|
msgid "Due Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14454,6 +14469,9 @@ msgstr ""
|
||||||
msgid "Enable GitLab Error Tracking"
|
msgid "Enable GitLab Error Tracking"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable GitLab Prometheus metrics endpoint"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enable Gitpod"
|
msgid "Enable Gitpod"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14538,9 +14556,6 @@ msgstr ""
|
||||||
msgid "Enable header and footer in emails"
|
msgid "Enable header and footer in emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enable health and performance metrics endpoint"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-product marketing emails"
|
msgid "Enable in-product marketing emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19414,6 +19429,9 @@ msgid_plural "Hide charts"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
msgid "Hide comments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Hide comments on this file"
|
msgid "Hide comments on this file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19578,6 +19596,9 @@ msgstr ""
|
||||||
msgid "How the job limiter handles jobs exceeding the thresholds specified below. The 'track' mode only logs the jobs. The 'compress' mode compresses the jobs and raises an exception if the compressed size exceeds the limit."
|
msgid "How the job limiter handles jobs exceeding the thresholds specified below. The 'track' mode only logs the jobs. The 'compress' mode compresses the jobs and raises an exception if the compressed size exceeds the limit."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "How to track time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "I accept the %{terms_link}"
|
msgid "I accept the %{terms_link}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25657,10 +25678,10 @@ msgstr ""
|
||||||
msgid "Monitor"
|
msgid "Monitor"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Monitor Settings"
|
msgid "Monitor GitLab with Prometheus."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Monitor the health and performance of GitLab with Prometheus."
|
msgid "Monitor Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Monitor your errors by integrating with Sentry."
|
msgid "Monitor your errors by integrating with Sentry."
|
||||||
|
|
|
@ -5,12 +5,13 @@ require 'date'
|
||||||
module QA
|
module QA
|
||||||
module Resource
|
module Resource
|
||||||
class PersonalAccessToken < Base
|
class PersonalAccessToken < Base
|
||||||
attr_accessor :name
|
attr_writer :name
|
||||||
|
|
||||||
# The user for which the personal access token is to be created
|
# The user for which the personal access token is to be created
|
||||||
# This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator
|
# This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator
|
||||||
attr_writer :user
|
attr_writer :user
|
||||||
|
|
||||||
|
attribute :id
|
||||||
attribute :token
|
attribute :token
|
||||||
|
|
||||||
# Only Admins can create PAT via the API.
|
# Only Admins can create PAT via the API.
|
||||||
|
@ -41,13 +42,28 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_get_path
|
def api_get_path
|
||||||
'/personal_access_tokens'
|
"/personal_access_tokens/#{id}"
|
||||||
|
rescue NoValueError
|
||||||
|
user_id = user.respond_to?(:id) ? user.id : Resource::User.build(user).reload!.id
|
||||||
|
|
||||||
|
token = auto_paginated_response(request_url("/personal_access_tokens?user_id=#{user_id}", per_page: '100'))
|
||||||
|
.find { |t| t[:name] == name }
|
||||||
|
|
||||||
|
raise ResourceNotFoundError unless token
|
||||||
|
|
||||||
|
@id = token[:id]
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
def name
|
||||||
|
@name ||= "api-personal-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_post_body
|
def api_post_body
|
||||||
{
|
{
|
||||||
name: name || 'api-test-token',
|
name: name,
|
||||||
scopes: ["api"]
|
scopes: ["api"],
|
||||||
|
expires_at: expires_at.to_s
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -65,6 +81,11 @@ module QA
|
||||||
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token
|
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Expire in 2 days just in case the token is created just before midnight
|
||||||
|
def expires_at
|
||||||
|
@expires_at || Time.now.utc.to_date + 2
|
||||||
|
end
|
||||||
|
|
||||||
def fabricate!
|
def fabricate!
|
||||||
return if find_and_set_value
|
return if find_and_set_value
|
||||||
|
|
||||||
|
@ -76,8 +97,7 @@ module QA
|
||||||
Page::Profile::PersonalAccessTokens.perform do |token_page|
|
Page::Profile::PersonalAccessTokens.perform do |token_page|
|
||||||
token_page.fill_token_name(name || 'api-test-token')
|
token_page.fill_token_name(name || 'api-test-token')
|
||||||
token_page.check_api
|
token_page.check_api
|
||||||
# Expire in 2 days just in case the token is created just before midnight
|
token_page.fill_expiry_date(expires_at)
|
||||||
token_page.fill_expiry_date(Time.now.utc.to_date + 2)
|
|
||||||
token_page.click_create_token_button
|
token_page.click_create_token_button
|
||||||
|
|
||||||
self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
|
self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
|
||||||
|
|
|
@ -34,6 +34,13 @@ module QA
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.build(struct)
|
||||||
|
Resource::User.init do |usr|
|
||||||
|
usr.username = struct.username
|
||||||
|
usr.password = struct.password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def admin?
|
def admin?
|
||||||
api_resource&.dig(:is_admin) || false
|
api_resource&.dig(:is_admin) || false
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,7 +66,7 @@ module QA
|
||||||
|
|
||||||
def download_project_archive_via_api(api_client, project, type = 'tar.gz')
|
def download_project_archive_via_api(api_client, project, type = 'tar.gz')
|
||||||
get_project_archive_zip = Runtime::API::Request.new(api_client, project.api_get_archive_path(type))
|
get_project_archive_zip = Runtime::API::Request.new(api_client, project.api_get_archive_path(type))
|
||||||
project_archive_download = get(get_project_archive_zip.url, raw_response: true)
|
project_archive_download = Support::API.get(get_project_archive_zip.url, raw_response: true)
|
||||||
expect(project_archive_download.code).to eq(200)
|
expect(project_archive_download.code).to eq(200)
|
||||||
|
|
||||||
project_archive_download.file
|
project_archive_download.file
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
module QA
|
module QA
|
||||||
module Support
|
module Support
|
||||||
module API
|
module API
|
||||||
|
extend self
|
||||||
|
|
||||||
HTTP_STATUS_OK = 200
|
HTTP_STATUS_OK = 200
|
||||||
HTTP_STATUS_CREATED = 201
|
HTTP_STATUS_CREATED = 201
|
||||||
HTTP_STATUS_NO_CONTENT = 204
|
HTTP_STATUS_NO_CONTENT = 204
|
||||||
|
|
|
@ -27,7 +27,6 @@ module QA
|
||||||
include Support::API
|
include Support::API
|
||||||
|
|
||||||
IGNORED_RESOURCES = [
|
IGNORED_RESOURCES = [
|
||||||
'QA::Resource::PersonalAccessToken',
|
|
||||||
'QA::Resource::CiVariable',
|
'QA::Resource::CiVariable',
|
||||||
'QA::Resource::Repository::Commit',
|
'QA::Resource::Repository::Commit',
|
||||||
'QA::EE::Resource::GroupIteration',
|
'QA::EE::Resource::GroupIteration',
|
||||||
|
|
|
@ -562,7 +562,7 @@ RSpec.describe 'Admin updates settings' do
|
||||||
|
|
||||||
it 'change Prometheus settings' do
|
it 'change Prometheus settings' do
|
||||||
page.within('.as-prometheus') do
|
page.within('.as-prometheus') do
|
||||||
check 'Enable health and performance metrics endpoint'
|
check 'Enable GitLab Prometheus metrics endpoint'
|
||||||
click_button 'Save changes'
|
click_button 'Save changes'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
|
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
|
||||||
|
import { HIDE_COMMENTS } from '~/diffs/i18n';
|
||||||
import discussionsMockData from '../mock_data/diff_discussions';
|
import discussionsMockData from '../mock_data/diff_discussions';
|
||||||
|
|
||||||
const getDiscussionsMockData = () => [{ ...discussionsMockData }];
|
const getDiscussionsMockData = () => [{ ...discussionsMockData }];
|
||||||
|
@ -42,6 +43,11 @@ describe('DiffGutterAvatars', () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(wrapper.emitted().toggleLineDiscussions).toBeDefined();
|
expect(wrapper.emitted().toggleLineDiscussions).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the proper title and aria-label ', () => {
|
||||||
|
expect(findCollapseButton().attributes('title')).toBe(HIDE_COMMENTS);
|
||||||
|
expect(findCollapseButton().attributes('aria-label')).toBe(HIDE_COMMENTS);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when collapsed', () => {
|
describe('when collapsed', () => {
|
||||||
|
|
|
@ -238,6 +238,24 @@ describe('SidebarDropdownWidget', () => {
|
||||||
expect(findSelectedAttribute().text()).toBe('None');
|
expect(findSelectedAttribute().text()).toBe('None');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when user doesn't have permission to view current attribute", () => {
|
||||||
|
it('renders no permission text', () => {
|
||||||
|
createComponent({
|
||||||
|
data: {
|
||||||
|
hasCurrentAttribute: true,
|
||||||
|
currentAttribute: null,
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
currentAttribute: { loading: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findSelectedAttribute().text()).toBe(
|
||||||
|
`You don't have permission to view this ${wrapper.props('issuableAttribute')}.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when a user can edit', () => {
|
describe('when a user can edit', () => {
|
||||||
|
|
|
@ -297,6 +297,66 @@ RSpec.describe ApplicationSettingsHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.spam_check_endpoint_enabled?' do
|
||||||
|
subject { helper.spam_check_endpoint_enabled? }
|
||||||
|
|
||||||
|
context 'when spam check endpoint is enabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when spam check endpoint is disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.anti_spam_service_enabled?' do
|
||||||
|
subject { helper.anti_spam_service_enabled? }
|
||||||
|
|
||||||
|
context 'when akismet is enabled and spam check endpoint is disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: false)
|
||||||
|
stub_application_setting(akismet_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when akismet is disabled and spam check endpoint is enabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: true)
|
||||||
|
stub_application_setting(akismet_enabled: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when akismet and spam check endpoint are both enabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: true)
|
||||||
|
stub_application_setting(akismet_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when akismet and spam check endpoint are both disabled' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(spam_check_endpoint_enabled: false)
|
||||||
|
stub_application_setting(akismet_enabled: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#sidekiq_job_limiter_modes_for_select' do
|
describe '#sidekiq_job_limiter_modes_for_select' do
|
||||||
subject { helper.sidekiq_job_limiter_modes_for_select }
|
subject { helper.sidekiq_job_limiter_modes_for_select }
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe API::Users do
|
RSpec.describe API::Users do
|
||||||
|
include WorkhorseHelpers
|
||||||
|
|
||||||
let_it_be(:admin) { create(:admin) }
|
let_it_be(:admin) { create(:admin) }
|
||||||
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
|
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
|
||||||
let_it_be(:key) { create(:key, user: user) }
|
let_it_be(:key) { create(:key, user: user) }
|
||||||
|
@ -1180,6 +1182,22 @@ RSpec.describe API::Users do
|
||||||
expect(new_user.user_preference.view_diffs_file_by_file?).to eq(true)
|
expect(new_user.user_preference.view_diffs_file_by_file?).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "creates user with avatar" do
|
||||||
|
workhorse_form_with_file(
|
||||||
|
api('/users', admin),
|
||||||
|
method: :post,
|
||||||
|
file_key: :avatar,
|
||||||
|
params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
|
|
||||||
|
new_user = User.find_by(id: json_response['id'])
|
||||||
|
|
||||||
|
expect(new_user).not_to eq(nil)
|
||||||
|
expect(json_response['avatar_url']).to include(new_user.avatar_path)
|
||||||
|
end
|
||||||
|
|
||||||
it "does not create user with invalid email" do
|
it "does not create user with invalid email" do
|
||||||
post api('/users', admin),
|
post api('/users', admin),
|
||||||
params: {
|
params: {
|
||||||
|
@ -1478,7 +1496,12 @@ RSpec.describe API::Users do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates user with avatar' do
|
it 'updates user with avatar' do
|
||||||
put api("/users/#{user.id}", admin), params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
|
workhorse_form_with_file(
|
||||||
|
api("/users/#{user.id}", admin),
|
||||||
|
method: :put,
|
||||||
|
file_key: :avatar,
|
||||||
|
params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
|
||||||
|
)
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,6 @@ require 'spec_helper'
|
||||||
RSpec.describe JiraConnect::OauthCallbacksController do
|
RSpec.describe JiraConnect::OauthCallbacksController do
|
||||||
describe 'GET /-/jira_connect/oauth_callbacks' do
|
describe 'GET /-/jira_connect/oauth_callbacks' do
|
||||||
context 'when logged in' do
|
context 'when logged in' do
|
||||||
let_it_be(:user) { create(:user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
sign_in(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a page prompting the user to close the window' do
|
it 'renders a page prompting the user to close the window' do
|
||||||
get '/-/jira_connect/oauth_callbacks'
|
get '/-/jira_connect/oauth_callbacks'
|
||||||
|
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func scanDeepen(body io.Reader) bool {
|
|
||||||
scanner := bufio.NewScanner(body)
|
|
||||||
scanner.Split(pktLineSplitter)
|
|
||||||
for scanner.Scan() {
|
|
||||||
if bytes.HasPrefix(scanner.Bytes(), []byte("deepen")) && scanner.Err() == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func pktLineSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
||||||
if len(data) < 4 {
|
|
||||||
if atEOF && len(data) > 0 {
|
|
||||||
return 0, nil, fmt.Errorf("pktLineSplitter: incomplete length prefix on %q", data)
|
|
||||||
}
|
|
||||||
return 0, nil, nil // want more data
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.HasPrefix(data, []byte("0000")) {
|
|
||||||
// special case: "0000" terminator packet: return empty token
|
|
||||||
return 4, data[:0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have at least 4 bytes available so we can decode the 4-hex digit
|
|
||||||
// length prefix of the packet line.
|
|
||||||
pktLength64, err := strconv.ParseInt(string(data[:4]), 16, 0)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("pktLineSplitter: decode length: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast is safe because we requested an int-size number from strconv.ParseInt
|
|
||||||
pktLength := int(pktLength64)
|
|
||||||
|
|
||||||
if pktLength < 0 {
|
|
||||||
return 0, nil, fmt.Errorf("pktLineSplitter: invalid length: %d", pktLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < pktLength {
|
|
||||||
if atEOF {
|
|
||||||
return 0, nil, fmt.Errorf("pktLineSplitter: less than %d bytes in input %q", pktLength, data)
|
|
||||||
}
|
|
||||||
return 0, nil, nil // want more data
|
|
||||||
}
|
|
||||||
|
|
||||||
// return "pkt" token without length prefix
|
|
||||||
return pktLength, data[4:pktLength], nil
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSuccessfulScanDeepen(t *testing.T) {
|
|
||||||
examples := []struct {
|
|
||||||
input string
|
|
||||||
output bool
|
|
||||||
}{
|
|
||||||
{"000dsomething000cdeepen 10000", true},
|
|
||||||
{"000dsomething0000000cdeepen 1", true},
|
|
||||||
{"000dsomething0000", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, example := range examples {
|
|
||||||
hasDeepen := scanDeepen(bytes.NewReader([]byte(example.input)))
|
|
||||||
|
|
||||||
if hasDeepen != example.output {
|
|
||||||
t.Fatalf("scanDeepen %q: expected %v, got %v", example.input, example.output, hasDeepen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFailedScanDeepen(t *testing.T) {
|
|
||||||
examples := []string{
|
|
||||||
"invalid data",
|
|
||||||
"deepen",
|
|
||||||
"000cdeepen",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, example := range examples {
|
|
||||||
if scanDeepen(bytes.NewReader([]byte(example))) {
|
|
||||||
t.Fatalf("scanDeepen %q: expected result to be false, got true", example)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -333,6 +333,10 @@ func configureRoutes(u *upstream) {
|
||||||
u.route("POST", apiPattern+`v4/groups\z`, tempfileMultipartProxy),
|
u.route("POST", apiPattern+`v4/groups\z`, tempfileMultipartProxy),
|
||||||
u.route("PUT", apiPattern+`v4/groups/[^/]+\z`, tempfileMultipartProxy),
|
u.route("PUT", apiPattern+`v4/groups/[^/]+\z`, tempfileMultipartProxy),
|
||||||
|
|
||||||
|
// User Avatar
|
||||||
|
u.route("POST", apiPattern+`v4/users\z`, tempfileMultipartProxy),
|
||||||
|
u.route("PUT", apiPattern+`v4/users/[0-9]+\z`, tempfileMultipartProxy),
|
||||||
|
|
||||||
// Explicitly proxy API requests
|
// Explicitly proxy API requests
|
||||||
u.route("", apiPattern, proxy),
|
u.route("", apiPattern, proxy),
|
||||||
u.route("", ciAPIPattern, proxy),
|
u.route("", ciAPIPattern, proxy),
|
||||||
|
|
|
@ -138,6 +138,8 @@ func TestAcceleratedUpload(t *testing.T) {
|
||||||
{"POST", `/api/v4/groups`, false},
|
{"POST", `/api/v4/groups`, false},
|
||||||
{"PUT", `/api/v4/groups/5`, false},
|
{"PUT", `/api/v4/groups/5`, false},
|
||||||
{"PUT", `/api/v4/groups/group%2Fsubgroup`, false},
|
{"PUT", `/api/v4/groups/group%2Fsubgroup`, false},
|
||||||
|
{"POST", `/api/v4/users`, false},
|
||||||
|
{"PUT", `/api/v4/users/42`, false},
|
||||||
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
|
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
|
||||||
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
|
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
|
||||||
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
|
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
|
||||||
|
|
Loading…
Reference in New Issue