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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql14
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue159
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/group.query.graphql5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js5
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss7
-rw-r--r--app/finders/users_finder.rb9
-rw-r--r--app/graphql/mutations/boards/lists/update.rb2
-rw-r--r--app/graphql/types/board_list_type.rb2
-rw-r--r--app/graphql/types/issuable_state_enum.rb8
-rw-r--r--app/models/concerns/boards/listable.rb20
-rw-r--r--app/models/list.rb20
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/security/vulnerability_uuid.rb9
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/import/bulk_imports/status.html.haml3
-rw-r--r--changelogs/unreleased/301018-cablett-collapsed-epic-board-list.yml5
-rw-r--r--changelogs/unreleased/admin-dashboard-typo.yml5
-rw-r--r--changelogs/unreleased/fix-include-external-users-in-user-search.yml5
-rw-r--r--config/metrics/counts_28d/20210216180524_projects_with_incidents.yml2
-rw-r--r--config/metrics/counts_28d/20210216180526_projects_with_alert_incidents.yml2
-rw-r--r--config/metrics/counts_all/20210216180707_gitlab_project.yml2
-rw-r--r--config/metrics/counts_all/20210216180709_gitlab.yml2
-rw-r--r--config/metrics/counts_all/20210216180713_bitbucket.yml2
-rw-r--r--config/metrics/counts_all/20210216180715_bitbucket_server.yml2
-rw-r--r--config/metrics/counts_all/20210216180716_gitea.yml2
-rw-r--r--config/metrics/counts_all/20210216180718_git.yml2
-rw-r--r--config/metrics/counts_all/20210216180720_manifest.yml2
-rw-r--r--config/metrics/counts_all/20210216180724_fogbugz.yml2
-rw-r--r--db/migrate/20210217101901_create_epic_list_user_preferences.rb20
-rw-r--r--db/migrate/20210222085529_add_epic_board_user_preference_user_fk.rb19
-rw-r--r--db/migrate/20210222085551_add_epic_board_user_preference_epic_list_fk.rb19
-rw-r--r--db/schema_migrations/202102171019011
-rw-r--r--db/schema_migrations/202102220855291
-rw-r--r--db/schema_migrations/202102220855511
-rw-r--r--db/structure.sql55
-rw-r--r--doc/api/graphql/reference/index.md27
-rw-r--r--doc/api/users.md29
-rw-r--r--doc/development/usage_ping/dictionary.md36
-rw-r--r--doc/user/application_security/vulnerability_report/img/project_security_dashboard_dismissal_v13_9.pngbin37066 -> 0 bytes
-rw-r--r--doc/user/application_security/vulnerability_report/img/project_security_dashboard_status_change_v13_9.pngbin0 -> 41154 bytes
-rw-r--r--doc/user/application_security/vulnerability_report/index.md8
-rw-r--r--lib/api/users.rb3
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb6
-rw-r--r--lib/gitlab/analytics/cycle_analytics/sorting.rb27
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb4
-rw-r--r--lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb16
-rw-r--r--lib/gitlab/usage/docs/templates/default.md.haml2
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/finders/users_finder_spec.rb20
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js101
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js1
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb58
-rw-r--r--spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb33
-rw-r--r--spec/models/list_spec.rb68
-rw-r--r--spec/requests/api/users_spec.rb12
-rw-r--r--spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb68
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
deleted file mode 100644
index ef96cf31fa4..00000000000
--- a/doc/user/application_security/vulnerability_report/img/project_security_dashboard_dismissal_v13_9.png
+++ /dev/null
Binary files differ
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
new file mode 100644
index 00000000000..f9f60810f20
--- /dev/null
+++ b/doc/user/application_security/vulnerability_report/img/project_security_dashboard_status_change_v13_9.png
Binary files differ
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