diff options
Diffstat (limited to 'app')
21 files changed, 147 insertions, 47 deletions
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js new file mode 100644 index 00000000000..650af5fa55a --- /dev/null +++ b/app/assets/javascripts/search/store/getters.js @@ -0,0 +1,9 @@ +import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants'; + +export const frequentGroups = (state) => { + return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY]; +}; + +export const frequentProjects = (state) => { + return state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY]; +}; diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js index 1923c8b96ab..4fa88822722 100644 --- a/app/assets/javascripts/search/store/index.js +++ b/app/assets/javascripts/search/store/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; import createState from './state'; @@ -8,6 +9,7 @@ Vue.use(Vuex); export const getStoreConfig = ({ query }) => ({ actions, + getters, mutations, state: createState({ query }), }); diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js index 9f058efeaa7..60c09221ca9 100644 --- a/app/assets/javascripts/search/store/utils.js +++ b/app/assets/javascripts/search/store/utils.js @@ -24,7 +24,15 @@ export const setFrequentItemToLS = (key, data, itemData) => { return; } - const keyList = ['id', 'avatar_url', 'name', 'full_name', 'name_with_namespace', 'frequency']; + const keyList = [ + 'id', + 'avatar_url', + 'name', + 'full_name', + 'name_with_namespace', + 'frequency', + 'lastUsed', + ]; try { const frequentItems = data[key].map((obj) => extractKeys(obj, keyList)); @@ -35,17 +43,25 @@ export const setFrequentItemToLS = (key, data, itemData) => { // Up the frequency (Max 5) const currentFrequency = frequentItems[existingItemIndex].frequency; frequentItems[existingItemIndex].frequency = Math.min(currentFrequency + 1, MAX_FREQUENCY); + frequentItems[existingItemIndex].lastUsed = new Date().getTime(); } else { // Only store a max of 5 items if (frequentItems.length >= MAX_FREQUENT_ITEMS) { frequentItems.pop(); } - frequentItems.push({ ...item, frequency: 1 }); + frequentItems.push({ ...item, frequency: 1, lastUsed: new Date().getTime() }); } - // Sort by frequency - frequentItems.sort((a, b) => b.frequency - a.frequency); + // Sort by frequency and lastUsed + frequentItems.sort((a, b) => { + if (a.frequency > b.frequency) { + return -1; + } else if (a.frequency < b.frequency) { + return 1; + } + return b.lastUsed - a.lastUsed; + }); // Note we do not need to commit a mutation here as immediately after this we refresh the page to // update the search results. diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue index 2040d2ca173..45a6ae73fac 100644 --- a/app/assets/javascripts/search/topbar/components/group_filter.vue +++ b/app/assets/javascripts/search/topbar/components/group_filter.vue @@ -1,6 +1,6 @@ <script> import { isEmpty } from 'lodash'; -import { mapState, mapActions } from 'vuex'; +import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants'; import SearchableDropdown from './searchable_dropdown.vue'; @@ -19,6 +19,7 @@ export default { }, computed: { ...mapState(['groups', 'fetchingGroups']), + ...mapGetters(['frequentGroups']), selectedGroup() { return isEmpty(this.initialData) ? ANY_OPTION : this.initialData; }, @@ -49,6 +50,7 @@ export default { :loading="fetchingGroups" :selected-item="selectedGroup" :items="groups" + :frequent-items="frequentGroups" @first-open="loadFrequentGroups" @search="fetchGroups" @change="handleGroupChange" diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue index 8589276e9f3..1ca31db61e5 100644 --- a/app/assets/javascripts/search/topbar/components/project_filter.vue +++ b/app/assets/javascripts/search/topbar/components/project_filter.vue @@ -1,5 +1,5 @@ <script> -import { mapState, mapActions } from 'vuex'; +import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants'; import SearchableDropdown from './searchable_dropdown.vue'; @@ -18,6 +18,7 @@ export default { }, computed: { ...mapState(['projects', 'fetchingProjects']), + ...mapGetters(['frequentProjects']), selectedProject() { return this.initialData ? this.initialData : ANY_OPTION; }, @@ -52,6 +53,7 @@ export default { :loading="fetchingProjects" :selected-item="selectedProject" :items="projects" + :frequent-items="frequentProjects" @first-open="loadFrequentProjects" @search="fetchProjects" @change="handleProjectChange" diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue index 488fee90fca..5653cddda60 100644 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue +++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue @@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, GlLoadingIcon, GlIcon, @@ -16,11 +17,13 @@ import SearchableDropdownItem from './searchable_dropdown_item.vue'; export default { i18n: { clearLabel: __('Clear'), + frequentlySearched: __('Frequently searched'), }, name: 'SearchableDropdown', components: { GlDropdown, GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, GlLoadingIcon, GlIcon, @@ -61,6 +64,11 @@ export default { required: false, default: () => [], }, + frequentItems: { + type: Array, + required: false, + default: () => [], + }, }, data() { return { @@ -68,6 +76,11 @@ export default { hasBeenOpened: false, }; }, + computed: { + showFrequentItems() { + return !this.searchText && this.frequentItems.length > 0; + }, + }, methods: { isSelected(selected) { return selected.id === this.selectedItem.id; @@ -139,6 +152,25 @@ export default { <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span> </gl-dropdown-item> </div> + <div + v-if="showFrequentItems" + class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2 gl-mb-2" + > + <gl-dropdown-section-header>{{ + $options.i18n.frequentlySearched + }}</gl-dropdown-section-header> + <searchable-dropdown-item + v-for="item in frequentItems" + :key="item.id" + :item="item" + :selected-item="selectedItem" + :search-text="searchText" + :name="name" + :full-name="fullName" + data-testid="frequent-items" + @change="updateDropdown" + /> + </div> <div v-if="!loading"> <searchable-dropdown-item v-for="item in items" @@ -148,6 +180,7 @@ export default { :search-text="searchText" :name="name" :full-name="fullName" + data-testid="searchable-items" @change="updateDropdown" /> </div> diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index 7f04927f517..9e16d195b00 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -22,7 +22,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end def destroy - @gpg_key.destroy + GpgKeys::DestroyService.new(current_user).execute(@gpg_key) respond_to do |format| format.html { redirect_to profile_gpg_keys_url, status: :found } diff --git a/app/graphql/types/ci/build_need_type.rb b/app/graphql/types/ci/build_need_type.rb index 3bd81f8fa8f..19ff758ad1d 100644 --- a/app/graphql/types/ci/build_need_type.rb +++ b/app/graphql/types/ci/build_need_type.rb @@ -7,6 +7,8 @@ module Types class BuildNeedType < BaseObject graphql_name 'CiBuildNeed' + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the job we need to complete.' field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the job we need to complete.' end diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb index 0b643a6b676..6310a62a103 100644 --- a/app/graphql/types/ci/detailed_status_type.rb +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -6,6 +6,9 @@ module Types class DetailedStatusType < BaseObject graphql_name 'DetailedStatus' + field :id, GraphQL::STRING_TYPE, null: false, + description: 'ID for a detailed status.', + extras: [:parent] field :group, GraphQL::STRING_TYPE, null: true, description: 'Group of the status.' field :icon, GraphQL::STRING_TYPE, null: true, @@ -29,6 +32,10 @@ module Types calls_gitaly: true, description: 'Action information for the status. This includes method, button title, icon, path, and title.' + def id(parent:) + "#{object.id}-#{parent.object.object.id}" + end + def action if object.has_action? { diff --git a/app/graphql/types/ci/group_type.rb b/app/graphql/types/ci/group_type.rb index d6d4252e8d7..3da183cb842 100644 --- a/app/graphql/types/ci/group_type.rb +++ b/app/graphql/types/ci/group_type.rb @@ -6,12 +6,14 @@ module Types class GroupType < BaseObject graphql_name 'CiGroup' + field :id, GraphQL::STRING_TYPE, null: false, + description: 'ID for a group.' field :name, GraphQL::STRING_TYPE, null: true, - description: 'Name of the job group.' + description: 'Name of the job group.' field :size, GraphQL::INT_TYPE, null: true, - description: 'Size of the group.' + description: 'Size of the group.' field :jobs, Ci::JobType.connection_type, null: true, - description: 'Jobs in group.' + description: 'Jobs in group.' field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the group.' diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index a9499e51124..ce3edb6c54f 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -6,20 +6,16 @@ module Types graphql_name 'CiStage' authorize :read_commit_status - field :name, - type: GraphQL::STRING_TYPE, - null: true, + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the stage.' + field :name, type: GraphQL::STRING_TYPE, null: true, description: 'Name of the stage.' - field :groups, - type: Ci::GroupType.connection_type, - null: true, + field :groups, type: Ci::GroupType.connection_type, null: true, extras: [:lookahead], description: 'Group of jobs for the stage.' - field :detailed_status, Types::Ci::DetailedStatusType, - null: true, + field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the stage.' - field :jobs, Ci::JobType.connection_type, - null: true, + field :jobs, Ci::JobType.connection_type, null: true, description: 'Jobs for the stage.', method: 'latest_statuses' field :status, GraphQL::STRING_TYPE, diff --git a/app/graphql/types/ci/status_action_type.rb b/app/graphql/types/ci/status_action_type.rb index 9f7299c0270..a06b09735b3 100644 --- a/app/graphql/types/ci/status_action_type.rb +++ b/app/graphql/types/ci/status_action_type.rb @@ -5,6 +5,9 @@ module Types class StatusActionType < BaseObject graphql_name 'StatusAction' + field :id, GraphQL::STRING_TYPE, null: false, + description: 'ID for a status action.', + extras: [:parent] field :button_title, GraphQL::STRING_TYPE, null: true, description: 'Title for the button, for example: Retry this job.' field :icon, GraphQL::STRING_TYPE, null: true, @@ -17,6 +20,10 @@ module Types field :title, GraphQL::STRING_TYPE, null: true, description: 'Title for the action, for example: Retry.' + def id(parent:) + "#{parent.parent.object.object.class.name}-#{parent.object.object.id}" + end + def action_method object[:method] end diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 47b91fcf2ce..e5cb2026503 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -10,6 +10,7 @@ module Ci class Group include StaticModel include Gitlab::Utils::StrongMemoize + include GlobalID::Identification attr_reader :project, :stage, :name, :jobs @@ -22,6 +23,10 @@ module Ci @jobs = jobs end + def id + "#{stage.id}-#{name}" + end + def ==(other) other.present? && other.is_a?(self.class) && project == other.project && diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 600abc33471..fc890bf687c 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -15,7 +15,7 @@ class NamespaceSetting < ApplicationRecord NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal, :lock_delayed_project_removal, :resource_access_token_creation_allowed, - :prevent_sharing_groups_outside_hierarchy].freeze + :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap].freeze self.primary_key = :namespace_id diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 63727e45a5c..e14ba035cc8 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -32,7 +32,9 @@ class UserCallout < ApplicationRecord pipeline_needs_hover_tip: 30, web_ide_ci_environments_guidance: 31, security_configuration_upgrade_banner: 32, - cloud_licensing_subscription_activation_banner: 33 # EE-only + cloud_licensing_subscription_activation_banner: 33, # EE-only + trial_status_reminder_d14: 34, # EE-only + trial_status_reminder_d3: 35 # EE-only } validates :user, presence: true diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index ba06b98e906..0b0edc7c452 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -156,6 +156,7 @@ class GroupPolicy < BasePolicy enable :set_note_created_at enable :set_emails_disabled enable :change_prevent_sharing_groups_outside_hierarchy + enable :change_new_user_signups_cap enable :update_default_branch_protection enable :create_deploy_token enable :destroy_deploy_token diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb index e41444b2a82..ab8b12732d7 100644 --- a/app/services/gpg_keys/create_service.rb +++ b/app/services/gpg_keys/create_service.rb @@ -3,9 +3,17 @@ module GpgKeys class CreateService < Keys::BaseService def execute - key = user.gpg_keys.create(params) + key = create(params) notification_service.new_gpg_key(key) if key.persisted? key end + + private + + def create(params) + user.gpg_keys.create(params) + end end end + +GpgKeys::CreateService.prepend_mod diff --git a/app/services/gpg_keys/destroy_service.rb b/app/services/gpg_keys/destroy_service.rb index cecbfe26611..2e82509897e 100644 --- a/app/services/gpg_keys/destroy_service.rb +++ b/app/services/gpg_keys/destroy_service.rb @@ -7,3 +7,5 @@ module GpgKeys end end end + +GpgKeys::DestroyService.prepend_mod diff --git a/app/services/namespace_settings/update_service.rb b/app/services/namespace_settings/update_service.rb index c71f015b9d4..25525265e1c 100644 --- a/app/services/namespace_settings/update_service.rb +++ b/app/services/namespace_settings/update_service.rb @@ -14,7 +14,15 @@ module NamespaceSettings def execute validate_resource_access_token_creation_allowed_param - validate_prevent_sharing_groups_outside_hierarchy_param + + validate_settings_param_for_root_group( + param_key: :prevent_sharing_groups_outside_hierarchy, + user_policy: :change_prevent_sharing_groups_outside_hierarchy + ) + validate_settings_param_for_root_group( + param_key: :new_user_signups_cap, + user_policy: :change_new_user_signups_cap + ) if group.namespace_settings group.namespace_settings.attributes = settings_params @@ -34,17 +42,17 @@ module NamespaceSettings end end - def validate_prevent_sharing_groups_outside_hierarchy_param - return if settings_params[:prevent_sharing_groups_outside_hierarchy].nil? + def validate_settings_param_for_root_group(param_key:, user_policy:) + return if settings_params[param_key].nil? - unless can?(current_user, :change_prevent_sharing_groups_outside_hierarchy, group) - settings_params.delete(:prevent_sharing_groups_outside_hierarchy) - group.namespace_settings.errors.add(:prevent_sharing_groups_outside_hierarchy, _('can only be changed by a group admin.')) + unless can?(current_user, user_policy, group) + settings_params.delete(param_key) + group.namespace_settings.errors.add(param_key, _('can only be changed by a group admin.')) end unless group.root? - settings_params.delete(:prevent_sharing_groups_outside_hierarchy) - group.namespace_settings.errors.add(:prevent_sharing_groups_outside_hierarchy, _('only available on top-level groups.')) + settings_params.delete(param_key) + group.namespace_settings.errors.add(param_key, _('only available on top-level groups.')) end end end diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index d9a46890c45..6d3b63de9fd 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -20,14 +20,12 @@ module Snippets @snippet.author = current_user - if Feature.enabled?(:snippet_spam) - Spam::SpamActionService.new( - spammable: @snippet, - spam_params: spam_params, - user: current_user, - action: :create - ).execute - end + Spam::SpamActionService.new( + spammable: @snippet, + spam_params: spam_params, + user: current_user, + action: :create + ).execute if save_and_commit UserAgentDetailService.new(spammable: @snippet, spam_params: spam_params).create diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb index f8374cc88bb..d83b21271c0 100644 --- a/app/services/snippets/update_service.rb +++ b/app/services/snippets/update_service.rb @@ -23,14 +23,12 @@ module Snippets update_snippet_attributes(snippet) - if Feature.enabled?(:snippet_spam) - Spam::SpamActionService.new( - spammable: snippet, - spam_params: spam_params, - user: current_user, - action: :update - ).execute - end + Spam::SpamActionService.new( + spammable: snippet, + spam_params: spam_params, + user: current_user, + action: :update + ).execute if save_and_commit(snippet) Gitlab::UsageDataCounters::SnippetCounter.count(:update) |