diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-21 15:09:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-21 15:09:17 +0300 |
commit | 0c4570435d417b69efd433057f95f01810618837 (patch) | |
tree | 4e402832206b83da2d73671977c1e5f7cae9074a /app | |
parent | 49abdb108a4d3c3f2ef9b27c7c4dcde43da1016a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 } } |