Commit 8eae26a8 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents ddfb1b0f c5659029
import initSetHelperText from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
export default () => {
......@@ -5,3 +6,5 @@ export default () => {
new PayloadPreviewer(trigger).init();
});
};
initSetHelperText();
......@@ -42,6 +42,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
import { fileByFile } from '../utils/preferences';
......@@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import MergeConflictWarning from './merge_conflict_warning.vue';
import NoChanges from './no_changes.vue';
import PreRenderer from './pre_renderer.vue';
import TreeList from './tree_list.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
export default {
name: 'DiffsApp',
......@@ -71,6 +74,8 @@ export default {
GlSprintf,
DynamicScroller,
DynamicScrollerItem,
PreRenderer,
VirtualScrollerScrollSync,
},
alerts: {
ALERT_OVERFLOW_HIDDEN,
......@@ -166,6 +171,7 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
};
},
computed: {
......@@ -323,6 +329,11 @@ export default {
this.setHighlightedRow(id.split('diff-content').pop().slice(1));
}
if (window.gon?.features?.diffsVirtualScrolling) {
diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
}
if (window.gon?.features?.diffSettingsUsageData) {
if (this.renderTreeList) {
api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
......@@ -377,6 +388,11 @@ export default {
diffsApp.deinstrument();
this.unsubscribeFromEvents();
this.removeEventListeners();
if (window.gon?.features?.diffsVirtualScrolling) {
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
}
},
methods: {
...mapActions(['startTaskList']),
......@@ -508,6 +524,20 @@ export default {
return this.setShowTreeList({ showTreeList, saving: false });
},
async scrollVirtualScrollerToFileHash(hash) {
const index = this.diffFiles.findIndex((f) => f.file_hash === hash);
if (index !== -1) {
this.scrollVirtualScrollerToIndex(index);
}
},
async scrollVirtualScrollerToIndex(index) {
this.virtualScrollCurrentIndex = index;
await this.$nextTick();
this.virtualScrollCurrentIndex = -1;
},
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
......@@ -572,6 +602,7 @@ export default {
<template v-else-if="renderDiffFiles">
<dynamic-scroller
v-if="isVirtualScrollingEnabled"
ref="virtualScroller"
:items="diffs"
:min-item-size="70"
:buffer="1000"
......@@ -579,7 +610,7 @@ export default {
page-mode
>
<template #default="{ item, index, active }">
<dynamic-scroller-item :item="item" :active="active">
<dynamic-scroller-item :item="item" :active="active" :class="{ active }">
<diff-file
:file="item"
:reviewed="fileReviews[item.id]"
......@@ -588,9 +619,29 @@ export default {
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
:active="active"
/>
</dynamic-scroller-item>
</template>
<template #before>
<pre-renderer :max-length="diffFilesLength">
<template #default="{ item, index, active }">
<dynamic-scroller-item :item="item" :active="active">
<diff-file
:file="item"
:reviewed="fileReviews[item.id]"
:is-first-file="index === 0"
:is-last-file="index === diffFilesLength - 1"
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
pre-render
/>
</dynamic-scroller-item>
</template>
</pre-renderer>
<virtual-scroller-scroll-sync :index="virtualScrollCurrentIndex" />
</template>
</dynamic-scroller>
<template v-else>
<diff-file
......
......@@ -68,6 +68,16 @@ export default {
type: Boolean,
required: true,
},
active: {
type: Boolean,
required: false,
default: true,
},
preRender: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -156,6 +166,8 @@ export default {
watch: {
'file.id': {
handler: function fileIdHandler() {
if (this.preRender) return;
this.manageViewedEffects();
},
},
......@@ -163,7 +175,7 @@ export default {
handler: function hashChangeWatch(newHash, oldHash) {
this.isCollapsed = isCollapsed(this.file);
if (newHash && oldHash && !this.hasDiff) {
if (newHash && oldHash && !this.hasDiff && !this.preRender) {
this.requestDiff();
}
},
......@@ -187,10 +199,14 @@ export default {
},
},
created() {
if (this.preRender) return;
notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener);
},
mounted() {
if (this.preRender) return;
if (this.hasDiff) {
this.postRender();
}
......@@ -198,6 +214,8 @@ export default {
this.manageViewedEffects();
},
beforeDestroy() {
if (this.preRender) return;
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
},
methods: {
......@@ -287,7 +305,7 @@ export default {
<template>
<div
:id="file.file_hash"
:id="!preRender && active && file.file_hash"
:class="{
'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink),
......@@ -330,7 +348,7 @@ export default {
</div>
<template v-else>
<div
:id="`diff-content-${file.file_hash}`"
:id="!preRender && active && `diff-content-${file.file_hash}`"
:class="hasBodyClasses.contentByHash"
data-testid="content-area"
>
......
<script>
export default {
inject: ['vscrollParent'],
props: {
maxLength: {
type: Number,
required: true,
},
},
data() {
return {
nextIndex: -1,
nextItem: null,
startedRender: false,
width: 0,
};
},
mounted() {
this.width = this.$el.parentNode.offsetWidth;
this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
await this.$nextTick();
const nextItem = this.findNextToRender();
if (nextItem) {
this.startedRender = true;
requestIdleCallback(() => {
this.nextItem = nextItem;
});
} else if (this.startedRender) {
this.clearRendering();
}
});
},
beforeDestroy() {
this.$_itemsWithSizeWatcher();
},
methods: {
clearRendering() {
this.nextItem = null;
if (this.maxLength === this.vscrollParent.itemsWithSize.length) {
this.$_itemsWithSizeWatcher();
}
},
findNextToRender() {
return this.vscrollParent.itemsWithSize.find(({ size }, index) => {
const isNext = size === 0;
if (isNext) {
this.nextIndex = index;
}
return isNext;
});
},
},
};
</script>
<template>
<div v-if="nextItem" :style="{ width: `${width}px` }" class="gl-absolute diff-file-offscreen">
<slot
v-bind="{ item: nextItem.item, index: nextIndex, active: true, itemWithSize: nextItem }"
></slot>
</div>
</template>
<style scoped>
.diff-file-offscreen {
top: -200%;
left: -200%;
}
</style>
import { handleLocationHash } from '~/lib/utils/common_utils';
export default {
inject: ['vscrollParent'],
props: {
index: {
type: Number,
required: true,
},
},
watch: {
index: {
handler() {
const { index } = this;
if (index < 0) return;
if (this.vscrollParent.itemsWithSize[index].size) {
this.scrollToIndex(index);
} else {
this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
await this.$nextTick();
if (this.vscrollParent.itemsWithSize[index].size) {
this.$_itemsWithSizeWatcher();
this.scrollToIndex(index);
await this.$nextTick();
}
});
}
},
immediate: true,
},
},
beforeDestroy() {
if (this.$_itemsWithSizeWatcher) this.$_itemsWithSizeWatcher();
},
methods: {
scrollToIndex(index) {
this.vscrollParent.scrollToItem(index);
setTimeout(() => {
handleLocationHash();
});
},
},
render(h) {
return h(null);
},
};
......@@ -100,7 +100,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
w: state.showWhitespace ? '0' : '1',
view: 'inline',
};
const hash = window.location.hash.replace('#', '').split('diff-content-').pop();
let totalLoaded = 0;
let scrolledVirtualScroller = false;
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
......@@ -115,6 +117,15 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
commit(types.SET_BATCH_LOADING, false);
if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) {
const index = state.diffFiles.findIndex((f) => f.file_hash === hash);
if (index >= 0) {
eventHub.$emit('scrollToIndex', index);
scrolledVirtualScroller = true;
}
}
if (!isNoteLink && !state.currentDiffFileId) {
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
}
......@@ -171,7 +182,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
.catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
return getBatch()
.then(handleLocationHash)
.then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash())
.catch(() => null);
};
......@@ -510,9 +521,18 @@ export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
commit(types.VIEW_DIFF_FILE, fileHash);
if (window.gon?.features?.diffsVirtualScrolling) {
eventHub.$emit('scrollToFileHash', fileHash);
setTimeout(() => {
window.history.replaceState(null, null, `#${fileHash}`);
});
} else {
document.location.hash = fileHash;
}
};
export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => {
......
......@@ -381,9 +381,15 @@ function prepareDiffFileLines(file) {
}
function finalizeDiffFile(file, index) {
let renderIt = Boolean(window.gon?.features?.diffsVirtualScrolling);
if (!window.gon?.features?.diffsVirtualScrolling) {
renderIt =
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false;
}
Object.assign(file, {
renderIt:
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false,
renderIt,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
......
import { __ } from '~/locale';
export const HELPER_TEXT_USAGE_PING_DISABLED = __(
'To enable Registration Features, make sure "Enable service ping" is checked.',
);
export const HELPER_TEXT_USAGE_PING_ENABLED = __(
'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.',
);
function setHelperText(usagePingCheckbox) {
const helperTextId = document.getElementById('usage_ping_features_helper_text');
const usagePingFeaturesLabel = document.getElementById('usage_ping_features_label');
const usagePingFeaturesCheckbox = document.getElementById(
'application_setting_usage_ping_features_enabled',
);
helperTextId.textContent = usagePingCheckbox.checked
? HELPER_TEXT_USAGE_PING_ENABLED
: HELPER_TEXT_USAGE_PING_DISABLED;
usagePingFeaturesLabel.classList.toggle('gl-cursor-not-allowed', !usagePingCheckbox.checked);
usagePingFeaturesCheckbox.disabled = !usagePingCheckbox.checked;
if (!usagePingCheckbox.checked) {
usagePingFeaturesCheckbox.disabled = true;
usagePingFeaturesCheckbox.checked = false;
}
}
export default function initSetHelperText() {
const usagePingCheckbox = document.getElementById('application_setting_usage_ping_enabled');
setHelperText(usagePingCheckbox);
usagePingCheckbox.addEventListener('change', () => {
setHelperText(usagePingCheckbox);
});
}
......@@ -1188,3 +1188,9 @@ table.code {
margin-top: 0;
}
}
// Note: Prevents tall files from appearing above sticky tabs
.diffs .vue-recycle-scroller__item-view > div:not(.active) {
position: absolute;
top: 100vh;
}
......@@ -331,6 +331,7 @@ module ApplicationSettingsHelper
:unique_ips_limit_per_user,
:unique_ips_limit_time_window,
:usage_ping_enabled,
:usage_ping_features_enabled,
:user_default_external,
:user_show_add_ssh_key_message,
:user_default_internal_regex,
......
......@@ -377,6 +377,10 @@ module ApplicationSettingImplementation
Settings.gitlab.usage_ping_enabled
end
def usage_ping_features_enabled?
usage_ping_enabled? && usage_ping_features_enabled
end
def usage_ping_enabled
usage_ping_can_be_configured? && super
end
......
......@@ -230,7 +230,7 @@ module CascadingNamespaceSettingAttribute
def namespace_ancestor_ids
strong_memoize(:namespace_ancestor_ids) do
namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id }
namespace.ancestor_ids(hierarchy_order: :asc)
end
end
......
......@@ -64,6 +64,13 @@ module Namespaces
traversal_ids.present?
end
def use_traversal_ids_for_ancestors?
return false unless use_traversal_ids?
return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
traversal_ids.present?
end
def root_ancestor
return super if parent.nil?
return super unless persisted?
......@@ -95,14 +102,33 @@ module Namespaces
end
def ancestors(hierarchy_order: nil)
return super() unless use_traversal_ids?
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
return super unless use_traversal_ids_for_ancestors?
return self.class.none if parent_id.blank?
lineage(bottom: parent, hierarchy_order: hierarchy_order)
end
def ancestor_ids(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
hierarchy_order == :desc ? traversal_ids[0..-2] : traversal_ids[0..-2].reverse
end
def self_and_ancestors(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
return self.class.where(id: id) if parent_id.blank?
lineage(bottom: self, hierarchy_order: hierarchy_order)
end
def self_and_ancestor_ids(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
hierarchy_order == :desc ? traversal_ids : traversal_ids.reverse
end
private
# Update the traversal_ids for the full hierarchy.
......
......@@ -10,7 +10,7 @@ module Namespaces
if persisted?
strong_memoize(:root_ancestor) do
self_and_ancestors.reorder(nil).find_by(parent_id: nil)
recursive_self_and_ancestors.reorder(nil).find_by(parent_id: nil)
end
else
parent.root_ancestor
......@@ -26,14 +26,19 @@ module Namespaces
alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
# Returns all the ancestors of the current namespaces.
def ancestors
def ancestors(hierarchy_order: nil)
return self.class.none unless parent_id
object_hierarchy(self.class.where(id: parent_id))
.base_and_ancestors
.base_and_ancestors(hierarchy_order: hierarchy_order)
end
alias_method :recursive_ancestors, :ancestors
def ancestor_ids(hierarchy_order: nil)
recursive_ancestors(hierarchy_order: hierarchy_order).pluck(:id)
end
alias_method :recursive_ancestor_ids, :ancestor_ids
# returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
......@@ -49,6 +54,11 @@ module Namespaces
end
alias_method :recursive_self_and_ancestors, :self_and_ancestors
def self_and_ancestor_ids(hierarchy_order: nil)
recursive_self_and_ancestors(hierarchy_order: hierarchy_order).pluck(:id)
end
alias_method :recursive_self_and_ancestor_ids, :self_and_ancestor_ids
# Returns all the descendants of the current namespace.
def descendants
object_hierarchy(self.class.where(parent_id: id))
......@@ -63,7 +73,7 @@ module Namespaces
alias_method :recursive_self_and_descendants, :self_and_descendants
def self_and_descendant_ids
self_and_descendants.select(:id)
recursive_self_and_descendants.select(:id)
end
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
......
......@@ -35,5 +35,26 @@
- deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
.form-group
- usage_ping_enabled = @application_setting.usage_ping_enabled?
.form-check
= f.check_box :usage_ping_features_enabled?, disabled: !usage_ping_enabled, class: 'form-check-input'
= f.label :usage_ping_features_enabled?, class: 'form-check-label gl-cursor-not-allowed', id: 'usage_ping_features_label' do
= _('Enable Registration Features')
= link_to sprite_icon('question-o'), help_page_path('development/usage_ping/index.md', anchor: 'registration-features-program')
.form-text.text-muted
- if usage_ping_enabled
%p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.')
- else
%p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('To enable Registration Features, make sure "Enable service ping" is checked.')
%p.gl-mb-3.text-muted= _('Registration Features include:')
.form-text
- email_from_gitlab_path = help_page_path('tools/email.md')
- link_end = '</a>'.html_safe
- email_from_gitlab_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: email_from_gitlab_path }
%ul
%li
= _('Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}.').html_safe % { link_start: email_from_gitlab_link, link_end: link_end }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
# frozen_string_literal: true
class AddUsagePingFeaturesEnabledToApplicationSettings < ActiveRecord::Migration[6.1]
def up
add_column :application_settings, :usage_ping_features_enabled, :boolean, default: false, null: false
end
def down
remove_column :application_settings, :usage_ping_features_enabled
end
end
1a0df6210d9ee0e0229f3cdf3e95acaaa47ebf4ca31ac0fd9f57255115355f99
\ No newline at end of file
......@@ -9525,6 +9525,7 @@ CREATE TABLE application_settings (
encrypted_mailgun_signing_key bytea,
encrypted_mailgun_signing_key_iv bytea,
mailgun_events_enabled boolean DEFAULT false NOT NULL,
usage_ping_features_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
......@@ -282,7 +282,7 @@ class License < ApplicationRecord
end
def features_with_usage_ping
return FEATURES_WITH_USAGE_PING if Gitlab::CurrentSettings.usage_ping_enabled?
return FEATURES_WITH_USAGE_PING if Gitlab::CurrentSettings.usage_ping_features_enabled?
[]
end
......
......@@ -20,7 +20,11 @@ module RequirementsManagement
belongs_to :author, inverse_of: :requirements, class_name: 'User'
belongs_to :project, inverse_of: :requirements
belongs_to :requirement_issue, class_name: 'Issue', foreign_key: :issue_id
# deleting an issue would result in deleting requirement record due to cascade delete via foreign key
# but to sync the other way around, we require a temporary `dependent: :destroy`
# See https://gitlab.com/gitlab-org/gitlab/-/issues/323779 for details.
# This will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/329432
belongs_to :requirement_issue, class_name: 'Issue', foreign_key: :issue_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :issue_id, uniqueness: true, allow_nil: true
......
......@@ -31,7 +31,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
stub_application_setting(usage_ping_enabled: false)
end
it 'returns 404' do
......@@ -44,10 +44,20 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
stub_application_setting(usage_ping_enabled: true)
end
it 'responds with 200' do
it 'responds 404 when feature is not activated' do
stub_application_setting(usage_ping_features_enabled: false)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
it 'responds with 200 when feature is activated' do
stub_application_setting(usage_ping_features_enabled: true)
subject
expect(response).to have_gitlab_http_status(:ok)
......@@ -136,7 +146,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
stub_application_setting(usage_ping_enabled: false)
end
it 'does not trigger the service to send emails' do
......@@ -155,23 +165,47 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
stub_application_setting(usage_ping_enabled: true)
end
it 'triggers the service to send emails' do
expect_next_instance_of(Admin::EmailService, recipients, email_subject, body) do |email_service|
expect(email_service).to receive(:execute)
context 'when feature is activated' do
before do
stub_application_setting(usage_ping_features_enabled: true)
end
subject
it 'triggers the service to send emails' do
expect_next_instance_of(Admin::EmailService, recipients, email_subject, body) do |email_service|
expect(email_service).to receive(:execute)
end
subject
end
it 'redirects to `admin_email_path` with success notice' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(admin_email_path)
expect(flash[:notice]).to eq('Email sent')
end
end
it 'redirects to `admin_email_path` with success notice' do
subject
context 'when feature is deactivated' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(admin_email_path)
expect(flash[:notice]).to eq('Email sent')
it 'does not trigger the service to send emails' do
expect(Admin::EmailService).not_to receive(:new)
subject
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......
......@@ -14,7 +14,7 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is not licensed' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
stub_application_setting(usage_ping_enabled: false)
end
it 'returns 404' do
......@@ -27,13 +27,31 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
stub_application_setting(usage_ping_enabled: true)
end
it 'returns 200' do
visit admin_email_path
context 'when feature is activated' do
before do
stub_application_setting(usage_ping_features_enabled: true)
end
it 'returns 200' do
visit admin_email_path
expect(page.status_code).to eq(200)
end
end
context 'when feature is deactivated' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
expect(page.status_code).to eq(200)
it 'returns 404' do
visit admin_email_path
expect(page.status_code).to eq(404)
end
end
end
......
......@@ -28,7 +28,7 @@ RSpec.describe "Admin::Users", :js do
context 'when `send_emails_from_admin_area` feature is disabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
stub_application_setting(usage_ping_enabled: false)
end
it "does not show the 'Send email to users' link" do
......@@ -41,13 +41,31 @@ RSpec.describe "Admin::Users", :js do
context 'when usage ping is enabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
stub_application_setting(usage_ping_enabled: true)
end
it "shows the 'Send email to users' link" do
visit admin_users_path
context 'when feature is activated' do
before do
stub_application_setting(usage_ping_features_enabled: true)
end
expect(page).to have_link(href: admin_email_path)
it "shows the 'Send email to users' link" do
visit admin_users_path
expect(page).to have_link(href: admin_email_path)
end
end
context 'when feature is disabled' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
it "does not show the 'Send email to users' link" do
visit admin_users_path
expect(page).not_to have_link(href: admin_email_path)
end
end
end
end
......
......@@ -22,7 +22,7 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
stub_application_setting(usage_ping_enabled: false)
end
it { is_expected.to be_falsey }
......@@ -31,11 +31,27 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do
before do
stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
stub_application_setting(usage_ping_enabled: true)
end
it 'returns true' do
expect(subject).to eq(true)
context 'when feature is activated' do
before do
stub_application_setting(usage_ping_features_enabled: true)
end
it 'returns true' do
expect(subject).to eq(true)
end
end
context 'when feature is deactivated' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
it 'returns false' do
expect(subject).to eq(false)
end
end
end
end
......
......@@ -184,4 +184,21 @@ RSpec.describe RequirementsManagement::Requirement do
end
end
end
describe 'sync with requirement issues' do
let_it_be_with_reload(:requirement) { create(:requirement) }
let_it_be_with_reload(:requirement_issue) { create(:requirement_issue, requirement: requirement) }
context 'when destroying a requirement' do
it 'also destroys the associated requirement issue' do
expect { requirement.destroy! }.to change { Issue.where(issue_type: 'requirement').count }.by(-1)
end
end
context 'when destroying a requirement issue' do
it 'also destroys the associated requirement' do
expect { requirement_issue.destroy! }.to change { RequirementsManagement::Requirement.count }.by(-1)
end
end
end
end
......@@ -11809,6 +11809,9 @@ msgstr ""
msgid "Email display name"
msgstr ""
msgid "Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}."
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce."
msgstr ""
......@@ -11950,6 +11953,9 @@ msgstr ""
msgid "Enable Pseudonymizer data collection"
msgstr ""
msgid "Enable Registration Features"
msgstr ""
msgid "Enable Repository Checks"
msgstr ""
......@@ -26848,6 +26854,9 @@ msgstr ""
msgid "Register with two-factor app"
msgstr ""
msgid "Registration Features include:"
msgstr ""
msgid "Registration|Checkout"
msgstr ""
......@@ -33913,6 +33922,9 @@ msgstr ""
msgid "To define internal users, first enable new users set to external"
msgstr ""
msgid "To enable Registration Features, make sure \"Enable service ping\" is checked."
msgstr ""
msgid "To ensure no loss of personal content, this account should only be used for matters related to %{group_name}."
msgstr ""
......@@ -37086,6 +37098,9 @@ msgstr ""
msgid "You can easily contribute to them by requesting to join these groups."
msgstr ""
msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service."
msgstr ""
msgid "You can enable project access token creation in %{link_start}group settings%{link_end}."
msgstr ""
......
......@@ -34,4 +34,12 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
expect(response).to be_successful
end
it 'application_settings/usage.html' do
stub_application_setting(usage_ping_enabled: false)
get :metrics_and_profiling
expect(response).to be_successful
end
end
import initSetHelperText, {
HELPER_TEXT_USAGE_PING_DISABLED,
HELPER_TEXT_USAGE_PING_ENABLED,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
describe('UsageStatistics', () => {
const FIXTURE = 'application_settings/usage.html';
let usagePingCheckBox;
let usagePingFeaturesCheckBox;
let usagePingFeaturesLabel;
let usagePingFeaturesHelperText;
beforeEach(() => {
loadFixtures(FIXTURE);
initSetHelperText();
usagePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
usagePingFeaturesCheckBox = document.getElementById(
'application_setting_usage_ping_features_enabled',
);
usagePingFeaturesLabel = document.getElementById('usage_ping_features_label');
usagePingFeaturesHelperText = document.getElementById('usage_ping_features_helper_text');
});
const expectEnabledUsagePingFeaturesCheckBox = () => {
expect(usagePingFeaturesCheckBox.classList.contains('gl-cursor-not-allowed')).toBe(false);
expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_USAGE_PING_ENABLED);
};
const expectDisabledUsagePingFeaturesCheckBox = () => {
expect(usagePingFeaturesLabel.classList.contains('gl-cursor-not-allowed')).toBe(true);
expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_USAGE_PING_DISABLED);
};
describe('Registration Features checkbox', () => {
it('is disabled when Usage Ping checkbox is unchecked', () => {
expect(usagePingCheckBox.checked).toBe(false);
expectDisabledUsagePingFeaturesCheckBox();
});
it('is enabled when Usage Ping checkbox is checked', () => {
usagePingCheckBox.click();
expect(usagePingCheckBox.checked).toBe(true);
expectEnabledUsagePingFeaturesCheckBox();
});
it('is switched to disabled when Usage Ping checkbox is unchecked ', () => {
usagePingCheckBox.click();
usagePingFeaturesCheckBox.click();
expectEnabledUsagePingFeaturesCheckBox();
usagePingCheckBox.click();
expect(usagePingCheckBox.checked).toBe(false);
expect(usagePingFeaturesCheckBox.checked).toBe(false);
expectDisabledUsagePingFeaturesCheckBox();
});
});
});
......@@ -996,6 +996,36 @@ RSpec.describe Namespace do
end
end
describe '#use_traversal_ids_for_ancestors?' do
let_it_be(:namespace, reload: true) { create(:namespace) }
subject { namespace.use_traversal_ids_for_ancestors? }
context 'when use_traversal_ids_for_ancestors? feature flag is true' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: true)
end
it { is_expected.to eq true }
end
context 'when use_traversal_ids_for_ancestors? feature flag is false' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: false)
end
it { is_expected.to eq false }
end
context 'when use_traversal_ids? feature flag is false' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it { is_expected.to eq false }
end
end
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
......
......@@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do
click_save_button
end
it 'disables setting in subgroups' do
visit subgroup_path
shared_examples 'subgroup settings are disabled' do
it 'disables setting in subgroups' do
visit subgroup_path
expect(find("#{setting_field_selector}[disabled]")).to be_checked
end
end
include_examples 'subgroup settings are disabled'
context 'when use_traversal_ids_for_ancestors is disabled' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: false)
end
expect(find("#{setting_field_selector}[disabled]")).to be_checked
include_examples 'subgroup settings are disabled'
end
it 'does not show enforcement checkbox in subgroups' do
......
......@@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do
it "makes a recursive query" do
groups.each do |group|
expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/)
expect { group.public_send(recursive_method).try(:load) }.to make_queries_matching(/WITH RECURSIVE/)
end
end
end
describe '#root_ancestor' do
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
describe '#root_ancestor' do
it 'returns the correct root ancestor' do
expect(group.root_ancestor).to eq(group)
expect(nested_group.root_ancestor).to eq(group)
......@@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_root_ancestor' do
let(:groups) { [group, nested_group, deep_nested_group] }
it "is equivalent to #recursive_root_ancestor" do
groups.each do |group|
expect(group.root_ancestor).to eq(group.recursive_root_ancestor)
......@@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#self_and_hierarchy' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
let!(:another_group) { create(:group) }
let!(:another_group_nested) { create(:group, parent: another_group) }
it 'returns the correct tree' do
expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
......@@ -54,18 +50,11 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_self_and_hierarchy' do
let(:groups) { [group, nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_hierarchy
end
end
describe '#ancestors' do
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct ancestors' do
# #reload is called to make sure traversal_ids are reloaded
expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group)
......@@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_ancestors' do
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :ancestors
end
end
describe '#self_and_ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
describe '#ancestor_ids' do
it 'returns the correct ancestor ids' do
expect(very_deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id)
expect(nested_group.ancestor_ids).to contain_exactly(group.id)
expect(group.ancestor_ids).to be_empty
end
describe '#recursive_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :ancestor_ids
end
end
describe '#self_and_ancestors' do
it 'returns the correct ancestors' do
expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
......@@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_self_and_ancestors' do
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_ancestors
end
end
describe '#self_and_ancestor_ids' do
it 'returns the correct ancestor ids' do
expect(very_deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id)
expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id)
expect(group.self_and_ancestor_ids).to contain_exactly(group.id)
end
describe '#recursive_self_and_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_ancestor_ids
end
end
describe '#descendants' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
let!(:another_group) { create(:group) }
let!(:another_group_nested) { create(:group, parent: another_group) }
it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([])
......@@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_descendants' do
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :descendants
end
end
describe '#self_and_descendants' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
let!(:another_group) { create(:group) }
let!(:another_group_nested) { create(:group, parent: another_group) }
it 'returns the correct descendants' do
expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
......@@ -139,24 +143,18 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#recursive_self_and_descendants' do
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
let_it_be(:groups) { [group, nested_group, deep_nested_group] }
it_behaves_like 'recursive version', :self_and_descendants
end
end
describe '#self_and_descendant_ids' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
subject { group.self_and_descendant_ids.pluck(:id) }
it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id) }
it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) }
describe '#recursive_self_and_descendant_ids' do
let(:groups) { [group, nested_group, deep_nested_group] }
it_behaves_like 'recursive version', :self_and_descendant_ids
end
end
......
......@@ -195,14 +195,15 @@ export default {
observeSize () {
if (!this.vscrollResizeObserver) return
this.vscrollResizeObserver.observe(this.$el.parentNode)
this.$el.parentNode.addEventListener('resize', this.onResize)
this.$_parentNode = this.$el.parentNode;
this.vscrollResizeObserver.observe(this.$_parentNode)
this.$_parentNode.addEventListener('resize', this.onResize)
},
unobserveSize () {
if (!this.vscrollResizeObserver) return
this.vscrollResizeObserver.unobserve(this.$el.parentNode)
this.$el.parentNode.removeEventListener('resize', this.onResize)
this.vscrollResizeObserver.unobserve(this.$_parentNode)
this.$_parentNode.removeEventListener('resize', this.onResize)
},
onResize (event) {
......
......@@ -572,20 +572,49 @@ export default {
},
scrollToItem (index) {
let scroll
if (this.itemSize === null) {
scroll = index > 0 ? this.sizes[index - 1].accumulator : 0
} else {
scroll = index * this.itemSize
}
this.scrollToPosition(scroll)
this.$_scrollDirty = true
const { viewport, scrollDirection, scrollDistance } = this.scrollToPosition(index)
viewport[scrollDirection] = scrollDistance
setTimeout(() => {
this.$_scrollDirty = false
this.updateVisibleItems(false, true)
})
},
scrollToPosition (position) {
if (this.direction === 'vertical') {
this.$el.scrollTop = position
} else {
this.$el.scrollLeft = position
scrollToPosition (index) {
const getPositionOfItem = (index) => {
if (this.itemSize === null) {
return index > 0 ? this.sizes[index - 1].accumulator : 0
} else {
return index * this.itemSize
}
}
const position = getPositionOfItem(index)
const direction = this.direction === 'vertical'
? { scroll: 'scrollTop', start: 'top' }
: { scroll: 'scrollLeft', start: 'left' }
if (this.pageMode) {
const viewportEl = ScrollParent(this.$el)
// HTML doesn't overflow like other elements
const scrollTop = viewportEl.tagName === 'HTML' ? 0 : viewportEl[direction.scroll]
const viewport = viewportEl.getBoundingClientRect()
const scroller = this.$el.getBoundingClientRect()
const scrollerPosition = scroller[direction.start] - viewport[direction.start]
return {
viewport: viewportEl,
scrollDirection: direction.scroll,
scrollDistance: position + scrollTop + scrollerPosition,
}
}
return {
viewport: this.$el,
scrollDirection: direction.scroll,
scrollDistance: position,
}
},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment