Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-21 15:09:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-21 15:09:17 +0300
commit0c4570435d417b69efd433057f95f01810618837 (patch)
tree4e402832206b83da2d73671977c1e5f7cae9074a /app
parent49abdb108a4d3c3f2ef9b27c7c4dcde43da1016a (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue91
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue55
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js7
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js14
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue27
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue42
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/sidebars_helper.rb10
-rw-r--r--app/models/audit_event.rb34
-rw-r--r--app/models/group.rb23
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/plan_limits.rb22
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/user.rb4
-rw-r--r--app/models/user_custom_attribute.rb1
-rw-r--r--app/serializers/diff_viewer_entity.rb2
-rw-r--r--app/services/admin/plan_limits/update_service.rb14
-rw-r--r--app/services/git/base_hooks_service.rb19
-rw-r--r--app/services/groups/participants_service.rb6
-rw-r--r--app/services/spam/spam_verdict_service.rb2
-rw-r--r--app/services/users/allow_possible_spam_service.rb18
-rw-r--r--app/services/users/disallow_possible_spam_service.rb13
-rw-r--r--app/views/groups/group_members/index.html.haml8
-rw-r--r--app/views/layouts/_page.html.haml2
27 files changed, 305 insertions, 141 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 0151dbb0bf7..bd8a7257d0c 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -6,9 +6,7 @@ import {
GlBadge,
GlAlert,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDisclosureDropdown,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_WARNING } from '~/alert';
@@ -38,9 +36,7 @@ export default {
GlBadge,
GlAlert,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDisclosureDropdown,
TimeAgoTooltip,
ErrorDetailsInfo,
TimelineChart,
@@ -167,6 +163,52 @@ export default {
showEmptyStacktraceAlert() {
return !this.loadingStacktrace && !this.showStacktrace && this.isStacktraceEmptyAlertVisible;
},
+ updateDropdownItems() {
+ return [
+ {
+ text: this.ignoreBtnLabel,
+ action: this.onIgnoreStatusUpdate,
+ extraAttrs: {
+ 'data-qa-selector': 'update_ignore_status_button',
+ },
+ },
+ {
+ text: this.resolveBtnLabel,
+ action: this.onResolveStatusUpdate,
+ extraAttrs: {
+ 'data-qa-selector': 'update_resolve_status_button',
+ },
+ },
+ ];
+ },
+ viewIssueDropdownItem() {
+ return {
+ text: __('View issue'),
+ href: this.error.gitlabIssuePath,
+ extraAttrs: {
+ 'data-qa-selector': 'view_issue_button',
+ },
+ };
+ },
+ createIssueDropdownItem() {
+ return {
+ text: __('Create issue'),
+ action: this.createIssue,
+ extraAttrs: {
+ 'data-qa-selector': 'create_issue_button',
+ },
+ };
+ },
+ dropdownItems() {
+ return [
+ { items: this.updateDropdownItems },
+ {
+ items: [
+ this.error.gitlabIssuePath ? this.viewIssueDropdownItem : this.createIssueDropdownItem,
+ ],
+ },
+ ];
+ },
},
watch: {
error(val) {
@@ -331,37 +373,14 @@ export default {
</gl-button>
</form>
</div>
- <gl-dropdown
- text="Options"
- class="gl-w-full gl-md-display-none"
- right
+ <gl-disclosure-dropdown
+ block
+ :toggle-text="__('Options')"
+ toggle-class="gl-md-display-none"
+ placement="right"
:disabled="issueUpdateInProgress"
- >
- <gl-dropdown-item
- data-qa-selector="update_ignore_status_button"
- @click="onIgnoreStatusUpdate"
- >{{ ignoreBtnLabel }}</gl-dropdown-item
- >
- <gl-dropdown-item
- data-qa-selector="update_resolve_status_button"
- @click="onResolveStatusUpdate"
- >{{ resolveBtnLabel }}</gl-dropdown-item
- >
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-if="error.gitlabIssuePath"
- data-qa-selector="view_issue_button"
- :href="error.gitlabIssuePath"
- >{{ __('View issue') }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="!error.gitlabIssuePath"
- :loading="issueCreationInProgress"
- data-qa-selector="create_issue_button"
- @click="createIssue"
- >{{ __('Create issue') }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :items="dropdownItems"
+ />
</div>
</div>
<div>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index 96e6c9bab9e..b8921bd0bfa 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -5,18 +5,20 @@ import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { getFormattedItem } from '../utils';
+
import {
COMMON_HANDLES,
COMMAND_HANDLE,
USER_HANDLE,
PROJECT_HANDLE,
ISSUE_HANDLE,
- GLOBAL_COMMANDS_GROUP_TITLE,
+ PATH_HANDLE,
PAGES_GROUP_TITLE,
+ PATH_GROUP_TITLE,
GROUP_TITLES,
} from './constants';
import SearchItem from './search_item.vue';
-import { commandMapper, linksReducer, autocompleteQuery } from './utils';
+import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
export default {
name: 'CommandPaletteItems',
@@ -25,7 +27,14 @@ export default {
GlLoadingIcon,
SearchItem,
},
- inject: ['commandPaletteCommands', 'commandPaletteLinks', 'autocompletePath', 'searchContext'],
+ inject: [
+ 'commandPaletteCommands',
+ 'commandPaletteLinks',
+ 'autocompletePath',
+ 'searchContext',
+ 'projectFilesPath',
+ 'projectBlobPath',
+ ],
props: {
searchQuery: {
type: String,
@@ -35,7 +44,7 @@ export default {
type: String,
required: true,
validator: (value) => {
- return COMMON_HANDLES.includes(value);
+ return [...COMMON_HANDLES, PATH_HANDLE].includes(value);
},
},
},
@@ -43,13 +52,14 @@ export default {
groups: [],
error: null,
loading: false,
+ projectFiles: [],
}),
computed: {
isCommandMode() {
return this.handle === COMMAND_HANDLE;
},
- isUserMode() {
- return this.handle === USER_HANDLE;
+ isPathMode() {
+ return this.handle === PATH_HANDLE;
},
commands() {
return this.commandPaletteCommands.map(commandMapper);
@@ -62,7 +72,7 @@ export default {
? this.commands
.map(({ name, items }) => {
return {
- name: name || GLOBAL_COMMANDS_GROUP_TITLE,
+ name,
items: this.filterBySearchQuery(items, 'text'),
};
})
@@ -73,7 +83,7 @@ export default {
return this.groups?.length && this.groups.some((group) => group.items?.length);
},
hasSearchQuery() {
- if (this.isCommandMode) {
+ if (this.isCommandMode || this.isPathMode) {
return this.searchQuery?.length > 0;
}
return this.searchQuery?.length > 2;
@@ -84,6 +94,12 @@ export default {
}
return this.searchQuery;
},
+ filteredProjectFiles() {
+ if (!this.searchQuery) {
+ return this.projectFiles;
+ }
+ return this.filterBySearchQuery(this.projectFiles, 'text');
+ },
},
watch: {
searchQuery: {
@@ -97,6 +113,9 @@ export default {
case ISSUE_HANDLE:
this.getScopedItems();
break;
+ case PATH_HANDLE:
+ this.getProjectFiles();
+ break;
default:
break;
}
@@ -162,6 +181,26 @@ export default {
},
];
},
+ async getProjectFiles() {
+ if (!this.projectFiles.length) {
+ this.loading = true;
+ try {
+ const response = await axios.get(this.projectFilesPath);
+ this.projectFiles = response?.data.map(fileMapper.bind(null, this.projectBlobPath));
+ } catch (error) {
+ this.error = error;
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ this.groups = [
+ {
+ name: PATH_GROUP_TITLE,
+ items: this.filteredProjectFiles,
+ },
+ ];
+ },
},
};
</script>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index 9dab16984f5..780936c1b88 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -4,17 +4,19 @@ export const COMMAND_HANDLE = '>';
export const USER_HANDLE = '@';
export const PROJECT_HANDLE = '&';
export const ISSUE_HANDLE = '#';
+export const PATH_HANDLE = '/';
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, ISSUE_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
- 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search...',
+ 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue, %{pathHandle} for project file or perform generic search...',
),
{
commandHandle: COMMAND_HANDLE,
userHandle: USER_HANDLE,
issueHandle: ISSUE_HANDLE,
projectHandle: PROJECT_HANDLE,
+ pathHandle: PATH_HANDLE,
},
false,
);
@@ -24,6 +26,7 @@ export const SEARCH_SCOPE_PLACEHOLDER = {
[USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'),
[PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'),
[ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'),
+ [PATH_HANDLE]: s__('CommandPalette|go to project file'),
};
export const SEARCH_SCOPE = {
@@ -37,9 +40,11 @@ export const USERS_GROUP_TITLE = s__('GlobalSearch|Users');
export const PAGES_GROUP_TITLE = s__('CommandPalette|Pages');
export const PROJECTS_GROUP_TITLE = s__('GlobalSearch|Projects');
export const ISSUE_GROUP_TITLE = s__('GlobalSearch|Recent issues');
+export const PATH_GROUP_TITLE = s__('CommandPalette|Project files');
export const GROUP_TITLES = {
[USER_HANDLE]: USERS_GROUP_TITLE,
[PROJECT_HANDLE]: PROJECTS_GROUP_TITLE,
[ISSUE_HANDLE]: ISSUE_GROUP_TITLE,
+ [PATH_HANDLE]: PATH_GROUP_TITLE,
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
index dce2b24f551..efd93e88fa9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
@@ -1,5 +1,5 @@
<script>
-import { COMMON_HANDLES, SEARCH_SCOPE_PLACEHOLDER } from './constants';
+import { COMMON_HANDLES, PATH_HANDLE, SEARCH_SCOPE_PLACEHOLDER } from './constants';
export default {
name: 'FakeSearchInput',
@@ -11,7 +11,7 @@ export default {
scope: {
type: String,
required: true,
- validator: (value) => COMMON_HANDLES.includes(value),
+ validator: (value) => [...COMMON_HANDLES, PATH_HANDLE].includes(value),
},
},
computed: {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 5c8c0e59eaf..347a8ffb0b4 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,12 +1,12 @@
import { isNil, omitBy } from 'lodash';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE } from './constants';
+import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
+import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
export const commandMapper = ({ name, items }) => {
// TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
// and is out of scope for the basic command palette items. If it proves to be useful, we can add it later.
return {
- name,
+ name: name || GLOBAL_COMMANDS_GROUP_TITLE,
items: items.filter(({ component }) => component !== 'invite_members'),
};
};
@@ -32,6 +32,14 @@ export const linksReducer = (acc, menuItem) => {
return acc;
};
+export const fileMapper = (projectBlobPath, file) => {
+ return {
+ icon: 'doc-code',
+ text: file,
+ href: joinPaths(projectBlobPath, file),
+ };
+};
+
export const autocompleteQuery = ({ path, searchTerm, handle, projectId }) => {
const query = omitBy(
{
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index cb34f2b8c26..30b10756ca9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -38,7 +38,11 @@ import {
} from '../constants';
import CommandPaletteItems from '../command_palette/command_palette_items.vue';
import FakeSearchInput from '../command_palette/fake_search_input.vue';
-import { COMMON_HANDLES, SEARCH_OR_COMMAND_MODE_PLACEHOLDER } from '../command_palette/constants';
+import {
+ COMMON_HANDLES,
+ PATH_HANDLE,
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+} from '../command_palette/constants';
import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue';
import GlobalSearchDefaultItems from './global_search_default_items.vue';
import GlobalSearchScopedItems from './global_search_scoped_items.vue';
@@ -135,7 +139,11 @@ export default {
return this.searchText?.trim().charAt(0);
},
isCommandMode() {
- return this.glFeatures?.commandPalette && COMMON_HANDLES.includes(this.searchTextFirstChar);
+ return (
+ this.glFeatures?.commandPalette &&
+ (COMMON_HANDLES.includes(this.searchTextFirstChar) ||
+ (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE))
+ );
},
commandPaletteQuery() {
if (this.isCommandMode) {
@@ -206,7 +214,7 @@ export default {
}
},
focusSearchInput() {
- this.$refs.searchInputBox.$el.querySelector('input').focus();
+ this.$refs.searchInput.$el.querySelector('input').focus();
},
focusNextItem(event, elements, offset) {
const { target } = event;
@@ -226,6 +234,13 @@ export default {
}
visitUrl(this.searchQuery);
},
+ onSearchModalShown() {
+ this.$emit('shown');
+ },
+ onSearchModalHidden() {
+ this.searchText = '';
+ this.$emit('hidden');
+ },
},
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
@@ -243,8 +258,8 @@ export default {
body-class="gl-p-0!"
modal-class="global-search-modal"
:centered="false"
- @hidden="$emit('hidden')"
- @shown="$emit('shown')"
+ @shown="onSearchModalShown"
+ @hide="onSearchModalHidden"
>
<form
role="search"
@@ -256,7 +271,7 @@ export default {
<div class="gl-p-1 gl-relative">
<gl-search-box-by-type
id="search"
- ref="searchInputBox"
+ ref="searchInput"
v-model="searchText"
role="searchbox"
data-testid="global-search-input"
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index f6afde02fa5..322eca72016 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -65,13 +65,23 @@ export const initSuperSidebar = () => {
if (!el) return false;
- const { rootPath, sidebar, toggleNewNavEndpoint, forceDesktopExpandedSidebar } = el.dataset;
+ const {
+ rootPath,
+ sidebar,
+ toggleNewNavEndpoint,
+ forceDesktopExpandedSidebar,
+ commandPalette,
+ } = el.dataset;
bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
const sidebarData = JSON.parse(sidebar);
const searchData = convertObjectPropsToCamelCase(sidebarData.search);
+
+ const commandPaletteData = JSON.parse(commandPalette);
+ const projectFilesPath = commandPaletteData.project_files_url;
+ const projectBlobPath = commandPaletteData.project_blob_url;
const commandPaletteCommands = sidebarData.create_new_menu_groups || [];
const commandPaletteLinks = convertObjectPropsToCamelCase(sidebarData.current_menu_items || []);
@@ -91,6 +101,8 @@ export const initSuperSidebar = () => {
commandPaletteLinks,
autocompletePath,
searchContext,
+ projectFilesPath,
+ projectBlobPath,
},
store: createStore({
searchPath,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 602a83132e4..b5a31f42ec7 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -397,30 +397,24 @@ export default {
/>
</div>
</div>
- <template v-if="hasSuggestion">
- <div
- v-show="previewMarkdown"
- ref="markdown-preview"
- class="js-vue-md-preview md-preview-holder gl-px-5"
- >
- <suggestions
- v-if="hasSuggestion"
- :note-html="markdownPreview"
- :line-type="lineType"
- :disabled="true"
- :suggestions="suggestions"
- :help-page-path="helpPagePath"
- />
- </div>
- </template>
- <template v-else>
- <div
- v-show="previewMarkdown"
- ref="markdown-preview"
- v-safe-html:[$options.safeHtmlConfig]="markdownPreview"
- class="js-vue-md-preview md md-preview-holder gl-px-5"
- ></div>
- </template>
+ <div
+ v-show="previewMarkdown"
+ ref="markdown-preview"
+ class="js-vue-md-preview md-preview-holder gl-px-5"
+ :class="{ md: !hasSuggestion }"
+ >
+ <suggestions
+ v-if="hasSuggestion"
+ :note-html="markdownPreview"
+ :line-type="lineType"
+ :disabled="true"
+ :suggestions="suggestions"
+ :help-page-path="helpPagePath"
+ />
+ <template v-else>
+ <div v-safe-html:[$options.safeHtmlConfig]="markdownPreview"></div>
+ </template>
+ </div>
<div
v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading"
v-safe-html:[$options.safeHtmlConfig]="referencedCommands"
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 96a3fab7e1a..a1d4df6ff48 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
+ include Gitlab::GonHelper
include InitializesCurrentUserMode
include Gitlab::Utils::StrongMemoize
+ before_action :add_gon_variables
before_action :verify_confirmed_email!, :verify_admin_allowed!
layout 'profile'
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index a4f463a23be..d84e3d1e079 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -25,6 +25,10 @@ module GroupsHelper
Ability.allowed?(current_user, :admin_group_member, group)
end
+ def can_admin_service_accounts?(group)
+ false
+ end
+
def group_icon_url(group, options = {})
if group.is_a?(String)
group = Group.find_by_full_path(group)
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 02a912d0227..bcb24aa0f7e 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -123,6 +123,16 @@ module SidebarsHelper
end
end
+ def command_palette_data(project: nil)
+ return {} unless project&.repo_exists?
+ return {} if project.empty_repo?
+
+ {
+ project_files_url: project_files_path(project, project.default_branch, format: :json),
+ project_blob_url: project_blob_path(project, project.default_branch)
+ }
+ end
+
private
def search_data
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 9370982be47..163e741d990 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -100,40 +100,6 @@ class AuditEvent < ApplicationRecord
super || details[:target_details]
end
- def self.by_group(group)
- group_id = group.id
-
- # Bring entity_type and entity_id from projects and group into one query
- scope1 = Group.find(group_id).all_projects.select("'Project' as entity_type", 'id AS entity_id')
- scope2 = Project.from("(VALUES ('Group', #{group_id})) as projects(entity_type, entity_id)").select('entity_type',
- 'entity_id')
- array_scope = Project.from_union([scope1, scope2], remove_duplicates: false).select(:entity_type, :entity_id)
-
- # order by created_at (id is the tie breaker)
- scope = AuditEvent.order(:created_at, :id)
-
- array_mapping_scope = ->(entity_type_expression, entity_id_expression) do
- AuditEvent.where(AuditEvent.arel_table[:entity_id].eq(entity_id_expression))
- .where(AuditEvent.arel_table[:entity_type].eq(entity_type_expression))
- end
-
- finder_query = ->(created_at_expression, id_expression) do
- # we need to add created_at filter as well because that's the partitioning key
- AuditEvent.where(
- AuditEvent.arel_table[:id].eq(id_expression)
- ).where(
- AuditEvent.arel_table[:created_at].eq(created_at_expression)
- )
- end
-
- Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
- scope: scope,
- array_scope: array_scope,
- array_mapping_scope: array_mapping_scope,
- finder_query: finder_query
- ).execute
- end
-
private
def sanitize_message
diff --git a/app/models/group.rb b/app/models/group.rb
index 85971c48567..5d36d63eac2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -663,13 +663,24 @@ class Group < Namespace
# 2. They belong to a project that belongs to the group
# 3. They belong to a sub-group or project in such sub-group
# 4. They belong to an ancestor group
- def direct_and_indirect_users
+ # 5. They belong to a group that is shared with this group, if share_with_groups is true
+ def direct_and_indirect_users(share_with_groups: false)
+ members = if share_with_groups
+ # We only need :user_id column, but
+ # `members_from_self_and_ancestor_group_shares` needs more
+ # columns to make the CTE query work.
+ GroupMember.from_union([
+ direct_and_indirect_members.select(:user_id, :source_type, :type),
+ members_from_self_and_ancestor_group_shares.reselect(:user_id, :source_type, :type)
+ ])
+ else
+ direct_and_indirect_members
+ end
+
User.from_union([
- User
- .where(id: direct_and_indirect_members.select(:user_id))
- .reorder(nil),
- project_users_with_descendants
- ])
+ User.where(id: members.select(:user_id)).reorder(nil),
+ project_users_with_descendants
+ ])
end
# Returns all users (also inactive) that are members of the group because:
diff --git a/app/models/integration.rb b/app/models/integration.rb
index f2f242136ab..c7de9e1e5ff 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -600,6 +600,10 @@ class Integration < ApplicationRecord
category == :chat
end
+ def ci?
+ category == :ci
+ end
+
private
# Ancestors sorted by hierarchy depth in bottom-top order.
diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb
index 8393f1043f8..245c0719439 100644
--- a/app/models/plan_limits.rb
+++ b/app/models/plan_limits.rb
@@ -2,6 +2,9 @@
class PlanLimits < ApplicationRecord
include IgnorableColumns
+ ALLOWED_LIMITS_HISTORY_ATTRIBUTES = %i[notification_limit enforcement_limit storage_size_limit
+ dashboard_limit_enabled_at].freeze
+
ignore_column :ci_max_artifact_size_running_container_scanning, remove_with: '14.3', remove_after: '2021-08-22'
ignore_column :web_hook_calls_high, remove_with: '15.10', remove_after: '2022-02-22'
ignore_column :ci_active_pipelines, remove_with: '16.3', remove_after: '2022-07-22'
@@ -50,18 +53,23 @@ class PlanLimits < ApplicationRecord
false
end
- def log_limits_changes(user, new_limits)
- new_limits.each do |attribute, value|
+ def format_limits_history(user, new_limits)
+ allowed_limits = new_limits.slice(*ALLOWED_LIMITS_HISTORY_ATTRIBUTES)
+ return {} if allowed_limits.empty?
+
+ allowed_limits.each do |attribute, value|
+ next if value == self[attribute]
+
limits_history[attribute] ||= []
limits_history[attribute] << {
- user_id: user&.id,
- username: user&.username,
- timestamp: Time.current.utc.to_i,
- value: value
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => Time.current.utc.to_i,
+ "value" => value
}
end
- update(limits_history: limits_history)
+ limits_history
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 452a5c8973c..23eb58c6020 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1840,10 +1840,12 @@ class Project < ApplicationRecord
triggered.add_hooks(hooks)
end
- def execute_integrations(data, hooks_scope = :push_hooks)
+ def execute_integrations(data, hooks_scope = :push_hooks, skip_ci: false)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
association("#{hooks_scope}_integrations").reader.each do |integration|
+ next if skip_ci && integration.ci?
+
integration.async_execute(data)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index bc4fe36bff4..57904f95372 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2286,6 +2286,10 @@ class User < ApplicationRecord
}
end
+ def allow_possible_spam?
+ custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists?
+ end
+
def namespace_commit_email_for_namespace(namespace)
return if namespace.nil?
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 63a5ee9770f..0ba58a13237 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -15,6 +15,7 @@ class UserCustomAttribute < ApplicationRecord
UNBLOCKED_BY = 'unblocked_by'
ARKOSE_RISK_BAND = 'arkose_risk_band'
AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id'
+ ALLOW_POSSIBLE_SPAM = 'allow_possible_spam'
class << self
def upsert_custom_attributes(custom_attributes)
diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb
index 8ff9d9612c6..f8d9778a3ee 100644
--- a/app/serializers/diff_viewer_entity.rb
+++ b/app/serializers/diff_viewer_entity.rb
@@ -5,7 +5,7 @@ class DiffViewerEntity < Grape::Entity
expose :render_error, as: :error
expose :render_error_message, as: :error_message
expose :collapsed?, as: :collapsed
- expose :whitespace_only, if: ->(_, _) { Feature.enabled?(:add_ignore_all_white_spaces) } do |_, options|
+ expose :whitespace_only do |_, options|
options[:whitespace_only]
end
end
diff --git a/app/services/admin/plan_limits/update_service.rb b/app/services/admin/plan_limits/update_service.rb
index a71d1f14112..cda9a7e7f8c 100644
--- a/app/services/admin/plan_limits/update_service.rb
+++ b/app/services/admin/plan_limits/update_service.rb
@@ -7,26 +7,34 @@ module Admin
@current_user = current_user
@params = params
@plan = plan
+ @plan_limits = plan.actual_limits
end
def execute
return error(_('Access denied'), :forbidden) unless can_update?
- if plan.actual_limits.update(parsed_params)
+ add_history_to_params!
+
+ if plan_limits.update(parsed_params)
success
else
- error(plan.actual_limits.errors.full_messages, :bad_request)
+ error(plan_limits.errors.full_messages, :bad_request)
end
end
private
- attr_accessor :current_user, :params, :plan
+ attr_accessor :current_user, :params, :plan, :plan_limits
def can_update?
current_user.can_admin_all_resources?
end
+ def add_history_to_params!
+ formatted_limits_history = plan_limits.format_limits_history(current_user, parsed_params)
+ parsed_params.merge!(limits_history: formatted_limits_history) unless formatted_limits_history.empty?
+ end
+
# Overridden in EE
def parsed_params
params
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index acf54dec51b..f9280be7ee2 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -67,7 +67,10 @@ module Git
# Creating push_data invokes one CommitDelta RPC per commit. Only
# build this data if we actually need it.
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
- project.execute_integrations(push_data, hook_name) if project.has_active_integrations?(hook_name)
+
+ return unless project.has_active_integrations?(hook_name)
+
+ project.execute_integrations(push_data, hook_name, skip_ci: integration_push_options&.fetch(:skip_ci).present?)
end
def enqueue_invalidate_cache
@@ -101,7 +104,19 @@ module Git
def ci_variables_from_push_options
strong_memoize(:ci_variables_from_push_options) do
- params[:push_options]&.deep_symbolize_keys&.dig(:ci, :variable)
+ push_options&.dig(:ci, :variable)
+ end
+ end
+
+ def integration_push_options
+ strong_memoize(:integration_push_options) do
+ push_options&.dig(:integrations)
+ end
+ end
+
+ def push_options
+ strong_memoize(:push_options) do
+ params[:push_options]&.deep_symbolize_keys
end
end
diff --git a/app/services/groups/participants_service.rb b/app/services/groups/participants_service.rb
index b6faf3fd9a5..575ae1236c2 100644
--- a/app/services/groups/participants_service.rb
+++ b/app/services/groups/participants_service.rb
@@ -2,6 +2,7 @@
module Groups
class ParticipantsService < Groups::BaseService
+ include Gitlab::Utils::StrongMemoize
include Users::ParticipableService
def execute(noteable)
@@ -29,7 +30,10 @@ module Groups
def group_members
return [] unless group
- @group_members ||= sorted(group.direct_and_indirect_users)
+ sorted(
+ group.direct_and_indirect_users(share_with_groups: group.member?(current_user))
+ )
end
+ strong_memoize_attr :group_members
end
end
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 2ecd431fd91..160ebb81b39 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -85,7 +85,7 @@ module Spam
# than the override verdict's priority value), then we don't need to override it.
return false if SUPPORTED_VERDICTS[verdict][:priority] > SUPPORTED_VERDICTS[OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM][:priority]
- target.allow_possible_spam?
+ target.allow_possible_spam? || user.allow_possible_spam?
end
def spamcheck_client
diff --git a/app/services/users/allow_possible_spam_service.rb b/app/services/users/allow_possible_spam_service.rb
new file mode 100644
index 00000000000..d9273fe0fc1
--- /dev/null
+++ b/app/services/users/allow_possible_spam_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Users
+ class AllowPossibleSpamService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ custom_attribute = {
+ user_id: user.id,
+ key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM,
+ value: "#{current_user.username}/#{current_user.id}+#{Time.current}"
+ }
+ UserCustomAttribute.upsert_custom_attributes([custom_attribute])
+ end
+ end
+end
diff --git a/app/services/users/disallow_possible_spam_service.rb b/app/services/users/disallow_possible_spam_service.rb
new file mode 100644
index 00000000000..e31ba7ddff0
--- /dev/null
+++ b/app/services/users/disallow_possible_spam_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Users
+ class DisallowPossibleSpamService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).delete_all
+ end
+ end
+end
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 04bf3f98a1e..e5c66c2c432 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -3,16 +3,18 @@
.row.gl-mt-3
.col-lg-12
- .gl-display-flex.gl-flex-wrap
+ .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
- if can_admin_group_member?(@group)
%h4
= _('Group members')
%p.gl-w-full.order-md-1
= group_member_header_subtext(@group)
- .gl-display-flex.gl-flex-wrap.gl-align-items-flex-start.gl-ml-auto.gl-md-w-auto.gl-w-full.gl-mt-3
+ .gl-display-flex.gl-flex-wrap.gl-align-items-center.gl-gap-3.gl-md-w-auto.gl-w-full
.js-invite-group-trigger{ data: { classes: 'gl-md-w-auto gl-w-full', display_text: _('Invite a group') } }
+ - if can_admin_service_accounts?(@group)
+ = render_if_exists 'groups/group_members/create_service_account'
.js-invite-members-trigger{ data: { variant: 'confirm',
- classes: 'gl-md-w-auto gl-w-full gl-md-ml-3 gl-md-mt-0 gl-mt-3',
+ classes: 'gl-md-w-auto gl-w-full',
trigger_source: 'group-members-page',
display_text: _('Invite members') } }
= render 'groups/invite_groups_modal', group: @group, reload_page_on_submit: true
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8e52f973e9e..eb3b6587bef 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,7 +7,7 @@
- sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user)
- sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json
- %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s } }
+ %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
- if display_whats_new?
#whats-new-app{ data: { version_digest: whats_new_version_digest } }