diff options
58 files changed, 705 insertions, 276 deletions
diff --git a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql new file mode 100644 index 00000000000..1d9497d65ce --- /dev/null +++ b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql @@ -0,0 +1,14 @@ +query searchProjectMembers($fullPath: ID!, $search: String) { + project(fullPath: $fullPath) { + projectMembers(search: $search) { + nodes { + user { + id + name + username + avatarUrl + } + } + } + } +} diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue index 9e3347a657f..8df51ef7f9b 100644 --- a/app/assets/javascripts/import_entities/components/import_status.vue +++ b/app/assets/javascripts/import_entities/components/import_status.vue @@ -34,7 +34,7 @@ export default { </script> <template> - <div> + <div class="gl-display-flex gl-h-7 gl-align-items-center"> <gl-loading-icon v-if="mappedStatus.loadingIcon" :inline="true" diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index c0fd076ee6b..d0a0a3463c0 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -33,6 +33,10 @@ export default { type: String, required: true, }, + groupPathRegex: { + type: RegExp, + required: true, + }, }, data() { @@ -165,12 +169,13 @@ export default { <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th> <th class="gl-py-4 import-jobs-cta-col"></th> </thead> - <tbody> + <tbody class="gl-vertical-align-top"> <template v-for="group in bulkImportSourceGroups.nodes"> <import-table-row :key="group.id" :group="group" :available-namespaces="availableNamespaces" + :group-path-regex="groupPathRegex" @update-target-namespace="updateTargetNamespace(group.id, $event)" @update-new-name="updateNewName(group.id, $event)" @import-group="importGroup(group.id)" diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index 3b8abeae122..aed879e75fb 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -1,16 +1,29 @@ <script> -import { GlButton, GlIcon, GlLink, GlFormInput } from '@gitlab/ui'; +import { + GlButton, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownSectionHeader, + GlIcon, + GlLink, + GlFormInput, +} from '@gitlab/ui'; import { joinPaths } from '~/lib/utils/url_utility'; -import { s__ } from '~/locale'; -import Select2Select from '~/vue_shared/components/select2_select.vue'; import ImportStatus from '../../components/import_status.vue'; import { STATUSES } from '../../constants'; +import groupQuery from '../graphql/queries/group.query.graphql'; + +const DEBOUNCE_INTERVAL = 300; export default { components: { - Select2Select, ImportStatus, GlButton, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownSectionHeader, GlLink, GlIcon, GlFormInput, @@ -24,82 +37,131 @@ export default { type: Array, required: true, }, + groupPathRegex: { + type: RegExp, + required: true, + }, }, - computed: { - isDisabled() { - return this.group.status !== STATUSES.NONE; + + apollo: { + existingGroup: { + query: groupQuery, + debounce: DEBOUNCE_INTERVAL, + variables() { + return { + fullPath: this.fullPath, + }; + }, + skip() { + return !this.isNameValid || this.isAlreadyImported; + }, }, + }, - isFinished() { - return this.group.status === STATUSES.FINISHED; + computed: { + importTarget() { + return this.group.import_target; }, - select2Options() { - const availableNamespacesData = this.availableNamespaces.map((namespace) => ({ - id: namespace.full_path, - text: namespace.full_path, - })); + isInvalid() { + return Boolean(!this.isNameValid || this.existingGroup); + }, - const select2Config = { - data: [{ id: '', text: s__('BulkImport|No parent') }], - }; + isNameValid() { + return this.groupPathRegex.test(this.importTarget.new_name); + }, - if (availableNamespacesData.length) { - select2Config.data.push({ - text: s__('BulkImport|Existing groups'), - children: availableNamespacesData, - }); - } + isAlreadyImported() { + return this.group.status !== STATUSES.NONE; + }, - return select2Config; + isFinished() { + return this.group.status === STATUSES.FINISHED; }, - }, - methods: { - getPath(group) { - return `${group.import_target.target_namespace}/${group.import_target.new_name}`; + + fullPath() { + return `${this.importTarget.target_namespace}/${this.importTarget.new_name}`; }, - getFullPath(group) { - return joinPaths(gon.relative_url_root || '/', this.getPath(group)); + absolutePath() { + return joinPaths(gon.relative_url_root || '/', this.fullPath); }, }, }; </script> <template> - <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1"> + <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"> <td class="gl-p-4"> - <gl-link :href="group.web_url" target="_blank"> + <gl-link + :href="group.web_url" + target="_blank" + class="gl-display-flex gl-align-items-center gl-h-7" + > {{ group.full_path }} <gl-icon name="external-link" /> </gl-link> </td> <td class="gl-p-4"> - <gl-link v-if="isFinished" :href="getFullPath(group)">{{ getPath(group) }}</gl-link> + <gl-link + v-if="isFinished" + class="gl-display-flex gl-align-items-center gl-h-7" + :href="absolutePath" + > + {{ fullPath }} + </gl-link> <div v-else class="import-entities-target-select gl-display-flex gl-align-items-stretch" :class="{ - disabled: isDisabled, + disabled: isAlreadyImported, }" > - <select2-select - :disabled="isDisabled" - :options="select2Options" - :value="group.import_target.target_namespace" - @input="$emit('update-target-namespace', $event)" - /> + <gl-dropdown + :text="importTarget.target_namespace" + :disabled="isAlreadyImported" + toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!" + class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1" + > + <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{ + s__('BulkImport|No parent') + }}</gl-dropdown-item> + <template v-if="availableNamespaces.length"> + <gl-dropdown-divider /> + <gl-dropdown-section-header> + {{ s__('BulkImport|Existing groups') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="ns in availableNamespaces" + :key="ns.full_path" + @click="$emit('update-target-namespace', ns.full_path)" + > + {{ ns.full_path }} + </gl-dropdown-item> + </template> + </gl-dropdown> <div - class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1" + class="import-entities-target-select-separator gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1" > / </div> - <gl-form-input - class="gl-rounded-top-left-none gl-rounded-bottom-left-none" - :disabled="isDisabled" - :value="group.import_target.new_name" - @input="$emit('update-new-name', $event)" - /> + <div class="gl-flex-fill-1"> + <gl-form-input + class="gl-rounded-top-left-none gl-rounded-bottom-left-none" + :class="{ 'is-invalid': isInvalid && !isAlreadyImported }" + :disabled="isAlreadyImported" + :value="importTarget.new_name" + @input="$emit('update-new-name', $event)" + /> + <p v-if="isInvalid" class="gl-text-red-500 gl-m-0 gl-mt-2"> + <template v-if="!isNameValid"> + {{ __('Please choose a group URL with no special characters.') }} + </template> + <template v-else-if="existingGroup"> + {{ s__('BulkImport|Name already exists.') }} + </template> + </p> + </div> </div> </td> <td class="gl-p-4 gl-white-space-nowrap"> @@ -107,7 +169,8 @@ export default { </td> <td class="gl-p-4"> <gl-button - v-if="!isDisabled" + v-if="!isAlreadyImported" + :disabled="isInvalid" variant="success" category="secondary" @click="$emit('import-group')" diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql new file mode 100644 index 00000000000..52df3581ac4 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql @@ -0,0 +1,5 @@ +query group($fullPath: ID!) { + existingGroup: group(fullPath: $fullPath) { + id + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js index cf56bb804d4..cc60c8cbdb0 100644 --- a/app/assets/javascripts/import_entities/import_groups/index.js +++ b/app/assets/javascripts/import_entities/import_groups/index.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '~/vue_shared/translate'; import ImportTable from './components/import_table.vue'; import { createApolloClient } from './graphql/client_factory'; @@ -17,7 +16,7 @@ export function mountImportGroupsApp(mountElement) { createBulkImportPath, jobsPath, sourceUrl, - canCreateGroup, + groupPathRegex, } = mountElement.dataset; const apolloProvider = new VueApollo({ defaultClient: createApolloClient({ @@ -38,7 +37,7 @@ export function mountImportGroupsApp(mountElement) { return createElement(ImportTable, { props: { sourceUrl, - canCreateGroup: parseBoolean(canCreateGroup), + groupPathRegex: new RegExp(`^(${groupPathRegex})$`), }, }); }, diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss index 453b810196b..525481638f3 100644 --- a/app/assets/stylesheets/page_bundles/import.scss +++ b/app/assets/stylesheets/page_bundles/import.scss @@ -1,5 +1,12 @@ @import 'mixins_and_variables_and_functions'; +// Fixing double scrollbar issue +// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1156 and +// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54837 +.import-entities-namespace-dropdown.show.dropdown .dropdown-menu { + max-height: initial; +} + .import-jobs-to-col { width: 39%; } diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 42042406f3f..5ac905e0dd4 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -14,6 +14,7 @@ # active: boolean # blocked: boolean # external: boolean +# non_external: boolean # without_projects: boolean # sort: string # id: integer @@ -40,6 +41,7 @@ class UsersFinder users = by_active(users) users = by_external_identity(users) users = by_external(users) + users = by_non_external(users) users = by_2fa(users) users = by_created_at(users) users = by_without_projects(users) @@ -97,13 +99,18 @@ class UsersFinder # rubocop: disable CodeReuse/ActiveRecord def by_external(users) - return users = users.where.not(external: true) unless current_user&.admin? return users unless params[:external] users.external end # rubocop: enable CodeReuse/ActiveRecord + def by_non_external(users) + return users unless params[:non_external] + + users.non_external + end + def by_2fa(users) case params[:two_factor] when 'enabled' diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb index d30d1d89bb2..0a025a21353 100644 --- a/app/graphql/mutations/boards/lists/update.rb +++ b/app/graphql/mutations/boards/lists/update.rb @@ -17,7 +17,7 @@ module Mutations argument :collapsed, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Indicates if list is collapsed for this user.' + description: 'Indicates if the list is collapsed for this user.' field :list, Types::BoardListType, diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb index 46b49c5d8a4..b367aa946ae 100644 --- a/app/graphql/types/board_list_type.rb +++ b/app/graphql/types/board_list_type.rb @@ -19,7 +19,7 @@ module Types field :label, Types::LabelType, null: true, description: 'Label of the list.' field :collapsed, GraphQL::BOOLEAN_TYPE, null: true, - description: 'Indicates if list is collapsed for this user.' + description: 'Indicates if the list is collapsed for this user.' field :issues_count, GraphQL::INT_TYPE, null: true, description: 'Count of issues in the list.' diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb index 543b7f8e5b2..96a72b6a972 100644 --- a/app/graphql/types/issuable_state_enum.rb +++ b/app/graphql/types/issuable_state_enum.rb @@ -5,9 +5,9 @@ module Types graphql_name 'IssuableState' description 'State of a GitLab issue or merge request' - value 'opened' - value 'closed' - value 'locked' - value 'all' + value 'opened', description: 'In open state' + value 'closed', description: 'In closed state' + value 'locked', description: 'Discussion has been locked' + value 'all', description: 'All available' end end diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb index b7c0a8b3489..d6863e87261 100644 --- a/app/models/concerns/boards/listable.rb +++ b/app/models/concerns/boards/listable.rb @@ -13,6 +13,14 @@ module Boards scope :ordered, -> { order(:list_type, :position) } scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } + + class << self + def preload_preferences_for_user(lists, user) + return unless user + + lists.each { |list| list.preferences_for(user) } + end + end end class_methods do @@ -33,6 +41,18 @@ module Boards self.class.movable_types.include?(list_type&.to_sym) end + def collapsed?(user) + preferences = preferences_for(user) + + preferences.collapsed? + end + + def update_preferences_for(user, preferences = {}) + return unless user + + preferences_for(user).update(preferences) + end + def title if label? label.name diff --git a/app/models/list.rb b/app/models/list.rb index 5bd00a1d7ef..e1954ed72c4 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -18,14 +18,6 @@ class List < ApplicationRecord alias_method :preferences, :list_user_preferences - class << self - def preload_preferences_for_user(lists, user) - return unless user - - lists.each { |list| list.preferences_for(user) } - end - end - def preferences_for(user) return preferences.build unless user @@ -39,18 +31,6 @@ class List < ApplicationRecord end end - def update_preferences_for(user, preferences = {}) - return unless user - - preferences_for(user).update(preferences) - end - - def collapsed?(user) - preferences = preferences_for(user) - - preferences.collapsed? - end - def as_json(options = {}) super(options).tap do |json| json[:collapsed] = false diff --git a/app/models/user.rb b/app/models/user.rb index d91fc3ebce4..4c2bef2c725 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -360,6 +360,7 @@ class User < ApplicationRecord scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) } scope :external, -> { where(external: true) } + scope :non_external, -> { where(external: false) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :active, -> { with_state(:active).non_internal } scope :active_without_ghosts, -> { with_state(:active).without_ghosts } diff --git a/app/services/security/vulnerability_uuid.rb b/app/services/security/vulnerability_uuid.rb new file mode 100644 index 00000000000..3eab0f3dad6 --- /dev/null +++ b/app/services/security/vulnerability_uuid.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Security + class VulnerabilityUUID + def self.generate(report_type:, primary_identifier_fingerprint:, location_fingerprint:, project_id:) + Gitlab::UUID.v5("#{report_type}-#{primary_identifier_fingerprint}-#{location_fingerprint}-#{project_id}") + end + end +end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index f6ebc4c465d..f16158d5656 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -60,7 +60,7 @@ .d-flex.align-items-center = sprite_icon('group', size: 16, css_class: 'gl-text-gray-700') %h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group) - .gl-mt-3.text-uppercase= s_('AdminArea|Projects') + .gl-mt-3.text-uppercase= s_('AdminArea|Groups') = link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default") .gl-card-footer.gl-bg-transparent .d-flex.align-items-center diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml index 778bc1ef1a4..917d88af75a 100644 --- a/app/views/import/bulk_imports/status.html.haml +++ b/app/views/import/bulk_imports/status.html.haml @@ -9,4 +9,5 @@ available_namespaces_path: import_available_namespaces_path(format: :json), create_bulk_import_path: import_bulk_imports_path(format: :json), jobs_path: realtime_changes_import_bulk_imports_path(format: :json), - source_url: @source_url } } + source_url: @source_url, + group_path_regex: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS } } diff --git a/changelogs/unreleased/301018-cablett-collapsed-epic-board-list.yml b/changelogs/unreleased/301018-cablett-collapsed-epic-board-list.yml new file mode 100644 index 00000000000..89862375316 --- /dev/null +++ b/changelogs/unreleased/301018-cablett-collapsed-epic-board-list.yml @@ -0,0 +1,5 @@ +--- +title: Expose epic board list collapsed value via GraphQL +merge_request: 54541 +author: +type: added diff --git a/changelogs/unreleased/admin-dashboard-typo.yml b/changelogs/unreleased/admin-dashboard-typo.yml new file mode 100644 index 00000000000..81b113f8b1a --- /dev/null +++ b/changelogs/unreleased/admin-dashboard-typo.yml @@ -0,0 +1,5 @@ +--- +title: "Renamed the second project panel to group" +merge_request: 54834 +author: Niklas van Schrick @Taucher2003 +type: fixed diff --git a/changelogs/unreleased/fix-include-external-users-in-user-search.yml b/changelogs/unreleased/fix-include-external-users-in-user-search.yml new file mode 100644 index 00000000000..7421db86f1e --- /dev/null +++ b/changelogs/unreleased/fix-include-external-users-in-user-search.yml @@ -0,0 +1,5 @@ +--- +title: 'API: include external users in user search for non-admins' +merge_request: 53584 +author: Jonas Wälter @wwwjon +type: changed diff --git a/config/metrics/counts_28d/20210216180524_projects_with_incidents.yml b/config/metrics/counts_28d/20210216180524_projects_with_incidents.yml index 8ad00f8c06e..15ebaa81855 100644 --- a/config/metrics/counts_28d/20210216180524_projects_with_incidents.yml +++ b/config/metrics/counts_28d/20210216180524_projects_with_incidents.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage_monthly.monitor.projects_with_incidents -description: 'Count of unique projects with an incident created in the last month ' +description: 'Count of unique projects with an incident created in the last month' product_section: ops product_stage: monitor product_group: group::health diff --git a/config/metrics/counts_28d/20210216180526_projects_with_alert_incidents.yml b/config/metrics/counts_28d/20210216180526_projects_with_alert_incidents.yml index 2ea8eb63889..3559638afb6 100644 --- a/config/metrics/counts_28d/20210216180526_projects_with_alert_incidents.yml +++ b/config/metrics/counts_28d/20210216180526_projects_with_alert_incidents.yml @@ -1,7 +1,7 @@ --- key_path: usage_activity_by_stage_monthly.monitor.projects_with_alert_incidents description: 'Count of unique projects with an incident from an alert created in the - last month ' + last month' product_section: ops product_stage: monitor product_group: group::health diff --git a/config/metrics/counts_all/20210216180707_gitlab_project.yml b/config/metrics/counts_all/20210216180707_gitlab_project.yml index 173e20245ae..a241abcad2c 100644 --- a/config/metrics/counts_all/20210216180707_gitlab_project.yml +++ b/config/metrics/counts_all/20210216180707_gitlab_project.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.gitlab_project -description: 'Distinct count of users that imported projects using Project Import/Export ' +description: 'Distinct count of users that imported projects using Project Import/Export' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180709_gitlab.yml b/config/metrics/counts_all/20210216180709_gitlab.yml index fee163d2728..c7b224a18a5 100644 --- a/config/metrics/counts_all/20210216180709_gitlab.yml +++ b/config/metrics/counts_all/20210216180709_gitlab.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.gitlab -description: 'Distinct count of users that imported projects from GitLab.com ' +description: 'Distinct count of users that imported projects from GitLab.com' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180713_bitbucket.yml b/config/metrics/counts_all/20210216180713_bitbucket.yml index f9aea1fd773..28f9b45d250 100644 --- a/config/metrics/counts_all/20210216180713_bitbucket.yml +++ b/config/metrics/counts_all/20210216180713_bitbucket.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.bitbucket -description: 'Distinct count of users that imported projects from Bitbucket Cloud ' +description: 'Distinct count of users that imported projects from Bitbucket Cloud' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180715_bitbucket_server.yml b/config/metrics/counts_all/20210216180715_bitbucket_server.yml index e9f46e2af33..58b4fc235b2 100644 --- a/config/metrics/counts_all/20210216180715_bitbucket_server.yml +++ b/config/metrics/counts_all/20210216180715_bitbucket_server.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.bitbucket_server -description: 'Distinct count of users that imported projects from Bitbucket Server ' +description: 'Distinct count of users that imported projects from Bitbucket Server' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180716_gitea.yml b/config/metrics/counts_all/20210216180716_gitea.yml index 4ff1f0c2aef..28c4b03f1e9 100644 --- a/config/metrics/counts_all/20210216180716_gitea.yml +++ b/config/metrics/counts_all/20210216180716_gitea.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.gitea -description: 'Distinct count of users that imported projects from Gitea ' +description: 'Distinct count of users that imported projects from Gitea' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180718_git.yml b/config/metrics/counts_all/20210216180718_git.yml index c36d2ce4d42..085bb5f9764 100644 --- a/config/metrics/counts_all/20210216180718_git.yml +++ b/config/metrics/counts_all/20210216180718_git.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.git -description: 'Distinct count of users that imported projects using Import by URL ' +description: 'Distinct count of users that imported projects using Import by URL' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180720_manifest.yml b/config/metrics/counts_all/20210216180720_manifest.yml index c5d35a9707b..8008afc98d7 100644 --- a/config/metrics/counts_all/20210216180720_manifest.yml +++ b/config/metrics/counts_all/20210216180720_manifest.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.projects_imported.manifest -description: 'Distinct count of users that imported projects using Manifest file ' +description: 'Distinct count of users that imported projects using Manifest file' product_section: dev product_stage: manage product_group: group::import diff --git a/config/metrics/counts_all/20210216180724_fogbugz.yml b/config/metrics/counts_all/20210216180724_fogbugz.yml index 2224b5dd717..18f4742bf81 100644 --- a/config/metrics/counts_all/20210216180724_fogbugz.yml +++ b/config/metrics/counts_all/20210216180724_fogbugz.yml @@ -1,6 +1,6 @@ --- key_path: usage_activity_by_stage.manage.issues_imported.fogbugz -description: 'Distinct count of users that imported issues into projects using FogBugz ' +description: 'Distinct count of users that imported issues into projects using FogBugz' product_section: dev product_stage: manage product_group: group::import diff --git a/db/migrate/20210217101901_create_epic_list_user_preferences.rb b/db/migrate/20210217101901_create_epic_list_user_preferences.rb new file mode 100644 index 00000000000..5aacea1938d --- /dev/null +++ b/db/migrate/20210217101901_create_epic_list_user_preferences.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateEpicListUserPreferences < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def up + create_table :boards_epic_list_user_preferences do |t| + t.bigint :user_id, null: false + t.bigint :epic_list_id, index: true, null: false + t.timestamps_with_timezone null: false + t.boolean :collapsed, null: false, default: false + end + + add_index :boards_epic_list_user_preferences, [:user_id, :epic_list_id], unique: true, name: 'index_epic_board_list_preferences_on_user_and_list' + end + + def down + drop_table :boards_epic_list_user_preferences + end +end diff --git a/db/migrate/20210222085529_add_epic_board_user_preference_user_fk.rb b/db/migrate/20210222085529_add_epic_board_user_preference_user_fk.rb new file mode 100644 index 00000000000..52de892a177 --- /dev/null +++ b/db/migrate/20210222085529_add_epic_board_user_preference_user_fk.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddEpicBoardUserPreferenceUserFk < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :boards_epic_list_user_preferences, :users, column: :user_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :boards_epic_list_user_preferences, :users + end + end +end diff --git a/db/migrate/20210222085551_add_epic_board_user_preference_epic_list_fk.rb b/db/migrate/20210222085551_add_epic_board_user_preference_epic_list_fk.rb new file mode 100644 index 00000000000..3f62036b899 --- /dev/null +++ b/db/migrate/20210222085551_add_epic_board_user_preference_epic_list_fk.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddEpicBoardUserPreferenceEpicListFk < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :boards_epic_list_user_preferences, :boards_epic_lists, column: :epic_list_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :boards_epic_list_user_preferences, :boards_epic_lists + end + end +end diff --git a/db/schema_migrations/20210217101901 b/db/schema_migrations/20210217101901 new file mode 100644 index 00000000000..f4c38be26e0 --- /dev/null +++ b/db/schema_migrations/20210217101901 @@ -0,0 +1 @@ +909aee5ed0ad447fec425f7252fc6dbec827a66ff720620bae1bf3a32536cb96
\ No newline at end of file diff --git a/db/schema_migrations/20210222085529 b/db/schema_migrations/20210222085529 new file mode 100644 index 00000000000..63f47781b3b --- /dev/null +++ b/db/schema_migrations/20210222085529 @@ -0,0 +1 @@ +858cd59ea324e3653801055c7f3fae2152b04ac175945a59faa00d67ae7fa223
\ No newline at end of file diff --git a/db/schema_migrations/20210222085551 b/db/schema_migrations/20210222085551 new file mode 100644 index 00000000000..f0ba3562c3a --- /dev/null +++ b/db/schema_migrations/20210222085551 @@ -0,0 +1 @@ +9e6f99ed0c3d4d76a8c290308805cabf84aa7e5fb6dc2b06d973d9d8726fc4d8
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6097ff0565b..6c463a668d2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9359,39 +9359,39 @@ CREATE TABLE application_settings ( elasticsearch_indexed_file_size_limit_kb integer DEFAULT 1024 NOT NULL, enforce_namespace_storage_limit boolean DEFAULT false NOT NULL, container_registry_delete_tags_service_timeout integer DEFAULT 250 NOT NULL, - kroki_url character varying, - kroki_enabled boolean, - elasticsearch_client_request_timeout integer DEFAULT 0 NOT NULL, gitpod_enabled boolean DEFAULT false NOT NULL, gitpod_url text DEFAULT 'https://gitpod.io/'::text, + elasticsearch_client_request_timeout integer DEFAULT 0 NOT NULL, abuse_notification_email character varying, require_admin_approval_after_user_signup boolean DEFAULT true NOT NULL, help_page_documentation_base_url text, automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL, + container_registry_expiration_policies_worker_capacity integer DEFAULT 0 NOT NULL, encrypted_ci_jwt_signing_key text, encrypted_ci_jwt_signing_key_iv text, - container_registry_expiration_policies_worker_capacity integer DEFAULT 0 NOT NULL, - elasticsearch_analyzers_smartcn_enabled boolean DEFAULT false NOT NULL, - elasticsearch_analyzers_smartcn_search boolean DEFAULT false NOT NULL, - elasticsearch_analyzers_kuromoji_enabled boolean DEFAULT false NOT NULL, - elasticsearch_analyzers_kuromoji_search boolean DEFAULT false NOT NULL, secret_detection_token_revocation_enabled boolean DEFAULT false NOT NULL, secret_detection_token_revocation_url text, encrypted_secret_detection_token_revocation_token text, encrypted_secret_detection_token_revocation_token_iv text, + elasticsearch_analyzers_smartcn_enabled boolean DEFAULT false NOT NULL, + elasticsearch_analyzers_smartcn_search boolean DEFAULT false NOT NULL, + elasticsearch_analyzers_kuromoji_enabled boolean DEFAULT false NOT NULL, + elasticsearch_analyzers_kuromoji_search boolean DEFAULT false NOT NULL, + new_user_signups_cap integer, domain_denylist_enabled boolean DEFAULT false, domain_denylist text, domain_allowlist text, - new_user_signups_cap integer, encrypted_cloud_license_auth_token text, encrypted_cloud_license_auth_token_iv text, secret_detection_revocation_token_types_url text, cloud_license_enabled boolean DEFAULT false NOT NULL, + kroki_url text, + kroki_enabled boolean DEFAULT false NOT NULL, disable_feed_token boolean DEFAULT false NOT NULL, personal_access_token_prefix text, rate_limiting_response_text text, - invisible_captcha_enabled boolean DEFAULT false NOT NULL, container_registry_cleanup_tags_service_max_list_size integer DEFAULT 200 NOT NULL, + invisible_captcha_enabled boolean DEFAULT false NOT NULL, enforce_ssh_key_expiration boolean DEFAULT false NOT NULL, git_two_factor_session_expiry integer DEFAULT 15 NOT NULL, asset_proxy_allowlist text, @@ -9402,7 +9402,7 @@ CREATE TABLE application_settings ( in_product_marketing_emails_enabled boolean DEFAULT true 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_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), - CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)), + CONSTRAINT check_17d9558205 CHECK ((char_length(kroki_url) <= 1024)), CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)), @@ -9919,6 +9919,24 @@ CREATE SEQUENCE boards_epic_boards_id_seq ALTER SEQUENCE boards_epic_boards_id_seq OWNED BY boards_epic_boards.id; +CREATE TABLE boards_epic_list_user_preferences ( + id bigint NOT NULL, + user_id bigint NOT NULL, + epic_list_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + collapsed boolean DEFAULT false NOT NULL +); + +CREATE SEQUENCE boards_epic_list_user_preferences_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE boards_epic_list_user_preferences_id_seq OWNED BY boards_epic_list_user_preferences.id; + CREATE TABLE boards_epic_lists ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -18755,6 +18773,8 @@ ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass); +ALTER TABLE ONLY boards_epic_list_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_list_user_preferences_id_seq'::regclass); + ALTER TABLE ONLY boards_epic_lists ALTER COLUMN id SET DEFAULT nextval('boards_epic_lists_id_seq'::regclass); ALTER TABLE ONLY boards_epic_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_user_preferences_id_seq'::regclass); @@ -19834,6 +19854,9 @@ ALTER TABLE ONLY boards_epic_board_positions ALTER TABLE ONLY boards_epic_boards ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id); +ALTER TABLE ONLY boards_epic_list_user_preferences + ADD CONSTRAINT boards_epic_list_user_preferences_pkey PRIMARY KEY (id); + ALTER TABLE ONLY boards_epic_lists ADD CONSTRAINT boards_epic_lists_pkey PRIMARY KEY (id); @@ -21574,6 +21597,8 @@ CREATE INDEX index_boards_epic_board_positions_on_scoped_relative_position ON bo CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id); +CREATE INDEX index_boards_epic_list_user_preferences_on_epic_list_id ON boards_epic_list_user_preferences USING btree (epic_list_id); + CREATE INDEX index_boards_epic_lists_on_epic_board_id ON boards_epic_lists USING btree (epic_board_id); CREATE UNIQUE INDEX index_boards_epic_lists_on_epic_board_id_and_label_id ON boards_epic_lists USING btree (epic_board_id, label_id) WHERE (list_type = 1); @@ -22098,6 +22123,8 @@ CREATE INDEX index_environments_on_project_id_state_environment_type ON environm CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING btree (state, auto_stop_at) WHERE ((auto_stop_at IS NOT NULL) AND ((state)::text = 'available'::text)); +CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id); + CREATE INDEX index_epic_issues_on_epic_id ON epic_issues USING btree (epic_id); CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btree (epic_id, issue_id); @@ -24510,6 +24537,9 @@ ALTER TABLE ONLY milestones ALTER TABLE ONLY vulnerabilities ADD CONSTRAINT fk_959d40ad0a FOREIGN KEY (confirmed_by_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE ONLY boards_epic_list_user_preferences + ADD CONSTRAINT fk_95eac55851 FOREIGN KEY (epic_list_id) REFERENCES boards_epic_lists(id) ON DELETE CASCADE; + ALTER TABLE ONLY application_settings ADD CONSTRAINT fk_964370041d FOREIGN KEY (usage_stats_set_by_user_id) REFERENCES users(id) ON DELETE SET NULL; @@ -24810,6 +24840,9 @@ ALTER TABLE ONLY design_management_designs_versions ALTER TABLE ONLY analytics_devops_adoption_segments ADD CONSTRAINT fk_f5aa768998 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY boards_epic_list_user_preferences + ADD CONSTRAINT fk_f5f2fe5c1f FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY cluster_agents ADD CONSTRAINT fk_f7d43dee13 FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7342370f729..b1fa37e4d06 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -669,7 +669,7 @@ Represents a list for an issue board. | Field | Type | Description | | ----- | ---- | ----------- | | `assignee` | User | Assignee in the list. | -| `collapsed` | Boolean | Indicates if list is collapsed for this user. | +| `collapsed` | Boolean | Indicates if the list is collapsed for this user. | | `id` | ID! | ID (global ID) of the list. | | `issues` | IssueConnection | Board issues. | | `issuesCount` | Int | Count of issues in the list. | @@ -1971,6 +1971,7 @@ Represents an epic board list. | Field | Type | Description | | ----- | ---- | ----------- | +| `collapsed` | Boolean | Indicates if this list is collapsed for this user. | | `epics` | EpicConnection | List epics. | | `id` | BoardsEpicListID! | Global ID of the board list. | | `label` | Label | Label of the list. | @@ -5310,10 +5311,10 @@ State of a GitLab issue or merge request. | Value | Description | | ----- | ----------- | -| `all` | | -| `closed` | | -| `locked` | | -| `opened` | | +| `all` | All available | +| `closed` | In closed state | +| `locked` | Discussion has been locked | +| `opened` | In open state | ### IssueSort @@ -5353,10 +5354,10 @@ State of a GitLab issue. | Value | Description | | ----- | ----------- | -| `all` | | -| `closed` | | -| `locked` | | -| `opened` | | +| `all` | All available | +| `closed` | In closed state | +| `locked` | Discussion has been locked | +| `opened` | In open state | ### IssueStateEvent @@ -5496,11 +5497,11 @@ State of a GitLab merge request. | Value | Description | | ----- | ----------- | -| `all` | | -| `closed` | | -| `locked` | | +| `all` | All available | +| `closed` | In closed state | +| `locked` | Discussion has been locked | | `merged` | Merge Request has been merged | -| `opened` | | +| `opened` | In open state | ### MilestoneStateEnum diff --git a/doc/api/users.md b/doc/api/users.md index 76b0bb3491c..5b966d6ba87 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -53,6 +53,9 @@ For example: GET /users?username=jack_smith ``` +NOTE: +Username search is case insensitive. + In addition, you can filter users based on the states `blocked` and `active`. It does not support `active=false` or `blocked=false`. The list of billable users is the total number of users minus the blocked users. @@ -65,17 +68,33 @@ GET /users?active=true GET /users?blocked=true ``` +In addition, you can search for external users only with `external=true`. +It does not support `external=false`. + +```plaintext +GET /users?external=true +``` + GitLab supports bot users such as the [alert bot](../operations/incident_management/integrations.md) or the [support bot](../user/project/service_desk.md#support-bot-user). -To exclude these users from the users' list, you can use the parameter `exclude_internal=true` +You can exclude the following types of [internal users](../development/internal_users.md#internal-users) +from the users' list, with the `exclude_internal=true` parameter, ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241144) in GitLab 13.4). +- Alert bot +- Support bot + +However, this action does not exclude [project bot users](../user/project/settings/project_access_tokens.md#project-bot-users). + ```plaintext GET /users?exclude_internal=true ``` -NOTE: -Username search is case insensitive. +In addition, to exclude external users from the users' list, you can use the parameter `exclude_external=true`. + +```plaintext +GET /users?exclude_external=true +``` ### For admins @@ -217,10 +236,6 @@ For example: GET /users?extern_uid=1234567&provider=github ``` -Instance administrators can search for users who are external with: `/users?external=true` - -You cannot search for external users if you are not an instance administrator. - You can search users by creation date time range with: ```plaintext diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 2cac2e45e6a..ecc0aee771d 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -24,7 +24,7 @@ bundle exec rake gitlab:usage_data:generate_metrics_dictionary The Metrics Dictionary is based on the following metrics definition YAML files: -- [`config/metrics`]('https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics') +- [`config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics) - [`ee/config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/metrics) Each table includes a `milestone`, which corresponds to the GitLab version when the metric was released. @@ -21927,7 +21927,7 @@ Distinct count of users that imported issues into projects using CSV upload ## `usage_activity_by_stage.manage.issues_imported.fogbugz` -Distinct count of users that imported issues into projects using FogBugz +Distinct count of users that imported issues into projects using FogBugz | field | value | | --- | --- | @@ -22269,7 +22269,7 @@ Count of projects imported using manifst file ## `usage_activity_by_stage.manage.projects_imported.bitbucket` -Distinct count of users that imported projects from Bitbucket Cloud +Distinct count of users that imported projects from Bitbucket Cloud | field | value | | --- | --- | @@ -22288,7 +22288,7 @@ Distinct count of users that imported projects from Bitbucket Cloud ## `usage_activity_by_stage.manage.projects_imported.bitbucket_server` -Distinct count of users that imported projects from Bitbucket Server +Distinct count of users that imported projects from Bitbucket Server | field | value | | --- | --- | @@ -22307,7 +22307,7 @@ Distinct count of users that imported projects from Bitbucket Server ## `usage_activity_by_stage.manage.projects_imported.git` -Distinct count of users that imported projects using Import by URL +Distinct count of users that imported projects using Import by URL | field | value | | --- | --- | @@ -22326,7 +22326,7 @@ Distinct count of users that imported projects using Import by URL ## `usage_activity_by_stage.manage.projects_imported.gitea` -Distinct count of users that imported projects from Gitea +Distinct count of users that imported projects from Gitea | field | value | | --- | --- | @@ -22364,7 +22364,7 @@ Distinct count of users that imported projects from GitHub ## `usage_activity_by_stage.manage.projects_imported.gitlab` -Distinct count of users that imported projects from GitLab.com +Distinct count of users that imported projects from GitLab.com | field | value | | --- | --- | @@ -22383,7 +22383,7 @@ Distinct count of users that imported projects from GitLab.com ## `usage_activity_by_stage.manage.projects_imported.gitlab_project` -Distinct count of users that imported projects using Project Import/Export +Distinct count of users that imported projects using Project Import/Export | field | value | | --- | --- | @@ -22402,7 +22402,7 @@ Distinct count of users that imported projects using Project Import/Export ## `usage_activity_by_stage.manage.projects_imported.manifest` -Distinct count of users that imported projects using Manifest file +Distinct count of users that imported projects using Manifest file | field | value | | --- | --- | @@ -23143,7 +23143,7 @@ Total successful deployments ## `usage_activity_by_stage.secure.api_fuzzing_scans` -Counts API fuzzing jobs +Counts API fuzzing jobs | field | value | | --- | --- | @@ -23162,7 +23162,7 @@ Counts API fuzzing jobs ## `usage_activity_by_stage.secure.container_scanning_scans` -Counts container scanning jobs +Counts container scanning jobs | field | value | | --- | --- | @@ -23181,7 +23181,7 @@ Counts container scanning jobs ## `usage_activity_by_stage.secure.coverage_fuzzing_scans` -Counts fuzzing jobs +Counts fuzzing jobs | field | value | | --- | --- | @@ -23200,7 +23200,7 @@ Counts fuzzing jobs ## `usage_activity_by_stage.secure.dast_scans` -Counts dast jobs +Counts dast jobs | field | value | | --- | --- | @@ -23219,7 +23219,7 @@ Counts dast jobs ## `usage_activity_by_stage.secure.dependency_scanning_scans` -Counts dependency scanning jobs +Counts dependency scanning jobs | field | value | | --- | --- | @@ -23238,7 +23238,7 @@ Counts dependency scanning jobs ## `usage_activity_by_stage.secure.sast_scans` -Counts sast jobs +Counts sast jobs | field | value | | --- | --- | @@ -23257,7 +23257,7 @@ Counts sast jobs ## `usage_activity_by_stage.secure.secret_detection_scans` -Counts secret detection jobs +Counts secret detection jobs | field | value | | --- | --- | @@ -25746,7 +25746,7 @@ Missing description ## `usage_activity_by_stage_monthly.monitor.projects_with_alert_incidents` -Count of unique projects with an incident from an alert created in the last month +Count of unique projects with an incident from an alert created in the last month | field | value | | --- | --- | @@ -25784,7 +25784,7 @@ Missing description ## `usage_activity_by_stage_monthly.monitor.projects_with_incidents` -Count of unique projects with an incident created in the last month +Count of unique projects with an incident created in the last month | field | value | | --- | --- | diff --git a/doc/user/application_security/vulnerability_report/img/project_security_dashboard_dismissal_v13_9.png b/doc/user/application_security/vulnerability_report/img/project_security_dashboard_dismissal_v13_9.png Binary files differdeleted file mode 100644 index ef96cf31fa4..00000000000 --- a/doc/user/application_security/vulnerability_report/img/project_security_dashboard_dismissal_v13_9.png +++ /dev/null diff --git a/doc/user/application_security/vulnerability_report/img/project_security_dashboard_status_change_v13_9.png b/doc/user/application_security/vulnerability_report/img/project_security_dashboard_status_change_v13_9.png Binary files differnew file mode 100644 index 00000000000..f9f60810f20 --- /dev/null +++ b/doc/user/application_security/vulnerability_report/img/project_security_dashboard_status_change_v13_9.png diff --git a/doc/user/application_security/vulnerability_report/index.md b/doc/user/application_security/vulnerability_report/index.md index ce29962d743..292f99dfc10 100644 --- a/doc/user/application_security/vulnerability_report/index.md +++ b/doc/user/application_security/vulnerability_report/index.md @@ -50,12 +50,12 @@ The Activity filter behaves differently from the other Vulnerability Report filt Contents of the unfiltered vulnerability report can be exported using our [export feature](#export-vulnerabilities). -You can also dismiss vulnerabilities in the table: +You can also change the status of vulnerabilities in the table: -1. Select the checkbox for each vulnerability you want to dismiss. -1. In the menu that appears, select the reason for dismissal and click **Dismiss Selected**. +1. Select the checkbox for each vulnerability you want to update the status of. +1. In the dropdown that appears select the desired status, then select **Change status**. -![Project Vulnerability Report](img/project_security_dashboard_dismissal_v13_9.png) +![Project Vulnerability Report](img/project_security_dashboard_status_change_v13_9.png) ## Project Vulnerability Report diff --git a/lib/api/users.rb b/lib/api/users.rb index 3c7c9999731..9b394ad1cfd 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -82,6 +82,7 @@ module API optional :search, type: String, desc: 'Search for a username' optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' + optional :exclude_external, as: :non_external, type: Boolean, default: false, desc: 'Filters only non external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' optional :created_after, type: DateTime, desc: 'Return users created after the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time' @@ -97,7 +98,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get feature_category: :users do - authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) + authenticated_as_admin! if params[:extern_uid].present? && params[:provider].present? unless current_user&.admin? params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects) diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb index be5d9be3d64..178ebe0d4d4 100644 --- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb +++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb @@ -29,6 +29,8 @@ module Gitlab @stage = stage @query = query @params = params + @sort = params[:sort] || :end_event + @direction = params[:direction] || :desc end def serialized_records @@ -52,7 +54,7 @@ module Gitlab private - attr_reader :stage, :query, :params + attr_reader :stage, :query, :params, :sort, :direction def columns MAPPINGS.fetch(subject_class).fetch(:columns_for_select).map do |column_name| @@ -90,7 +92,7 @@ module Gitlab end def ordered_and_limited_query - order_by_end_event(query, columns).limit(MAX_RECORDS) + order_by(query, sort, direction, columns).limit(MAX_RECORDS) end def records diff --git a/lib/gitlab/analytics/cycle_analytics/sorting.rb b/lib/gitlab/analytics/cycle_analytics/sorting.rb new file mode 100644 index 00000000000..828879d466d --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/sorting.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + class Sorting + # rubocop: disable CodeReuse/ActiveRecord + SORTING_OPTIONS = { + end_event: { + asc: -> (query, stage) { query.reorder(stage.end_event.timestamp_projection.asc) }, + desc: -> (query, stage) { query.reorder(stage.end_event.timestamp_projection.desc) } + }.freeze, + duration: { + asc: -> (query, stage) { query.reorder(Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).asc) }, + desc: -> (query, stage) { query.reorder(Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).desc) } + }.freeze + }.freeze + # rubocop: enable CodeReuse/ActiveRecord, + + def self.apply(query, stage, sort, direction) + sort_lambda = SORTING_OPTIONS.dig(sort, direction) || SORTING_OPTIONS.dig(:end_event, :desc) + sort_lambda.call(query, stage) + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb index 80e426e6e17..777a8278e6e 100644 --- a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb +++ b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb @@ -24,8 +24,8 @@ module Gitlab end # rubocop: disable CodeReuse/ActiveRecord - def order_by_end_event(query, extra_columns_to_select = [:id]) - ordered_query = query.reorder(stage.end_event.timestamp_projection.desc) + def order_by(query, sort, direction, extra_columns_to_select = [:id]) + ordered_query = Gitlab::Analytics::CycleAnalytics::Sorting.apply(query, stage, sort, direction) # When filtering for more than one label, postgres requires the columns in ORDER BY to be present in the GROUP BY clause if requires_grouping? diff --git a/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb b/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb index 52b09e07fd5..dc31f995ae0 100644 --- a/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb +++ b/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb @@ -61,16 +61,12 @@ module Gitlab private def calculated_uuid - Gitlab::UUID.v5(uuid_components) - end - - def uuid_components - [ - category, - vulnerability_finding.primary_identifier.fingerprint, - vulnerability_finding.location_fingerprint, - project_id - ].join('-') + ::Security::VulnerabilityUUID.generate( + report_type: category, + primary_identifier_fingerprint: vulnerability_finding.primary_identifier.fingerprint, + location_fingerprint: vulnerability_finding.location_fingerprint, + project_id: project_id + ) end def finding_key diff --git a/lib/gitlab/usage/docs/templates/default.md.haml b/lib/gitlab/usage/docs/templates/default.md.haml index 86e93be66c7..ee8740ce400 100644 --- a/lib/gitlab/usage/docs/templates/default.md.haml +++ b/lib/gitlab/usage/docs/templates/default.md.haml @@ -13,7 +13,7 @@ The Metrics Dictionary is based on the following metrics definition YAML files: - - [`config/metrics`]('https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics') + - [`config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics) - [`ee/config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/metrics) Each table includes a `milestone`, which corresponds to the GitLab version when the metric diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 904aebaacfc..c9fb3b39247 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -348,6 +348,11 @@ msgid_plural "%d vulnerabilities dismissed" msgstr[0] "" msgstr[1] "" +msgid "%d vulnerability updated" +msgid_plural "%d vulnerabilities updated" +msgstr[0] "" +msgstr[1] "" + msgid "%d warning found:" msgid_plural "%d warnings found:" msgstr[0] "" @@ -2069,6 +2074,9 @@ msgstr "" msgid "AdminArea|Features" msgstr "" +msgid "AdminArea|Groups" +msgstr "" + msgid "AdminArea|Guest" msgstr "" @@ -5093,6 +5101,9 @@ msgstr "" msgid "BulkImport|Importing the group failed" msgstr "" +msgid "BulkImport|Name already exists." +msgstr "" + msgid "BulkImport|No parent" msgstr "" @@ -7683,6 +7694,9 @@ msgstr "" msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act" msgstr "" +msgid "ComplianceFramework|New Compliance Framework" +msgstr "" + msgid "ComplianceFramework|PCI-DSS" msgstr "" @@ -26402,6 +26416,9 @@ msgstr "" msgid "SecurityReports|All" msgstr "" +msgid "SecurityReports|Change status" +msgstr "" + msgid "SecurityReports|Comment added to '%{vulnerabilityName}'" msgstr "" @@ -26417,9 +26434,6 @@ msgstr "" msgid "SecurityReports|Create issue" msgstr "" -msgid "SecurityReports|Dismiss Selected" -msgstr "" - msgid "SecurityReports|Dismiss vulnerability" msgstr "" @@ -26456,9 +26470,6 @@ msgstr "" msgid "SecurityReports|Failed to get security report information. Please reload the page or try again later." msgstr "" -msgid "SecurityReports|False positive" -msgstr "" - msgid "SecurityReports|Fuzzing artifacts" msgstr "" @@ -26534,7 +26545,7 @@ msgstr "" msgid "SecurityReports|Select a project to add by using the project search field above." msgstr "" -msgid "SecurityReports|Select a reason" +msgid "SecurityReports|Set status" msgstr "" msgid "SecurityReports|Severity" @@ -26570,11 +26581,6 @@ msgstr "" msgid "SecurityReports|There was an error deleting the comment." msgstr "" -msgid "SecurityReports|There was an error dismissing %d vulnerability. Please try again later." -msgid_plural "SecurityReports|There was an error dismissing %d vulnerabilities. Please try again later." -msgstr[0] "" -msgstr[1] "" - msgid "SecurityReports|There was an error dismissing the vulnerabilities." msgstr "" @@ -26620,18 +26626,12 @@ msgstr "" msgid "SecurityReports|With issues" msgstr "" -msgid "SecurityReports|Won't fix / Accept risk" -msgstr "" - msgid "SecurityReports|You do not have sufficient permissions to access this report" msgstr "" msgid "SecurityReports|You must sign in as an authorized user to see this report" msgstr "" -msgid "SecurityReports|[No reason]" -msgstr "" - msgid "Security|Policies" msgstr "" diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index d9cc71106d5..b0f8b803141 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -12,7 +12,7 @@ RSpec.describe UsersFinder do it 'returns all users' do users = described_class.new(user).execute - expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user) + expect(users).to contain_exactly(user, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user) end it 'filters by username' do @@ -48,12 +48,18 @@ RSpec.describe UsersFinder do it 'filters by active users' do users = described_class.new(user, active: true).execute - expect(users).to contain_exactly(user, normal_user, omniauth_user, admin_user) + expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, admin_user) end - it 'returns no external users' do + it 'filters by external users' do users = described_class.new(user, external: true).execute + expect(users).to contain_exactly(external_user) + end + + it 'filters by non external users' do + users = described_class.new(user, non_external: true).execute + expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user) end @@ -71,7 +77,7 @@ RSpec.describe UsersFinder do it 'filters by non internal users' do users = described_class.new(user, non_internal: true).execute - expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, admin_user) + expect(users).to contain_exactly(user, normal_user, external_user, blocked_user, omniauth_user, admin_user) end it 'does not filter by custom attributes' do @@ -80,18 +86,18 @@ RSpec.describe UsersFinder do custom_attributes: { foo: 'bar' } ).execute - expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user) + expect(users).to contain_exactly(user, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user) end it 'orders returned results' do users = described_class.new(user, sort: 'id_asc').execute - expect(users).to eq([normal_user, admin_user, blocked_user, omniauth_user, internal_user, user]) + expect(users).to eq([normal_user, admin_user, blocked_user, external_user, omniauth_user, internal_user, user]) end it 'does not filter by admins' do users = described_class.new(user, admins: true).execute - expect(users).to contain_exactly(user, normal_user, admin_user, blocked_user, omniauth_user, internal_user) + expect(users).to contain_exactly(user, normal_user, external_user, admin_user, blocked_user, omniauth_user, internal_user) end end diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js index 074d6cf3786..7a83136e785 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -1,10 +1,15 @@ -import { GlButton, GlLink, GlFormInput } from '@gitlab/ui'; +import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { STATUSES } from '~/import_entities/constants'; import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; -import Select2Select from '~/vue_shared/components/select2_select.vue'; +import groupQuery from '~/import_entities/import_groups/graphql/queries/group.query.graphql'; import { availableNamespacesFixture } from '../graphql/fixtures'; +Vue.use(VueApollo); + const getFakeGroup = (status) => ({ web_url: 'https://fake.host/', full_path: 'fake_group_1', @@ -17,8 +22,12 @@ const getFakeGroup = (status) => ({ status, }); +const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group'; +const EXISTING_GROUP_PATH = 'existing-path'; + describe('import table row', () => { let wrapper; + let apolloProvider; let group; const findByText = (cmp, text) => { @@ -26,12 +35,27 @@ describe('import table row', () => { }; const findImportButton = () => findByText(GlButton, 'Import'); const findNameInput = () => wrapper.find(GlFormInput); - const findNamespaceDropdown = () => wrapper.find(Select2Select); + const findNamespaceDropdown = () => wrapper.find(GlDropdown); const createComponent = (props) => { + apolloProvider = createMockApollo([ + [ + groupQuery, + ({ fullPath }) => { + const existingGroup = + fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}` + ? { id: 1 } + : null; + return Promise.resolve({ data: { existingGroup } }); + }, + ], + ]); + wrapper = shallowMount(ImportTableRow, { + apolloProvider, propsData: { availableNamespaces: availableNamespacesFixture, + groupPathRegex: /.*/, ...props, }, }); @@ -49,15 +73,24 @@ describe('import table row', () => { }); it.each` - selector | sourceEvent | payload | event - ${findNamespaceDropdown} | ${'input'} | ${'demo'} | ${'update-target-namespace'} - ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'} - ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'} + selector | sourceEvent | payload | event + ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'} + ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'} `('invokes $event', ({ selector, sourceEvent, payload, event }) => { selector().vm.$emit(sourceEvent, payload); expect(wrapper.emitted(event)).toBeDefined(); expect(wrapper.emitted(event)[0][0]).toBe(payload); }); + + it('emits update-target-namespace when dropdown option is clicked', () => { + const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2); + const dropdownItemText = dropdownItem.text(); + + dropdownItem.vm.$emit('click'); + + expect(wrapper.emitted('update-target-namespace')).toBeDefined(); + expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(dropdownItemText); + }); }); describe('when entity status is NONE', () => { @@ -81,12 +114,12 @@ describe('import table row', () => { availableNamespaces: [], }); - const dropdownData = findNamespaceDropdown().props().options.data; - const noParentOption = dropdownData.find((o) => o.text === 'No parent'); - const existingGroupOption = dropdownData.find((o) => o.text === 'Existing groups'); + const items = findNamespaceDropdown() + .findAllComponents(GlDropdownItem) + .wrappers.map((w) => w.text()); - expect(noParentOption.id).toBe(''); - expect(existingGroupOption).toBeUndefined(); + expect(items[0]).toBe('No parent'); + expect(items).toHaveLength(1); }); it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => { @@ -95,12 +128,12 @@ describe('import table row', () => { availableNamespaces: availableNamespacesFixture, }); - const dropdownData = findNamespaceDropdown().props().options.data; - const noParentOption = dropdownData.find((o) => o.text === 'No parent'); - const existingGroupOption = dropdownData.find((o) => o.text === 'Existing groups'); + const [firstItem, ...rest] = findNamespaceDropdown() + .findAllComponents(GlDropdownItem) + .wrappers.map((w) => w.text()); - expect(noParentOption.id).toBe(''); - expect(existingGroupOption.children).toHaveLength(availableNamespacesFixture.length); + expect(firstItem).toBe('No parent'); + expect(rest).toHaveLength(availableNamespacesFixture.length); }); describe('when entity status is SCHEDULING', () => { @@ -137,4 +170,38 @@ describe('import table row', () => { expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true); }); }); + + describe('validations', () => { + it('Reports invalid group name when name is not matching regex', () => { + createComponent({ + group: { + ...getFakeGroup(STATUSES.NONE), + import_target: { + target_namespace: 'root', + new_name: 'very`bad`name', + }, + }, + groupPathRegex: /^[a-zA-Z]+$/, + }); + + expect(wrapper.text()).toContain('Please choose a group URL with no special characters.'); + }); + + it('Reports invalid group name if group already exists', async () => { + createComponent({ + group: { + ...getFakeGroup(STATUSES.NONE), + import_target: { + target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, + new_name: EXISTING_GROUP_PATH, + }, + }, + }); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(wrapper.text()).toContain('Name already exists.'); + }); + }); }); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 59330e58e26..14445132f48 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -43,6 +43,7 @@ describe('import table', () => { wrapper = shallowMount(ImportTable, { propsData: { sourceUrl: 'https://demo.host', + groupPathRegex: /.*/, }, stubs: { GlSprintf, diff --git a/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb new file mode 100644 index 00000000000..8f5be709a11 --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Analytics::CycleAnalytics::Sorting do + let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) } + + subject(:order_values) { described_class.apply(MergeRequest.joins(:metrics), stage, sort, direction).order_values } + + context 'when invalid sorting params are given' do + let(:sort) { :unknown_sort } + let(:direction) { :unknown_direction } + + it 'falls back to end_event DESC sorting' do + expect(order_values).to eq([stage.end_event.timestamp_projection.desc]) + end + end + + context 'sorting end_event' do + let(:sort) { :end_event } + + context 'direction desc' do + let(:direction) { :desc } + + specify do + expect(order_values).to eq([stage.end_event.timestamp_projection.desc]) + end + end + + context 'direction asc' do + let(:direction) { :asc } + + specify do + expect(order_values).to eq([stage.end_event.timestamp_projection.asc]) + end + end + end + + context 'sorting duration' do + let(:sort) { :duration } + + context 'direction desc' do + let(:direction) { :desc } + + specify do + expect(order_values).to eq([Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).desc]) + end + end + + context 'direction asc' do + let(:direction) { :asc } + + specify do + expect(order_values).to eq([Arel::Nodes::Subtraction.new(stage.end_event.timestamp_projection, stage.start_event.timestamp_projection).asc]) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb index 8e74935e127..07b1d99d333 100644 --- a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb @@ -27,12 +27,33 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityF let(:finding_1) { finding_creator.call(sast_report, location_fingerprint_1) } let(:finding_2) { finding_creator.call(dast_report, location_fingerprint_2) } let(:finding_3) { finding_creator.call(secret_detection_report, location_fingerprint_3) } - let(:uuid_1_components) { ['sast', identifier.fingerprint, location_fingerprint_1, project.id].join('-') } - let(:uuid_2_components) { ['dast', identifier.fingerprint, location_fingerprint_2, project.id].join('-') } - let(:uuid_3_components) { ['secret_detection', identifier.fingerprint, location_fingerprint_3, project.id].join('-') } - let(:expected_uuid_1) { Gitlab::UUID.v5(uuid_1_components) } - let(:expected_uuid_2) { Gitlab::UUID.v5(uuid_2_components) } - let(:expected_uuid_3) { Gitlab::UUID.v5(uuid_3_components) } + let(:expected_uuid_1) do + Security::VulnerabilityUUID.generate( + report_type: 'sast', + primary_identifier_fingerprint: identifier.fingerprint, + location_fingerprint: location_fingerprint_1, + project_id: project.id + ) + end + + let(:expected_uuid_2) do + Security::VulnerabilityUUID.generate( + report_type: 'dast', + primary_identifier_fingerprint: identifier.fingerprint, + location_fingerprint: location_fingerprint_2, + project_id: project.id + ) + end + + let(:expected_uuid_3) do + Security::VulnerabilityUUID.generate( + report_type: 'secret_detection', + primary_identifier_fingerprint: identifier.fingerprint, + location_fingerprint: location_fingerprint_3, + project_id: project.id + ) + end + let(:finding_creator) do -> (report_type, location_fingerprint) do findings.create!( diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 922d6b01b82..ad07ee1115b 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe List do it_behaves_like 'having unique enum values' it_behaves_like 'boards listable model', :list + it_behaves_like 'list_preferences_for user', :list, :list_id describe 'relationships' do it { is_expected.to belong_to(:board) } @@ -29,71 +30,4 @@ RSpec.describe List do expect(lists.where(board: board)).to match_array([backlog_list]) end end - - describe '#update_preferences_for' do - let(:user) { create(:user) } - let(:list) { create(:list) } - - context 'when user is present' do - context 'when there are no preferences for user' do - it 'creates new user preferences' do - expect { list.update_preferences_for(user, collapsed: true) }.to change { ListUserPreference.count }.by(1) - expect(list.preferences_for(user).collapsed).to eq(true) - end - end - - context 'when there are preferences for user' do - it 'updates user preferences' do - list.update_preferences_for(user, collapsed: false) - - expect { list.update_preferences_for(user, collapsed: true) }.not_to change { ListUserPreference.count } - expect(list.preferences_for(user).collapsed).to eq(true) - end - end - - context 'when user is nil' do - it 'does not create user preferences' do - expect { list.update_preferences_for(nil, collapsed: true) }.not_to change { ListUserPreference.count } - end - end - end - end - - describe '#preferences_for' do - let(:user) { create(:user) } - let(:list) { create(:list) } - - context 'when user is nil' do - it 'returns not persisted preferences' do - preferences = list.preferences_for(nil) - - expect(preferences.persisted?).to eq(false) - expect(preferences.list_id).to eq(list.id) - expect(preferences.user_id).to be_nil - end - end - - context 'when a user preference already exists' do - before do - list.update_preferences_for(user, collapsed: true) - end - - it 'loads preference for user' do - preferences = list.preferences_for(user) - - expect(preferences).to be_persisted - expect(preferences.collapsed).to eq(true) - end - end - - context 'when preferences for user does not exist' do - it 'returns not persisted preferences' do - preferences = list.preferences_for(user) - - expect(preferences.persisted?).to eq(false) - expect(preferences.user_id).to eq(user.id) - expect(preferences.list_id).to eq(list.id) - end - end - end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 44b7a38ed2b..4a7232d6316 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -320,6 +320,18 @@ RSpec.describe API::Users do expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/)) end + it "returns an array of external users" do + create(:user) + external_user = create(:user, external: true) + + get api("/users?external=true", user) + + expect(response).to match_response_schema('public_api/v4/user/basics') + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response[0]['id']).to eq(external_user.id) + end + it "returns one user" do get api("/users?username=#{omniauth_user.username}", user) diff --git a/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb new file mode 100644 index 00000000000..766aeac9476 --- /dev/null +++ b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'list_preferences_for user' do |list_factory, list_id_attribute| + subject { create(list_factory) } # rubocop:disable Rails/SaveBang + + let_it_be(:user) { create(:user) } + + describe '#preferences_for' do + context 'when user is nil' do + it 'returns not persisted preferences' do + preferences = subject.preferences_for(nil) + + expect(preferences).not_to be_persisted + expect(preferences[list_id_attribute]).to eq(subject.id) + expect(preferences.user_id).to be_nil + end + end + + context 'when a user preference already exists' do + before do + subject.update_preferences_for(user, collapsed: true) + end + + it 'loads preference for user' do + preferences = subject.preferences_for(user) + + expect(preferences).to be_persisted + expect(preferences.collapsed).to eq(true) + end + end + + context 'when preferences for user does not exist' do + it 'returns not persisted preferences' do + preferences = subject.preferences_for(user) + + expect(preferences).not_to be_persisted + expect(preferences.user_id).to eq(user.id) + expect(preferences.public_send(list_id_attribute)).to eq(subject.id) + end + end + end + + describe '#update_preferences_for' do + context 'when user is present' do + context 'when there are no preferences for user' do + it 'creates new user preferences' do + expect { subject.update_preferences_for(user, collapsed: true) }.to change { subject.preferences.count }.by(1) + expect(subject.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when there are preferences for user' do + it 'updates user preferences' do + subject.update_preferences_for(user, collapsed: false) + + expect { subject.update_preferences_for(user, collapsed: true) }.not_to change { subject.preferences.count } + expect(subject.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when user is nil' do + it 'does not create user preferences' do + expect { subject.update_preferences_for(nil, collapsed: true) }.not_to change { subject.preferences.count } + end + end + end + end +end |