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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-15 12:10:30 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-15 12:10:30 +0300
commitcd5f4619db234c00d0c01ed63bb92df6c10b0fd3 (patch)
tree09ef12a2231c616d4f747b9d5b83597ae179bb06
parent024f77efd68833bb78540ff9b4c7b4ec4b9dfe39 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/database/avoid_inheritance_column.yml6
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue10
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue47
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue78
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue11
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue12
-rw-r--r--app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue7
-rw-r--r--app/assets/javascripts/service_desk/components/info_banner.vue2
-rw-r--r--app/assets/stylesheets/pages/projects.scss13
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb18
-rw-r--r--app/models/concerns/enum_inheritance.rb58
-rw-r--r--app/serializers/admin/abuse_report_details_entity.rb8
-rw-r--r--app/services/admin/abuse_report_update_service.rb91
-rw-r--r--app/services/admin/abuse_reports/moderate_user_service.rb93
-rw-r--r--app/views/admin/application_settings/appearances/_form.html.haml267
-rw-r--r--app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml51
-rw-r--r--app/views/admin/deploy_keys/new.html.haml14
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml12
-rw-r--r--app/views/projects/issues/service_desk.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml51
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml12
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml8
-rw-r--r--config/routes/admin.rb6
-rw-r--r--doc/api/member_roles.md8
-rw-r--r--doc/api/project_job_token_scopes.md17
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/ci/large_repositories/index.md1
-rw-r--r--doc/development/contributing/design.md8
-rw-r--r--doc/development/database/single_table_inheritance.md60
-rw-r--r--doc/development/fe_guide/dark_mode.md3
-rw-r--r--doc/development/namespaces.md302
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--doc/user/profile/preferences.md71
-rw-r--r--locale/gitlab.pot8
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb5
-rw-r--r--qa/qa/resource/deploy_key.rb1
-rw-r--r--rubocop/cop/database/avoid_inheritance_column.rb20
-rw-r--r--spec/features/issues/service_desk_spec.rb2
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb11
-rw-r--r--spec/fixtures/lib/backup/design_repo.refs2
-rw-r--r--spec/fixtures/lib/backup/personal_snippet_repo.refs2
-rw-r--r--spec/fixtures/lib/backup/project_repo.refs2
-rw-r--r--spec/fixtures/lib/backup/project_snippet_repo.refs2
-rw-r--r--spec/fixtures/lib/backup/wiki_repo.refs2
-rw-r--r--spec/frontend/admin/abuse_report/components/report_actions_spec.js31
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js1
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js6
-rw-r--r--spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js9
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb26
-rw-r--r--spec/models/concerns/enum_inheritance_spec.rb97
-rw-r--r--spec/requests/admin/abuse_reports_controller_spec.rb24
-rw-r--r--spec/rubocop/cop/database/avoid_inheritance_column_spec.rb23
-rw-r--r--spec/serializers/admin/abuse_report_details_entity_spec.rb3
-rw-r--r--spec/services/admin/abuse_reports/moderate_user_service_spec.rb (renamed from spec/services/admin/abuse_report_update_service_spec.rb)2
55 files changed, 1154 insertions, 480 deletions
diff --git a/.rubocop_todo/database/avoid_inheritance_column.yml b/.rubocop_todo/database/avoid_inheritance_column.yml
new file mode 100644
index 00000000000..ff4519c5568
--- /dev/null
+++ b/.rubocop_todo/database/avoid_inheritance_column.yml
@@ -0,0 +1,6 @@
+---
+Database/AvoidInheritanceColumn:
+ Exclude:
+ - 'app/models/event.rb'
+ - 'app/models/integration.rb'
+ - 'lib/gitlab/background_migration/backfill_project_repositories.rb'
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
index 57d5d46ceb4..92478e10289 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -95,10 +95,12 @@ export default {
return;
}
- axios
- .put(this.report.updatePath, this.form)
- .then(this.handleResponse)
- .catch(this.handleError);
+ // TODO: In 16.4 use moderateUserPath without falling back to using updatePath
+ // See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
+ const { moderateUserPath, updatePath } = this.report;
+ const path = moderateUserPath || updatePath;
+
+ axios.put(path, this.form).then(this.handleResponse).catch(this.handleError);
},
handleResponse({ data }) {
this.toggleActionsDrawer();
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 4860215d8f2..ec17bbea48f 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -14,8 +14,9 @@ export default {
ConfirmModal,
KeysPanel,
NavigationTabs,
- GlLoadingIcon,
+ GlButton,
GlIcon,
+ GlLoadingIcon,
},
props: {
endpoint: {
@@ -42,6 +43,10 @@ export default {
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
},
+ i18n: {
+ loading: s__('DeployKeys|Loading deploy keys'),
+ addButton: s__('DeployKeys|Add new key'),
+ },
computed: {
tabs() {
return Object.keys(this.$options.scopes).map((scope) => {
@@ -132,23 +137,41 @@ export default {
</script>
<template>
- <div class="gl-mb-3 deploy-keys">
+ <div class="deploy-keys">
<confirm-modal :visible="confirmModalVisible" @remove="removeKey" @cancel="cancel" />
<gl-loading-icon
v-if="isLoading && !hasKeys"
- :label="s__('DeployKeys|Loading deploy keys')"
- size="lg"
+ :label="$options.i18n.loading"
+ size="sm"
+ class="gl-m-5"
/>
<template v-else-if="hasKeys">
- <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
- <div class="fade-left">
- <gl-icon name="chevron-lg-left" :size="12" />
- </div>
- <div class="fade-right">
- <gl-icon name="chevron-lg-right" :size="12" />
+ <div class="gl-new-card-header gl-align-items-center gl-pt-0 gl-pb-0 gl-pl-0">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-b-0">
+ <div class="fade-left">
+ <gl-icon name="chevron-lg-left" :size="12" />
+ </div>
+ <div class="fade-right">
+ <gl-icon name="chevron-lg-right" :size="12" />
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ scope="deployKeys"
+ class="gl-rounded-lg"
+ @onChangeTab="onChangeTab"
+ />
</div>
- <navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" />
+ <div class="gl-new-card-actions">
+ <gl-button
+ size="small"
+ class="js-toggle-button js-toggle-content"
+ data-testid="add-new-deploy-key-button"
+ >
+ {{ $options.i18n.addButton }}
+ </gl-button>
+ </div>
</div>
<keys-panel
:project-id="projectId"
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 79c45553659..16c745d8cff 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlIcon, GlLink, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { GlBadge, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { head, tail } from 'lodash';
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -10,9 +10,9 @@ import ActionBtn from './action_btn.vue';
export default {
components: {
ActionBtn,
+ GlBadge,
GlButton,
GlIcon,
- GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -111,12 +111,21 @@ export default {
</script>
<template>
- <div class="gl-responsive-table-row deploy-key">
+ <div
+ class="gl-responsive-table-row gl-align-items-flex-start deploy-key gl-bg-gray-10 gl-md-pl-5 gl-md-pr-5 gl-border-gray-100!"
+ >
<div class="table-section section-40">
- <div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div>
+ <div
+ role="rowheader"
+ class="table-mobile-header gl-align-self-start gl-font-weight-bold gl-text-gray-700"
+ >
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
<div class="table-mobile-content" data-testid="key-container">
- <strong class="title" data-testid="key-title-content"> {{ deployKey.title }} </strong>
- <dl>
+ <p class="title gl-font-weight-semibold gl-text-gray-700" data-testid="key-title-content">
+ {{ deployKey.title }}
+ </p>
+ <dl class="gl-font-sm gl-mb-0">
<dt>{{ __('SHA256') }}</dt>
<dd class="fingerprint" data-testid="key-sha256-fingerprint-content">
{{ deployKey.fingerprint_sha256 }}
@@ -133,53 +142,62 @@ export default {
</div>
</div>
<div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Project usage') }}</div>
- <div class="table-mobile-content deploy-project-list">
+ <div role="rowheader" class="table-mobile-header gl-font-weight-bold gl-text-gray-700">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div class="table-mobile-content deploy-project-list gl-display-flex gl-flex-wrap">
<template v-if="projects.length > 0">
- <gl-link
+ <gl-badge
v-gl-tooltip
:title="projectTooltipTitle(firstProject)"
- class="label deploy-project-label"
+ :icon="firstProject.can_push ? 'lock-open' : 'lock'"
+ class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate"
>
- <span> {{ firstProject.project.full_name }} </span>
- <gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
- </gl-link>
- <gl-link
+ <span class="gl-text-truncate">{{ firstProject.project.full_name }}</span>
+ </gl-badge>
+
+ <gl-badge
v-if="isExpandable"
v-gl-tooltip
:title="restProjectsTooltip"
- class="label deploy-project-label"
- @click="toggleExpanded"
+ class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate"
+ href="#"
+ @click.native="toggleExpanded"
>
- <span>{{ restProjectsLabel }}</span>
- </gl-link>
- <gl-link
+ <span class="gl-text-truncate">{{ restProjectsLabel }}</span>
+ </gl-badge>
+
+ <gl-badge
v-for="deployKeysProject in restProjects"
v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
v-gl-tooltip
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
- class="label deploy-project-label"
+ :icon="deployKeysProject.can_push ? 'lock-open' : 'lock'"
+ class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate"
>
- <span> {{ deployKeysProject.project.full_name }} </span>
- <gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
- </gl-link>
+ <span class="gl-text-truncate">{{ deployKeysProject.project.full_name }}</span>
+ </gl-badge>
</template>
- <span v-else class="text-secondary">{{ __('None') }}</span>
+ <span v-else class="gl-text-secondary">{{ __('None') }}</span>
</div>
</div>
<div class="table-section section-15">
- <div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div>
- <div class="table-mobile-content text-secondary key-created-at">
+ <div role="rowheader" class="table-mobile-header gl-font-weight-bold gl-text-gray-700">
+ {{ __('Created') }}
+ </div>
+ <div class="table-mobile-content gl-text-gray-700 key-created-at">
<span v-gl-tooltip :title="tooltipTitle(deployKey.created_at)">
<gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
</span>
</div>
</div>
<div class="table-section section-15">
- <div role="rowheader" class="table-mobile-header">{{ __('Expires') }}</div>
- <div class="table-mobile-content text-secondary key-expires-at">
+ <div role="rowheader" class="table-mobile-header gl-font-weight-bold gl-text-gray-700">
+ {{ __('Expires') }}
+ </div>
+ <div class="table-mobile-content gl-text-gray-700 key-expires-at">
<span
v-if="deployKey.expires_at"
v-gl-tooltip
@@ -214,7 +232,7 @@ export default {
:deploy-key="deployKey"
:title="__('Remove')"
:aria-label="__('Remove')"
- category="primary"
+ category="secondary"
variant="danger"
icon="remove"
type="remove"
@@ -229,7 +247,7 @@ export default {
type="disable"
data-container="body"
icon="cancel"
- category="primary"
+ category="secondary"
variant="danger"
/>
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index e04cbbe72b9..dac63188aa5 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -28,9 +28,12 @@ export default {
</script>
<template>
- <div class="deploy-keys-panel table-holder">
+ <div class="deploy-keys-panel table-holder gl-bg-white gl-rounded-lg">
<template v-if="keys.length > 0">
- <div role="row" class="gl-responsive-table-row table-row-header">
+ <div
+ role="row"
+ class="gl-responsive-table-row table-row-header gl-font-base gl-font-weight-bold gl-text-gray-900 gl-md-pl-5 gl-md-pr-5 gl-bg-gray-10 gl-border-gray-100!"
+ >
<div role="rowheader" class="table-section section-40">
{{ s__('DeployKeys|Deploy key') }}
</div>
@@ -50,8 +53,8 @@ export default {
:project-id="projectId"
/>
</template>
- <div v-else class="settings-message text-center gl-mt-5">
- {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
+ <div v-else class="gl-new-card-empty gl-bg-gray-10 gl-text-center gl-p-5">
+ {{ s__('DeployKeys|No deploy keys found, start by adding a new one above.') }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
index 82302ec4602..4e5d6b0ce6c 100644
--- a/app/assets/javascripts/nav/components/new_nav_toggle.vue
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -68,10 +68,15 @@ export default {
<gl-disclosure-dropdown-item v-if="newNavigation" @action="toggleNav">
<div class="gl-new-dropdown-item-content">
<div
- class="gl-new-dropdown-item-text-wrapper gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-py-2!"
+ class="gl-new-dropdown-item-text-wrapper gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-py-2! gl-gap-3"
>
{{ $options.i18n.toggleMenuItemLabel }}
- <gl-toggle :value="isEnabled" :label="$options.i18n.toggleLabel" label-position="hidden" />
+ <gl-toggle
+ class="gl-flex-grow-0!"
+ :value="isEnabled"
+ :label="$options.i18n.toggleLabel"
+ label-position="hidden"
+ />
</div>
</div>
</gl-disclosure-dropdown-item>
@@ -84,11 +89,12 @@ export default {
</div>
<div
- class="menu-item gl-cursor-pointer gl-display-flex! gl-justify-content-space-between gl-align-items-center"
+ class="menu-item gl-cursor-pointer gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-gap-3"
@click.prevent.stop="toggleNav"
>
{{ $options.i18n.toggleMenuItemLabel }}
<gl-toggle
+ class="gl-flex-grow-0!"
:value="isEnabled"
:label="$options.i18n.toggleLabel"
label-position="hidden"
diff --git a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue b/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
index 0679d31a8b8..9dbed2c2579 100644
--- a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
@@ -1,6 +1,5 @@
<script>
import { GlEmptyState, GlLink } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
import {
noIssuesSignedOutButtonText,
infoBannerTitle,
@@ -17,7 +16,6 @@ export default {
infoBannerAdminNote,
learnMore,
},
- serviceDeskHelpPagePath: helpPagePath('user/project/service_desk/index'),
components: {
GlEmptyState,
GlLink,
@@ -29,6 +27,7 @@ export default {
'canAdminIssues',
'isServiceDeskEnabled',
'serviceDeskEmailAddress',
+ 'serviceDeskHelpPath',
],
computed: {
canSeeEmailAddress() {
@@ -50,7 +49,7 @@ export default {
{{ $options.i18n.infoBannerAdminNote }} <br /><code>{{ serviceDeskEmailAddress }}</code>
</p>
<p>{{ $options.i18n.infoBannerUserNote }}</p>
- <gl-link :href="$options.serviceDeskHelpPagePath" target="_blank">
+ <gl-link :href="serviceDeskHelpPath">
{{ $options.i18n.learnMore }}
</gl-link>
</template>
@@ -67,7 +66,7 @@ export default {
>
<template #description>
<p>{{ $options.i18n.infoBannerUserNote }}</p>
- <gl-link :href="$options.serviceDeskHelpPagePath">
+ <gl-link :href="serviceDeskHelpPath">
{{ $options.i18n.learnMore }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/service_desk/components/info_banner.vue b/app/assets/javascripts/service_desk/components/info_banner.vue
index 8aaced839a5..5667ee2f31d 100644
--- a/app/assets/javascripts/service_desk/components/info_banner.vue
+++ b/app/assets/javascripts/service_desk/components/info_banner.vue
@@ -51,7 +51,7 @@ export default {
</p>
<p>
{{ $options.i18n.infoBannerUserNote }}
- <gl-link :href="serviceDeskHelpPath" target="_blank">{{ $options.i18n.learnMore }}</gl-link
+ <gl-link :href="serviceDeskHelpPath">{{ $options.i18n.learnMore }}</gl-link
>.
</p>
<p v-if="canEnableServiceDesk" class="gl-mt-3">
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 71936f37333..b9cae28537d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -65,7 +65,6 @@
white-space: normal;
}
- .deploy-project-label,
.key-created-at {
svg {
vertical-align: text-top;
@@ -83,18 +82,6 @@
.deploy-project-list {
margin-bottom: -$gl-padding-4;
-
- a.deploy-project-label {
- margin-right: $gl-padding-4;
- margin-bottom: $gl-padding-4;
- color: $gl-text-color-secondary;
- background-color: $gray-50;
- line-height: $gl-btn-line-height;
-
- &:hover {
- color: $blue-600;
- }
- }
}
.vs-public {
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 6b998c3d494..329c4e4921a 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -4,7 +4,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
feature_category :insider_threat
before_action :set_status_param, only: :index, if: -> { Feature.enabled?(:abuse_reports_list) }
- before_action :find_abuse_report, only: [:show, :update, :destroy]
+ before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
def index
@abuse_reports = AbuseReportsFinder.new(params).execute
@@ -12,8 +12,22 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def show; end
+ # Kept for backwards compatibility.
+ # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
+ # In 16.4 remove or re-use this endpoint after frontend has migrated to using moderate_user endpoint
def update
- response = Admin::AbuseReportUpdateService.new(@abuse_report, current_user, permitted_params).execute
+ response = Admin::AbuseReports::ModerateUserService.new(@abuse_report, current_user, permitted_params).execute
+
+ if response.success?
+ render json: { message: response.message }
+ else
+ render json: { message: response.message }, status: :unprocessable_entity
+ end
+ end
+
+ def moderate_user
+ response = Admin::AbuseReports::ModerateUserService.new(@abuse_report, current_user, permitted_params).execute
+
if response.success?
render json: { message: response.message }
else
diff --git a/app/models/concerns/enum_inheritance.rb b/app/models/concerns/enum_inheritance.rb
new file mode 100644
index 00000000000..1df1f3d43fd
--- /dev/null
+++ b/app/models/concerns/enum_inheritance.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module EnumInheritance
+ # == STI through Enum
+ #
+ # WARNING: Usage of STI is heavily discouraged: https://docs.gitlab.com/ee/development/database/single_table_inheritance.html
+ #
+ # Active Record allows definition of STI through the <tt>Base.inheritance_column</tt>. However, this stores the class
+ # name as string into the record, which is heavy and unnecessary. EnumInheritance adapts ActiveRecord to use an enum
+ # instead.
+ #
+ # Details:
+ # - Correct class mapping is specified in the <tt>self.sti_type_map<\tt>, which maps the symbol of the type to
+ # a fully classified class as string.
+ # - If the type passed does not have an specified class, then the class will be the base class
+ #
+ # Example
+ # class Animal
+ # include EnumInheritable
+ #
+ # enum animal_type: {
+ # dog: 1,
+ # cat: 2,
+ # bird: 3
+ # }
+ #
+ # def self.inheritance_column_to_class_map = {
+ # dog: 'Animals::Dog',
+ # cat: 'Animals::Cat'
+ # }
+ #
+ # def self.inheritance_column = 'animal_type'
+ # end
+ #
+ # class Animals::Dog < Animal; end
+ # class Animals::Cat < Animal; end
+ extend ActiveSupport::Concern
+
+ included do
+ def self.sti_class_to_enum_map = inheritance_column_to_class_map.invert
+ end
+
+ class_methods do
+ extend ::Gitlab::Utils::Override
+
+ def inheritance_column_to_class_map = {}.freeze
+
+ override :sti_class_for
+ def sti_class_for(type_name)
+ inheritance_column_to_class_map[type_name.to_sym]&.constantize || base_class
+ end
+
+ override :sti_name
+ def sti_name
+ sti_class_to_enum_map[name].to_s
+ end
+ end
+end
diff --git a/app/serializers/admin/abuse_report_details_entity.rb b/app/serializers/admin/abuse_report_details_entity.rb
index f0e84fc44d2..3efb8508e5e 100644
--- a/app/serializers/admin/abuse_report_details_entity.rb
+++ b/app/serializers/admin/abuse_report_details_entity.rb
@@ -79,9 +79,17 @@ module Admin
expose :reported_content, as: :content
expose :reported_from_url, as: :url
expose :screenshot_path, as: :screenshot
+
+ # Kept for backwards compatibility.
+ # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
+ # In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
expose :update_path do |report|
admin_abuse_report_path(report)
end
+
+ expose :moderate_user_path do |report|
+ moderate_user_admin_abuse_report_path(report)
+ end
end
end
end
diff --git a/app/services/admin/abuse_report_update_service.rb b/app/services/admin/abuse_report_update_service.rb
deleted file mode 100644
index 12cf8bf14a8..00000000000
--- a/app/services/admin/abuse_report_update_service.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-module Admin
- class AbuseReportUpdateService < BaseService
- attr_reader :abuse_report, :params, :current_user, :action
-
- def initialize(abuse_report, current_user, params)
- @abuse_report = abuse_report
- @current_user = current_user
- @params = params
- @action = determine_action
- end
-
- def execute
- return ServiceResponse.error(message: 'Admin is required') unless current_user&.can_admin_all_resources?
- return ServiceResponse.error(message: 'Action is required') unless action.present?
-
- result = perform_action
- if result[:status] == :success
- event = close_report_and_record_event
- ServiceResponse.success(message: event.success_message)
- else
- ServiceResponse.error(message: result[:message])
- end
- end
-
- private
-
- def determine_action
- action = params[:user_action]
- if action.in?(ResourceEvents::AbuseReportEvent.actions.keys)
- action.to_sym
- elsif close_report?
- :close_report
- end
- end
-
- def perform_action
- case action
- when :ban_user then ban_user
- when :block_user then block_user
- when :delete_user then delete_user
- when :close_report then close_report
- end
- end
-
- def ban_user
- Users::BanService.new(current_user).execute(abuse_report.user)
- end
-
- def block_user
- Users::BlockService.new(current_user).execute(abuse_report.user)
- end
-
- def delete_user
- abuse_report.user.delete_async(deleted_by: current_user)
- success
- end
-
- def close_report
- return error('Report already closed') if abuse_report.closed?
-
- abuse_report.closed!
- success
- end
-
- def close_report_and_record_event
- event = action
-
- if close_report? && action != :close_report
- close_report
- event = "#{action}_and_close_report"
- end
-
- record_event(event)
- end
-
- def close_report?
- params[:close].to_s == 'true'
- end
-
- def record_event(action)
- reason = params[:reason]
- unless reason.in?(ResourceEvents::AbuseReportEvent.reasons.keys)
- reason = ResourceEvents::AbuseReportEvent.reasons[:other]
- end
-
- abuse_report.events.create(action: action, user: current_user, reason: reason, comment: params[:comment])
- end
- end
-end
diff --git a/app/services/admin/abuse_reports/moderate_user_service.rb b/app/services/admin/abuse_reports/moderate_user_service.rb
new file mode 100644
index 00000000000..da61a4dc8f6
--- /dev/null
+++ b/app/services/admin/abuse_reports/moderate_user_service.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Admin
+ module AbuseReports
+ class ModerateUserService < BaseService
+ attr_reader :abuse_report, :params, :current_user, :action
+
+ def initialize(abuse_report, current_user, params)
+ @abuse_report = abuse_report
+ @current_user = current_user
+ @params = params
+ @action = determine_action
+ end
+
+ def execute
+ return ServiceResponse.error(message: 'Admin is required') unless current_user&.can_admin_all_resources?
+ return ServiceResponse.error(message: 'Action is required') unless action.present?
+
+ result = perform_action
+ if result[:status] == :success
+ event = close_report_and_record_event
+ ServiceResponse.success(message: event.success_message)
+ else
+ ServiceResponse.error(message: result[:message])
+ end
+ end
+
+ private
+
+ def determine_action
+ action = params[:user_action]
+ if action.in?(ResourceEvents::AbuseReportEvent.actions.keys)
+ action.to_sym
+ elsif close_report?
+ :close_report
+ end
+ end
+
+ def perform_action
+ case action
+ when :ban_user then ban_user
+ when :block_user then block_user
+ when :delete_user then delete_user
+ when :close_report then close_report
+ end
+ end
+
+ def ban_user
+ Users::BanService.new(current_user).execute(abuse_report.user)
+ end
+
+ def block_user
+ Users::BlockService.new(current_user).execute(abuse_report.user)
+ end
+
+ def delete_user
+ abuse_report.user.delete_async(deleted_by: current_user)
+ success
+ end
+
+ def close_report
+ return error('Report already closed') if abuse_report.closed?
+
+ abuse_report.closed!
+ success
+ end
+
+ def close_report_and_record_event
+ event = action
+
+ if close_report? && action != :close_report
+ close_report
+ event = "#{action}_and_close_report"
+ end
+
+ record_event(event)
+ end
+
+ def close_report?
+ params[:close].to_s == 'true'
+ end
+
+ def record_event(action)
+ reason = params[:reason]
+ unless reason.in?(ResourceEvents::AbuseReportEvent.reasons.keys)
+ reason = ResourceEvents::AbuseReportEvent.reasons[:other]
+ end
+
+ abuse_report.events.create(action: action, user: current_user, reason: reason, comment: params[:comment])
+ end
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/appearances/_form.html.haml b/app/views/admin/application_settings/appearances/_form.html.haml
index fb5c320268e..672af002e5e 100644
--- a/app/views/admin/application_settings/appearances/_form.html.haml
+++ b/app/views/admin/application_settings/appearances/_form.html.haml
@@ -3,146 +3,143 @@
= gitlab_ui_form_for @appearance, url: admin_application_settings_appearances_path, html: { class: 'gl-mt-3' } do |f|
= form_errors(@appearance)
- .row
- .col-lg-4
- %h4.gl-mt-0= _('Navigation bar')
-
- .col-lg-8
- .form-group
- = f.label :header_logo, _('Header logo'), class: 'col-form-label gl-pt-0'
- %p
- - if @appearance.header_logo?
- = image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: header_logos_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Header logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove header logo') } }) do
- = _('Remove header logo')
- %hr
- = f.hidden_field :header_logo_cache
- = f.file_field :header_logo, class: "", accept: 'image/*'
- .form-text.text-muted
- = _('Maximum file size is 1MB. Pages are optimized for a 24px tall header logo')
- %hr
- .row
- .col-lg-4
- %h4.gl-mt-0 Favicon
-
- .col-lg-8
- .form-group
- = f.label :favicon, _('Favicon'), class: 'col-form-label gl-pt-0'
- %p
- - if @appearance.favicon?
- = image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: favicon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Favicon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove favicon') } }) do
- = _('Remove favicon')
- %hr
- = f.hidden_field :favicon_cache
- = f.file_field :favicon, class: '', accept: 'image/*'
- .form-text.text-muted
- = _("Maximum file size is 1 MB. Image size must be 32 x 32 pixels. Allowed image formats are %{favicon_extension_allowlist}.") % { favicon_extension_allowlist: favicon_extension_allowlist }
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0= _('Navigation bar')
+
+ .form-group
+ = f.label :header_logo, _('Header logo'), class: 'col-form-label gl-pt-0'
+ %p
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: header_logos_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Header logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove header logo') } }) do
+ = _('Remove header logo')
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: "", accept: 'image/*'
+ .form-text.text-muted
+ = _('Maximum file size is 1MB. Pages are optimized for a 24px tall header logo')
+
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0 Favicon
+
+ .form-group
+ = f.label :favicon, _('Favicon'), class: 'col-form-label gl-pt-0'
+ %p
+ - if @appearance.favicon?
+ = image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
%br
- = _("Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.")
+ = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: favicon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Favicon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove favicon') } }) do
+ = _('Remove favicon')
+ %hr
+ = f.hidden_field :favicon_cache
+ = f.file_field :favicon, class: '', accept: 'image/*'
+ .form-text.text-muted
+ = _("Maximum file size is 1 MB. Image size must be 32 x 32 pixels. Allowed image formats are %{favicon_extension_allowlist}.") % { favicon_extension_allowlist: favicon_extension_allowlist }
+ %br
+ = _("Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.")
= render partial: 'admin/application_settings/appearances/system_header_footer_form', locals: { form: f }
- %hr
- .row
- .col-lg-4
- %h4.gl-mt-0= _('Sign in/Sign up pages')
-
- .col-lg-8
- .form-group
- = f.label :title, class: 'col-form-label'
- = f.text_field :title, class: "form-control gl-form-input"
- .form-group
- = f.label :description, class: 'col-form-label'
- = f.text_area :description, class: "form-control gl-form-input", rows: 10
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0= _('Sign in/Sign up pages')
+
+ .form-group
+ = f.label :title, class: 'col-form-label'
+ = f.text_field :title, class: "form-control gl-form-input"
+ .form-group
+ = f.label :description, class: 'col-form-label'
+ = f.text_area :description, class: "form-control gl-form-input", rows: 10
+ .form-text.text-muted
+ = parsed_with_gfm
+ .form-group
+ = f.label :logo, class: 'col-form-label gl-pt-0'
+ %p
+ - if @appearance.logo?
+ = image_tag @appearance.logo_path, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: logo_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove logo') } }) do
+ = _('Remove logo')
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: "", accept: 'image/*'
+ .form-text.text-muted
+ = _('Maximum file size is 1 MB. Pages are optimized for a 128x128 px logo.')
+
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0= _('Progressive Web App (PWA)')
+
+ .form-group
+ = f.label _("Name"), class: 'col-form-label'
+ = f.text_field :pwa_name, class: "form-control gl-form-input"
+ .form-group
+ = f.label _("Short name"), class: 'col-form-label'
+ = f.text_field :pwa_short_name, class: "form-control gl-form-input"
+ .form-group
+ = f.label _("Description"), class: 'col-form-label'
+ = f.text_area :pwa_description, class: "form-control gl-form-input", rows: 10
+ .form-text.text-muted
+ = parsed_with_gfm
+ .form-group
+ = f.label :pwa_icon, class: 'col-form-label gl-pt-0'
+ %p
+ - if @appearance.pwa_icon?
+ = image_tag @appearance.pwa_icon_path, class: 'appearance-pwa-icon-preview'
+ - if @appearance.persisted?
+ %br
+ = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: pwa_icon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Icon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove icon') } }) do
+ = _('Remove icon')
+ %hr
+ = f.hidden_field :pwa_icon_cache
+ = f.file_field :pwa_icon, class: "", accept: 'image/*'
+ .form-text.text-muted
+ = _('Maximum file size is 1MB.')
+
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0= _('New project pages')
+
+ .form-group
+ = f.label :new_project_guidelines, class: 'col-form-label'
+ %p
+ = f.text_area :new_project_guidelines, class: "form-control gl-form-input", rows: 10
.form-text.text-muted
= parsed_with_gfm
- .form-group
- = f.label :logo, class: 'col-form-label gl-pt-0'
- %p
- - if @appearance.logo?
- = image_tag @appearance.logo_path, class: 'appearance-logo-preview'
- - if @appearance.persisted?
- %br
- = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: logo_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove logo') } }) do
- = _('Remove logo')
- %hr
- = f.hidden_field :logo_cache
- = f.file_field :logo, class: "", accept: 'image/*'
- .form-text.text-muted
- = _('Maximum file size is 1 MB. Pages are optimized for a 128x128 px logo.')
-
- %hr
- .row
- .col-lg-4
- %h4.gl-mt-0= _('Progressive Web App (PWA)')
-
- .col-lg-8
- .form-group
- = f.label _("Name"), class: 'col-form-label'
- = f.text_field :pwa_name, class: "form-control gl-form-input"
- .form-group
- = f.label _("Short name"), class: 'col-form-label'
- = f.text_field :pwa_short_name, class: "form-control gl-form-input"
- .form-group
- = f.label _("Description"), class: 'col-form-label'
- = f.text_area :pwa_description, class: "form-control gl-form-input", rows: 10
+
+ .settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0= _('Profile image guideline')
+
+ .form-group
+ = f.label :profile_image_guidelines, class: 'col-form-label'
+ %p
+ = f.text_area :profile_image_guidelines, class: "form-control gl-form-input", rows: 10
.form-text.text-muted
= parsed_with_gfm
- .form-group
- = f.label :pwa_icon, class: 'col-form-label gl-pt-0'
- %p
- - if @appearance.pwa_icon?
- = image_tag @appearance.pwa_icon_path, class: 'appearance-pwa-icon-preview'
- - if @appearance.persisted?
- %br
- = render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: pwa_icon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Icon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove icon') } }) do
- = _('Remove icon')
- %hr
- = f.hidden_field :pwa_icon_cache
- = f.file_field :pwa_icon, class: "", accept: 'image/*'
- .form-text.text-muted
- = _('Maximum file size is 1MB.')
-
- %hr
- .row
- .col-lg-4
- %h4.gl-mt-0= _('New project pages')
-
- .col-lg-8
- .form-group
- = f.label :new_project_guidelines, class: 'col-form-label'
- %p
- = f.text_area :new_project_guidelines, class: "form-control gl-form-input", rows: 10
- .form-text.text-muted
- = parsed_with_gfm
-
- %hr
- .row
- .col-lg-4
- %h4.gl-mt-0= _('Profile image guideline')
-
- .col-lg-8
- .form-group
- = f.label :profile_image_guidelines, class: 'col-form-label'
- %p
- = f.text_area :profile_image_guidelines, class: "form-control gl-form-input", rows: 10
- .form-text.text-muted
- = parsed_with_gfm
-
- .gl-mt-3.gl-mb-3
- = f.submit _('Update appearance settings'), pajamas_button: true
- - if @appearance.persisted? || @appearance.updated_at
- .mt-4
- - if @appearance.persisted?
- Preview last save:
- = link_to _('Sign-in page'), preview_sign_in_admin_application_settings_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- = link_to _('New project page'), new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
-
- - if @appearance.updated_at
- %span.float-right
- Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
+
+ - if @appearance.persisted? || @appearance.updated_at
+ .settings-section
+ - if @appearance.persisted?
+ Preview last save:
+ = link_to _('Sign-in page'), preview_sign_in_admin_application_settings_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('New project page'), new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+
+ - if @appearance.updated_at
+ %span.float-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
+
+ .settings-sticky-footer
+ = f.submit _('Update appearance settings'), pajamas_button: true
diff --git a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
index 2ca037db532..61df5f5fd0d 100644
--- a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
+++ b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
@@ -1,30 +1,29 @@
- form = local_assigns.fetch(:form)
-%hr
-.row
- .col-lg-4
- %h4.gl-mt-0
- = _('System header and footer')
+.settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
+ = _('System header and footer')
- .col-lg-8
- .form-group
- = form.label :header_message, _('Header message'), class: 'col-form-label label-bold'
- = form.text_area :header_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize"
- .form-group
- = form.label :footer_message, _('Footer message'), class: 'col-form-label label-bold'
- = form.text_area :footer_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize"
- .form-group
- = form.gitlab_ui_checkbox_component :email_header_and_footer_enabled,
- _('Enable header and footer in emails'),
- help_text: _('Add header and footer to emails. Please note that color settings will only be applied within the application interface'),
- label_options: { class: 'gl-font-weight-bold!' }
+ .form-group
+ = form.label :header_message, _('Header message'), class: 'col-form-label label-bold'
+ = form.text_area :header_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize"
+ .form-group
+ = form.label :footer_message, _('Footer message'), class: 'col-form-label label-bold'
+ = form.text_area :footer_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize"
+ .form-group
+ = form.gitlab_ui_checkbox_component :email_header_and_footer_enabled,
+ _('Enable header and footer in emails'),
+ help_text: _('Add header and footer to emails. Please note that color settings will only be applied within the application interface'),
+ label_options: { class: 'gl-font-weight-bold!' }
- .form-group.js-toggle-colors-container
- = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'js-toggle-colors-link' }) do
- = _('Customize colors')
- .form-group.js-toggle-colors-container.hide
- = form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold'
- = form.color_field :message_background_color, class: "form-control gl-form-input"
- .form-group.js-toggle-colors-container.hide
- = form.label :message_font_color, _('Font Color'), class: 'col-form-label label-bold'
- = form.color_field :message_font_color, class: "form-control gl-form-input"
+ .form-group.js-toggle-colors-container
+ = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'js-toggle-colors-link' }) do
+ = _('Customize colors')
+ .form-group.js-toggle-colors-container.hide
+ = form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold'
+ = form.color_field :message_background_color, class: "form-control gl-form-input"
+ .form-group.js-toggle-colors-container.hide
+ = form.label :message_font_color, _('Font Color'), class: 'col-form-label label-bold'
+ = form.color_field :message_font_color, class: "form-control gl-form-input"
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index a03d6cb5a94..3d73b255a5e 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -1,11 +1,9 @@
- page_title _('New Deploy Key')
%h1.page-title.gl-font-size-h-display= _('New public deploy key')
-%hr
-%div
- = gitlab_ui_form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
- = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
- .form-actions
- = f.submit 'Create', data: { qa_selector: "add_deploy_key_button" }, pajamas_button: true
- = render Pajamas::ButtonComponent.new(href: admin_deploy_keys_path) do
- = _('Cancel')
+= gitlab_ui_form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
+ = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
+ .gl-display-flex.gl-mt-6.gl-gap-3
+ = f.submit 'Create', data: { qa_selector: "add_deploy_key_button" }, pajamas_button: true
+ = render Pajamas::ButtonComponent.new(href: admin_deploy_keys_path) do
+ = _('Cancel')
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index e7ea139eb71..397ba7ae700 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -40,7 +40,7 @@
%p.gl-text-secondary
= s_('Preferences|Customize the appearance of the syntax.')
= succeed '.' do
- = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'change-the-syntax-highlighting-theme'), target: '_blank', rel: 'noopener noreferrer'
.syntax-theme.row
- Gitlab::ColorSchemes.each do |scheme|
%label.col-6.col-sm-4.col-md-3.col-lg-auto.gl-mb-5
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index 997443d5fa9..0044ff4dc24 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -1,10 +1,8 @@
- page_title _('Edit Deploy Key')
%h1.page-title.gl-font-size-h-display= _('Edit Deploy Key')
-%hr
-%div
- = gitlab_ui_form_for [@project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
- = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
- .form-actions
- = f.submit _('Save changes'), pajamas_button: true
- = link_button_to _('Cancel'), project_settings_repository_path(@project)
+= gitlab_ui_form_for [@project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
+ = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
+ .gl-display-flex.gl-mt-6.gl-gap-3
+ = f.submit _('Save changes'), pajamas_button: true
+ = link_button_to _('Cancel'), project_settings_repository_path(@project, anchor: 'js-deploy-keys-settings')
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index 9793f21e4a9..2d17719a8c2 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -15,7 +15,7 @@
can_edit_project_settings: can?(current_user, :admin_project, @project).to_s,
service_desk_callout_svg_path: image_path('service_desk_callout.svg'),
service_desk_settings_path: edit_project_path(@project, anchor: 'js-service-desk'),
- service_desk_help_path: help_page_path('user/project/service_desk'),
+ service_desk_help_path: help_page_path('user/project/service_desk/index'),
is_service_desk_supported: Gitlab::ServiceDesk.supported?.to_s,
is_service_desk_enabled: @project.service_desk_enabled?.to_s } }
- else
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index 280362a12a9..bbaf5bf9627 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -5,37 +5,34 @@
= form_errors(deploy_key)
.form-group
- = form.label :title, class: 'col-form-label col-sm-2'
- .col-sm-10= form.text_field :title, class: 'form-control gl-form-input', data: { testid: 'deploy-key-title-field' }, readonly: ('readonly' unless can?(current_user, :update_deploy_key, deploy_key))
+ = form.label :title
+ = form.text_field :title, class: 'form-control gl-form-input', data: { testid: 'deploy-key-title-field' }, readonly: ('readonly' unless can?(current_user, :update_deploy_key, deploy_key))
-.form-group
- - if deploy_key.new_record?
- = form.label :key, class: 'col-form-label col-sm-2'
- .col-sm-10
- %p.light
- - link_start = "<a href='#{help_page_path('user/ssh')}' target='_blank' rel='noreferrer noopener'>".html_safe
- - link_end = '</a>'
- = _('Paste a public key here. %{link_start}How do I generate it?%{link_end}').html_safe % { link_start: link_start, link_end: link_end.html_safe }
- = form.text_area :key, class: 'form-control gl-form-input thin_area', rows: 5, data: { testid: 'deploy-key-field' }
- - else
- - if deploy_key.fingerprint_sha256.present?
- = form.label :fingerprint, _('Fingerprint (SHA256)'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = form.text_field :fingerprint_sha256, class: 'form-control gl-form-input', readonly: 'readonly'
- - if deploy_key.fingerprint.present?
- = form.label :fingerprint, _('Fingerprint (MD5)'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = form.text_field :fingerprint, class: 'form-control gl-form-input', readonly: 'readonly'
+- if deploy_key.new_record?
+ .form-group
+ = form.label :key
+
+ %p.gl-text-secondary
+ - link_start = "<a href='#{help_page_path('user/ssh')}' target='_blank' rel='noreferrer noopener'>".html_safe
+ - link_end = '</a>'
+ = _('Paste a public key here. %{link_start}How do I generate it?%{link_end}').html_safe % { link_start: link_start, link_end: link_end.html_safe }
+ = form.text_area :key, class: 'form-control gl-form-input thin_area', rows: 5, data: { testid: 'deploy-key-field' }
+- else
+ - if deploy_key.fingerprint_sha256.present?
+ .form-group
+ = form.label :fingerprint, _('Fingerprint (SHA256)')
+ = form.text_field :fingerprint_sha256, class: 'form-control gl-form-input', readonly: 'readonly'
+ - if deploy_key.fingerprint.present?
+ .form-group
+ = form.label :fingerprint, _('Fingerprint (MD5)')
+ = form.text_field :fingerprint, class: 'form-control gl-form-input', readonly: 'readonly'
.form-group
- .col-sm-10
- = form.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
- = form.text_field :expires_at, class: 'form-control gl-form-input', readonly: 'readonly'
+ = form.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
+ = form.text_field :expires_at, class: 'form-control gl-form-input', readonly: 'readonly'
- if deploy_keys_project.present?
= form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form|
.form-group
- .col-form-label.col-sm-2
- .col-sm-10
- = deploy_keys_project_form.gitlab_ui_checkbox_component :can_push, _('Grant write permissions to this key'),
- help_text: _('Allow this key to push to this repository')
+ = deploy_keys_project_form.gitlab_ui_checkbox_component :can_push, _('Grant write permissions to this key'),
+ help_text: _('Allow this key to push to this repository')
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
index 32720d0353b..3f81cbbb9b1 100644
--- a/app/views/shared/deploy_keys/_index.html.haml
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -4,11 +4,13 @@
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Deploy keys')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
- %p
+ %p.gl-text-secondary.gl-mb-0
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_keys/index') }
= _("Add deploy keys to grant read/write access to this repository. %{link_start}What are deploy keys?%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.settings-content
- %h5.gl-mt-0
- = render @deploy_keys.form_partial_path
- %hr
- #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
+ - c.with_body do
+ .gl-new-card-add-form.gl-m-3.gl-display-none.js-toggle-content
+ = render @deploy_keys.form_partial_path
+
+ #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
index 2f90345ed12..c633088b26a 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -1,5 +1,9 @@
= gitlab_ui_form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
= form_errors(@deploy_keys.new_key)
+
+ .form-group.row
+ %h4.gl-my-0= s_('DeployKeys|Add new deploy key')
+
.form-group.row
= f.label :title, class: "label-bold"
= f.text_field :title, class: 'form-control gl-form-input', required: true, data: { testid: 'deploy-key-title-field' }
@@ -20,5 +24,7 @@
= f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-key-expires-at-field' }, value: f.object.expires_at
%p.form-text.text-muted= ssh_key_expires_field_description
- .form-group.row
+ .form-group.row.gl-mb-0
= f.submit _("Add key"), data: { testid: "add-deploy-key-button"}, pajamas_button: true
+ = render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-3 js-toggle-button' }) do
+ = _('Cancel')
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 0123bf0627c..5513ac1813a 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -35,7 +35,11 @@ namespace :admin do
resource :impersonation, only: :destroy
- resources :abuse_reports, only: [:index, :show, :update, :destroy]
+ resources :abuse_reports, only: [:index, :show, :update, :destroy] do
+ member do
+ put :moderate_user
+ end
+ end
resources :gitaly_servers, only: [:index]
resources :spam_logs, only: [:index, :destroy] do
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 08878c58356..ad3e6a8d8c3 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -76,6 +76,8 @@ Example response:
## Add a member role to a group
+> Ability to add a name and description when creating a custom role [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126423) in GitLab 16.3.
+
Adds a member role to a group.
```plaintext
@@ -131,6 +133,12 @@ Example response:
}
```
+In GitLab 16.3 and later, you can use the API to:
+
+- Add a name (required) and description (optional) when you
+ [create a new custom role](../user/permissions.md#create-a-custom-role).
+- Update an existing custom role's name and description.
+
### Remove member role of a group
Deletes a member role of a group.
diff --git a/doc/api/project_job_token_scopes.md b/doc/api/project_job_token_scopes.md
index 486149ad180..130d3fbdbc6 100644
--- a/doc/api/project_job_token_scopes.md
+++ b/doc/api/project_job_token_scopes.md
@@ -144,7 +144,7 @@ Example response:
}
```
-## Create a new project to a project's CI/CD job token inbound allowlist
+## Add a project to a CI/CD job token inbound allowlist
Add a project to the [CI/CD job token inbound allowlist](../ci/jobs/ci_job_token.md#allow-access-to-your-project-with-a-job-token) of a project.
@@ -161,16 +161,16 @@ Supported attributes:
If successful, returns [`201`](rest/index.md#status-codes) and the following response attributes:
-| Attribute | Type | Description |
-|:--------------------|:--------|:----------------------|
-| `source_project_id` | integer | The ID of the project whose CI/CD job token inbound allowlist is added to. |
-| `target_project_id` | integer | The ID of the project that is added to the inbound allowlist of the source project. |
+| Attribute | Type | Description |
+|---------------------|---------|-------------|
+| `source_project_id` | integer | ID of the project containing the CI/CD job token inbound allowlist to update. |
+| `target_project_id` | integer | ID of the project that is added to the source project's inbound allowlist. |
Example request:
```shell
-curl --request PATCH \
- --url "https://gitlab.example.com/api/v4/projects/1/job_token_scope" \
+curl --request POST \
+ --url "https://gitlab.example.com/api/v4/projects/1/job_token_scope/allowlist" \
--header 'PRIVATE-TOKEN: <your_access_token>' \
--header 'Content-Type: application/json' \
--data '{ "target_project_id": 2 }'
@@ -185,7 +185,7 @@ Example response:
}
```
-## Remove a project from a project's CI/CD job token inbound allowlist
+## Remove a project from a CI/CD job token inbound allowlist
Remove a project from the [CI/CD job token inbound allowlist](../ci/jobs/ci_job_token.md#allow-access-to-your-project-with-a-job-token) of a project.
@@ -205,7 +205,6 @@ If successful, returns [`204`](rest/index.md#status-codes) and no response body.
Example request:
```shell
-
curl --request DELETE \
--url "https://gitlab.example.com/api/v4/projects/1/job_token_scope/allowlist/2" \
--header 'PRIVATE-TOKEN: <your_access_token>' \
diff --git a/doc/api/users.md b/doc/api/users.md
index 34b6c2b7261..f872cef57a9 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -536,7 +536,7 @@ Parameters:
| `avatar` | No | Image file for user's avatar |
| `bio` | No | User's biography |
| `can_create_group` | No | User can create top-level groups - true or false |
-| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#syntax-highlighting-theme)) |
+| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#change-the-syntax-highlighting-theme)) |
| `email` | Yes | Email |
| `extern_uid` | No | External UID |
| `external` | No | Flags the user as external - true or false (default) |
@@ -585,7 +585,7 @@ Parameters:
| `avatar` | No | Image file for user's avatar |
| `bio` | No | User's biography |
| `can_create_group` | No | User can create groups - true or false |
-| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#syntax-highlighting-theme) for more information) |
+| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#change-the-syntax-highlighting-theme) for more information) |
| `commit_email` | No | User's commit email. Set to `_private` to use the private commit email. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/375148) in GitLab 15.5. |
| `email` | No | Email |
| `extern_uid` | No | External UID |
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index 4b17f3354aa..d76bb113516 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -234,7 +234,6 @@ concurrent = 4
cache_dir = "/cache"
environment = [
- "GIT_DEPTH=10",
"GIT_CLONE_PATH=$CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME"
]
diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md
index 58ec0de6074..59e35e658e7 100644
--- a/doc/development/contributing/design.md
+++ b/doc/development/contributing/design.md
@@ -61,7 +61,7 @@ Check these aspects both when _designing_ and _reviewing_ UI changes.
### Visual design
-Check visual design properties using your browser's _elements inspector_ ([Chrome](https://developer.chrome.com/docs/devtools/css/),
+Check visual design properties using your browser's elements inspector ([Chrome](https://developer.chrome.com/docs/devtools/css/),
[Firefox](https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/open_the_inspector/index.html)).
- Use recommended [colors](https://design.gitlab.com/product-foundations/color)
@@ -71,9 +71,11 @@ Check visual design properties using your browser's _elements inspector_ ([Chrom
or propose new ones according to [iconography](https://design.gitlab.com/product-foundations/iconography/)
and [illustration](https://design.gitlab.com/product-foundations/illustration/)
guidelines.
-- _Optionally_ consider [dark mode](../../user/profile/preferences.md#dark-mode). [^1]
+- Optional: Consider dark mode. For more information, see [Change the syntax highlighting theme](../../user/profile/preferences.md#change-the-syntax-highlighting-theme).
- [^1]: You're not required to design for [dark mode](../../user/profile/preferences.md#dark-mode) while the feature is an [Experiment](../../policy/experiment-beta-support.md#experiment). The [UX Foundations team](https://about.gitlab.com/direction/manage/foundations/) plans to improve the dark mode in the future. Until we integrate [Pajamas](https://design.gitlab.com/) components into the product and the underlying design strategy is in place to support dark mode, we cannot guarantee that we won't introduce bugs and debt to this mode. At your discretion, evaluate the need to create dark mode patches.
+### Dark Mode
+
+You're not required to design for dark mode while the feature is an [Experiment](../../policy/experiment-beta-support.md#experiment). The [UX Foundations team](https://about.gitlab.com/direction/manage/foundations/) plans to improve the dark mode in the future. Until we integrate [Pajamas](https://design.gitlab.com/) components into the product and the underlying design strategy is in place to support dark mode, we cannot guarantee that we won't introduce bugs and debt to this mode. At your discretion, evaluate the need to create dark mode patches.
### States
diff --git a/doc/development/database/single_table_inheritance.md b/doc/development/database/single_table_inheritance.md
index dcf696b85bc..40b608bd110 100644
--- a/doc/development/database/single_table_inheritance.md
+++ b/doc/development/database/single_table_inheritance.md
@@ -6,22 +6,58 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Single Table Inheritance
-**Summary:** don't use Single Table Inheritance (STI), use separate tables
-instead.
+**Summary:** Don't design new tables using Single Table Inheritance (STI). For existing tables that use STI as a pattern, avoid adding new types, and consider splitting them into separate tables.
-Rails makes it possible to have multiple models stored in the same table and map
-these rows to the correct models using a `type` column. This can be used to for
-example store two different types of SSH keys in the same table.
+STI is a database design pattern where a single table stores
+different types of records. These records have a subset of shared columns and another column
+that instructs the application which object that record should be represented by.
+This can be used to for example store two different types of SSH keys in the same
+table. ActiveRecord makes use of it and provides some features that make STI usage
+more convenient.
-While tempting to use one should avoid this at all costs for the same reasons as
-outlined in the document ["Polymorphic Associations"](polymorphic_associations.md).
+We no longer allow new STI tables because they:
-## Solution
+- Lead to tables with large number of rows, when we should strive to keep tables small.
+- Need additional indexes, increasing our usage of lightweight locks, whose saturation can cause incidents.
+- Add overhead by having to filter all of the data by a value, leading to more page accesses on read.
+- Use the `class_name` to load the correct class for an object, but storing
+ the class name is costly and unnecessary.
-The solution is very simple: just use a separate table for every type you'd
-otherwise store in the same table. For example, instead of having a `keys` table
-with `type` set to either `Key` or `DeployKey` you'd have two separate tables:
-`keys` and `deploy_keys`.
+Instead of using STI, consider the following alternatives:
+
+- Use a different table for each type.
+- Avoid adding `*_type` columns. This is a code smell that might indicate that new types will be added in the future, and refactoring in the future will be much harder.
+- If you already have a table that is effectively an STI on a `_type` column, consider:
+ - Splitting the existent data into multiple tables.
+ - Refactoring so that new types can be added as new tables while keeping existing ones (for example, move logic of the base class into a concern).
+
+If, **after considering all of the above downsides and alternatives**, STI
+is the only solution for the problem at hand, we can at least avoid the
+issues with saving the class name in the record by using an enum type
+instead and the `EnumInheritance` concern:
+
+```ruby
+class Animal < ActiveRecord::Base
+ include EnumInheritance
+
+ enum species: {
+ dog: 1,
+ cat: 2
+ }
+
+ def self.inheritance_column_to_class_map = {
+ dog: 'Dog',
+ cat: 'Cat'
+ }
+
+ def self.inheritance_column = 'species'
+end
+
+class Dog < Animal; end
+class Cat < Animal; end
+```
+
+If your table already has a `*_type`, new classes for the different types can be added as needed.
## In migrations
diff --git a/doc/development/fe_guide/dark_mode.md b/doc/development/fe_guide/dark_mode.md
index 55181edd64c..5e8f844172a 100644
--- a/doc/development/fe_guide/dark_mode.md
+++ b/doc/development/fe_guide/dark_mode.md
@@ -5,8 +5,7 @@ group: Development
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-This page is about developing dark mode for GitLab. We also have documentation on how
-[to enable dark mode](../../user/profile/preferences.md#dark-mode).
+This page is about developing dark mode for GitLab. For more information on how to enable dark mode, see [Change the syntax highlighting theme]](../../user/profile/preferences.md#change-the-syntax-highlighting-theme).
# How dark mode works
diff --git a/doc/development/namespaces.md b/doc/development/namespaces.md
new file mode 100644
index 00000000000..e25b0f57f08
--- /dev/null
+++ b/doc/development/namespaces.md
@@ -0,0 +1,302 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Namespaces
+
+Namespaces are containers for projects and associated resources. A `Namespace` is instantiated through its subclasses of `Group`, `ProjectNamespace`, and `UserNamespace`.
+
+```mermaid
+graph TD
+ Namespace -.- Group
+ Namespace -.- ProjectNamespace
+ Namespace -.- UserNamespace
+```
+
+A `User` has one `UserNamespace`, and can be a member of many `Namespaces`.
+
+```mermaid
+graph TD
+ Namespace -.- Group
+ Namespace -.- ProjectNamespace
+ Namespace -.- UserNamespace
+
+ User -- has one --- UserNamespace
+ Namespace --- Member --- User
+```
+
+`Group` exists in a recursive hierarchical relationship. `Groups` have many `ProjectNamespaces` which parent one `Project`.
+
+```mermaid
+graph TD
+ Group -- has many --- ProjectNamespace -- has one --- Project
+ Group -- has many --- Group
+```
+
+## Querying namespaces
+
+There is a set of methods provided to query the namespace hierarchy. The methods produce standard Rails `ActiveRecord::Relation` objects.
+The methods can be split into two similar halves. One set of methods operate on a Namespace object, while the other set operate as composable Namespace scopes.
+
+By their nature, the object methods will operate within a single `Namespace` hierarchy, while the scopes can span hierarchies.
+
+The following is a non-exhaustive list of methods to query `Namespace` hierarchies.
+
+### Root namespaces
+
+The root is the top most `Namespace` in the hierarchy. A root has a `nil` `parent_id`.
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A.A.B active
+ class A sel
+```
+
+```ruby
+Namespace.where(...).roots
+
+namespace_object.root_ancestor
+```
+
+### Descendant namespaces
+
+The descendants of a namespace are its children, their children, and so on.
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A.A active
+ class A.A.A,A.A.B sel
+```
+
+We can return ourself and our descendants through `self_and_descendants`.
+
+```ruby
+Namespace.where(...).self_and_descendants
+
+namespace_object.self_and_descendants
+```
+
+We can return only our descendants excluding ourselves:
+
+```ruby
+Namespace.where(...).self_and_descendants(include_self: false)
+
+namespace_object.descendants
+```
+
+We could not name the scope method `.descendants` because we would override the `Object` method of the same name.
+
+It can be more efficient to return the descendant IDs instead of the whole record:
+
+```ruby
+Namespace.where(...).self_and_descendant_ids
+Namespace.where(...).self_and_descendant_ids(include_self: false)
+
+namespace_object.self_and_descendant_ids
+namespace_object.descendant_ids
+```
+
+### Ancestor namespaces
+
+The ancestors of a namespace are its children, their children, and so on.
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A.A active
+ class A sel
+```
+
+We can return ourself and our ancestors through `self_and_ancestors`.
+
+```ruby
+Namespace.where(...).self_and_ancestors
+
+namespace_object.self_and_ancestors
+```
+
+We can return only our ancestors excluding ourselves:
+
+```ruby
+Namespace.where(...).self_and_ancestors(include_self: false)
+
+namespace_object.ancestors
+```
+
+We could not name the scope method `.ancestors` because we would override the `Module` method of the same name.
+
+It can be more efficient to return the ancestor ids instead of the whole record:
+
+```ruby
+Namespace.where(...).self_and_ancstor_ids
+Namespace.where(...).self_and_ancestor_ids(include_self: false)
+
+namespace_object.self_and_ancestor_ids
+namespace_object.ancestor_ids
+```
+
+### Hierarchies
+
+A Namespace hierarchy is a `Namespace`, its ancestors, and its descendants.
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A.A active
+ class A,A.A.A,A.A.B sel
+```
+
+We can query a namespace hierarchy:
+
+```ruby
+Namespace.where(...).self_and_hierarchy
+
+namespace_object.self_and_hierarchy
+```
+
+### Recursive queries
+
+The queries above are known as the linear queries because they use the `namespaces.traversal_ids` column to perform standard SQL queries instead of recursive CTE queries.
+
+A set of legacy recursive queries are also accessible if needed:
+
+```ruby
+Namespace.where(...).recursive_self_and_descendants
+Namespace.where(...).recursive_self_and_descendants(include_self: false)
+Namespace.where(...).recursive_self_and_descendant_ids
+Namespace.where(...).recursive_self_and_descendant_ids(include_self: false)
+Namespace.where(...).recursive_self_and_ancestors
+Namespace.where(...).recursive_self_and_ancestors(include_self: false)
+Namespace.where(...).recursive_self_and_ancstor_ids
+Namespace.where(...).recursive_self_and_ancestor_ids(include_self: false)
+Namespace.where(...).recursive_self_and_hierarchy
+
+namespace_object.recursive_root_ancestor
+namespace_object.recursive_self_and_descendants
+namespace_object.recursive_descendants
+namespace_object.recursive_self_and_descendant_ids
+namespace_object.recursive_descendant_ids
+namespace_object.recursive_self_and_ancestors
+namespace_object.recursive_ancestors
+namespace_object.recursive_self_and_ancestor_ids
+namespace_object.recursive_ancestor_ids
+namespace_object.recursive_self_and_hierarchy
+```
+
+## Namespace query implementation
+
+The linear queries are executed using the `namespaces.traversal_ids` array column. Each array represents an ordered set of `Namespace` IDs from the root `Namespace` to the current `Namespace`.
+
+Given the scenario:
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A.A.B active
+```
+
+The `traversal_ids` for `Namespace` `A.A.B` would be `[A, A.A, A.A.B]`.
+
+The `traversal_ids` have some useful properties to keep in mind if working in this area:
+
+- The root of every `Namespace` is provided by `traversal_ids[1]`. Note that PostgreSQL array indexes begin at `1`.
+- The ID of the current `Namespace` is provided by `traversal_ids[array_length(traversal_ids, 1)]`.
+- The `Namespace` ancestors are represented by the `traversal_ids`.
+- A `Namespace`'s `traversal_ids` are a subset of their descendants `traversal_ids`. A `Namespace` with `traversal_ids = [1,2,3]` will have descendants that all start with `[1,2,3,...]`.
+- PostgreSQL arrays are ordered such that `[1] < [1,1] < [2]`.
+
+Using these properties we find the `root` and `ancestors` are already provided for by `traversal_ids`.
+
+With the object descendant queries we lean on the `@>` array operator which will test inclusion of an array inside another array.
+The `@>` operator has been found to be quite slow as the search space grows. Another method is used for scope queries which tend to have larger search spaces.
+With scope queries we combine comparison operators with the array ordering property.
+
+All descendants of a `Namespace` with `traversal_ids = [1,2,3]` have `traversal_ids` that are greater than `[1,2,3]` but less than `[1,2,4]`.
+In this example `[1,2,3]` and `[1,2,4]` are siblings, and `[1,2,4]` is the next sibling after `[1,2,3]`. A SQL function is provided to find the next sibling of a `traversal_ids` called `next_traversal_ids_sibling`.
+
+```sql
+gitlabhq_development=# select next_traversal_ids_sibling(ARRAY[1,2,3]);
+ next_traversal_ids_sibling
+----------------------------
+ {1,2,4}
+(1 row)
+```
+
+We then build descendant linear query scopes using comparison operators:
+
+```sql
+WHERE namespaces.traversal_ids > ARRAY[1,2,3]
+ AND namespaces.traversal_ids < next_traversal_ids_sibling(ARRAY[1,2,3])
+```
+
+### Superset
+
+`Namespace` queries are prone to returning duplicate results. For example, consider a query to find descendants of `A` and `A.A`:
+
+```mermaid
+graph TD
+ classDef active fill:#f00,color:#fff
+ classDef sel fill:#00f,color:#fff
+
+ A --- A.A --- A.A.A
+ A.A --- A.A.B
+ A --- A.B --- A.B.A
+ A.B --- A.B.B
+
+ class A,A.A active
+ class A.A.A,A.A.B,A.B,A.B.A,A.B.B sel
+```
+
+```ruby
+namespaces = Namespace.where(name: ['A', 'A.A'])
+
+namespaces.self_and_descendants
+
+=> A.A, A.A.A, A.A.B, A.B, A.B.A, A.B.B
+```
+
+Searching for the descendants of both `A` and `A.A` is unnecessary because `A.A` is already a descendant of `A`.
+In extreme cases this can create excessive I/O leading to poor performance.
+
+Redundant `Namespaces` are eliminated from a query if a `Namespace` `ID` in the `traversal_ids` attribute matches an `ID` belonging to another `Namespace` in the set of `Namespaces` being queried.
+A match of this condition signifies that an ancestor exists in the set of `Namespaces` being queried, and the current `Namespace` is therefore redundant.
+This optimization will result in much better performance of edge cases that would otherwise be very slow.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 616e10e9c63..2089566a9d6 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -426,5 +426,5 @@ a session if the browser is closed or the existing session expires.
- Manage applications that can [use GitLab as an OAuth provider](../../integration/oauth_provider.md)
- Manage [personal access tokens](personal_access_tokens.md) to access your account via API and authorized applications
- Manage [SSH keys](../ssh.md) to access your account via SSH
-- Change your [syntax highlighting theme](preferences.md#syntax-highlighting-theme)
+- [Change the syntax highlighting theme](preferences.md#change-the-syntax-highlighting-theme)
- [View your active sessions](active_sessions.md) and revoke any of them if necessary
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index bbca3083010..15dbf42b149 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -13,7 +13,9 @@ You can update your preferences to change the look and feel of GitLab.
You can change the color theme of the GitLab UI. These colors are displayed on the left sidebar.
Using individual color themes might help you differentiate between your different
-GitLab instances.
+GitLab instances.
+
+To change the color theme:
1. On the left sidebar, select your avatar.
1. Select **Preferences**.
@@ -21,63 +23,46 @@ GitLab instances.
### Dark mode
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28252) in GitLab 13.1 as an [Experiment](../../policy/experiment-beta-support.md#experiment).
-
-GitLab has started work on dark mode! The dark mode Experiment release is available in the
-spirit of iteration and the lower expectations of
-[Experiment features](../../policy/experiment-beta-support.md#experiment).
-
-Progress on dark mode is tracked in the [Dark theme epic](https://gitlab.com/groups/gitlab-org/-/epics/2902).
-See the epic for:
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28252) in GitLab 13.1 as an [Experiment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28252).
-- A list of known issues.
-- Our planned direction and next steps.
+Dark mode makes elements on the GitLab UI stand out on a dark background.
-If you find an issue that isn't listed, leave a comment on the epic or create a
-new issue.
+- To turn on Dark mode, Select **Preferences > Color theme > Dark Mode**.
-Dark mode is available as a navigation theme, for MVC and compatibility reasons.
-[An issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/219512)
-to make it configurable in its own section along with support for
-different navigation themes.
+Dark mode works only with the **Dark** Syntax highlighting theme. You can report and view issues, send feedback, and track progress in [epic 2092](https://gitlab.com/groups/gitlab-org/-/epics/2902).
-Dark mode works only with the **Dark** syntax highlighting theme.
+## Change the syntax highlighting theme
-## Syntax highlighting theme
+> Changing the default syntax highlighting theme for authenticated and unauthenticated users [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/25129) in GitLab 15.1.
-> Changing the default syntax highlighting theme for new users and users who are not signed in [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/25129) in GitLab 15.10.
+Syntax highlighting is a feature in code editors and IDEs. The highlighter assigns a color to each type of code, such as strings and comments.
-GitLab uses the [Rouge Ruby library](https://github.com/rouge-ruby/rouge)
-for syntax highlighting outside of any Editor context. The WebIDE (like Snippets)
-uses [Monaco Editor](https://microsoft.github.io/monaco-editor/) and it's provided
-[Monarch](https://microsoft.github.io/monaco-editor/monarch.html) library for
-syntax highlighting. For a list of supported languages, see the documentation of
-the respective libraries.
+To change the syntax highlighting theme:
-Changing this setting allows you to customize the color theme when viewing any
-syntax highlighted code on GitLab.
+1. On the left sidebar, select your avatar.
+1. Select **Preferences**.
+1. In the **Syntax highlighting theme** section, select a theme.
+1. Select **Save changes**.
-![Profile preferences syntax highlighting themes](img/profile-preferences-syntax-themes_v15_11.png)
+To view the updated syntax highlighting theme, refresh your project's page.
-Introduced in GitLab 13.6, the themes [Solarized](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) also apply to the [Web IDE](../project/web_ide/index.md) and [Snippets](../snippets.md).
+To customize the syntax highlighting theme, you can also [use the Application settings API](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). Use `default_syntax_highlighting_theme` to change the syntax highlighting colors on a more granular level.
-You can use an API call to change the default syntax highlighting theme for new users and users
-who are not signed in. For more information, see the `default_syntax_highlighting_theme`
-in the [list of settings that can be accessed through API calls](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
+If these steps do not work, your programming language might not be supported by the syntax highlighters.
+For more information, view [Rouge Ruby Library](https://github.com/rouge-ruby/rouge) for guidance on code files and Snippets. View [Moncaco Editor](https://microsoft.github.io/monaco-editor/) and [Monarch](https://microsoft.github.io/monaco-editor/monarch.html) for guidance on the Web IDE.
-## Diff colors
+## Change the diff colors
-A diff compares the old/removed content with the new/added content (for example, when
-[reviewing a merge request](../project/merge_requests/reviews/index.md#review-a-merge-request) or in a
-[Markdown inline diff](../markdown.md#inline-diff)).
-Typically, the colors red and green are used for removed and added lines in diffs.
-The exact colors depend on the selected [syntax highlighting theme](#syntax-highlighting-theme).
-The colors may lead to difficulties in case of red-green color blindness.
+Diffs use two different background colors to show changes between versions of code. By default, the original file in red and the changes made in green.
-For this reason, you can customize the following colors:
+To change the diff colors:
-- Color for removed lines
-- Color for added lines
+1. On the left sidebar, select your avatar.
+1. Select **Preferences**.
+1. Go to the **Diff colors** section.
+1. Complete the fields.
+1. Select **Save changes**.
+1. Optional. Type a color code in the fields.
## Behavior
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1959fd07c4e..cda2232e1f4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16012,6 +16012,12 @@ msgstr ""
msgid "DeployKeys|+%{count} others"
msgstr ""
+msgid "DeployKeys|Add new deploy key"
+msgstr ""
+
+msgid "DeployKeys|Add new key"
+msgstr ""
+
msgid "DeployKeys|Current project"
msgstr ""
@@ -16039,7 +16045,7 @@ msgstr ""
msgid "DeployKeys|Loading deploy keys"
msgstr ""
-msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgid "DeployKeys|No deploy keys found, start by adding a new one above."
msgstr ""
msgid "DeployKeys|Privately accessible deploy keys"
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 79ae584ca13..d8369c12268 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -19,6 +19,7 @@ module QA
view 'app/assets/javascripts/deploy_keys/components/app.vue' do
element 'project-deploy-keys-container'
+ element 'add-new-deploy-key-button'
end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
@@ -27,6 +28,10 @@ module QA
element 'key-sha256-fingerprint-content'
end
+ def add_new_key
+ click_element('add-new-deploy-key-button')
+ end
+
def add_key
click_element('add-deploy-key-button')
end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
index b194f97ef1b..36d1221dfda 100644
--- a/qa/qa/resource/deploy_key.rb
+++ b/qa/qa/resource/deploy_key.rb
@@ -29,6 +29,7 @@ module QA
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |page|
+ page.add_new_key
page.fill_key_title(title)
page.fill_key_value(key)
diff --git a/rubocop/cop/database/avoid_inheritance_column.rb b/rubocop/cop/database/avoid_inheritance_column.rb
new file mode 100644
index 00000000000..d6c4903e754
--- /dev/null
+++ b/rubocop/cop/database/avoid_inheritance_column.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Database
+ # Checks for `self.inheritance_column` usage, which is discouraged https://docs.gitlab.com/ee/development/database/single_table_inheritance.html
+ class AvoidInheritanceColumn < RuboCop::Cop::Base
+ MSG = "Do not use Single Table Inheritance https://docs.gitlab.com/ee/development/database/single_table_inheritance.html"
+
+ def_node_search :inheritance_column_used?, <<~PATTERN
+ (send (self) :inheritance_column= !(sym :_type_disabled))
+ PATTERN
+
+ def on_send(node)
+ add_offense(node) if inheritance_column_used?(node)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 6aa7788b8e1..1b99c8b39d3 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -233,7 +233,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_des
it 'displays the small info box, documentation, a button to configure service desk, and the address' do
aggregate_failures do
- expect(page).to have_link('Learn more about Service Desk', href: help_page_path('user/project/service_desk'))
+ expect(page).to have_link('Learn more about Service Desk', href: help_page_path('user/project/service_desk/index'))
expect(page).not_to have_link('Enable Service Desk')
expect(page).to have_content(project.service_desk_address)
end
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
index 0006762a971..4e8f42ae792 100644
--- a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -91,6 +91,7 @@ RSpec.describe "User interacts with deploy keys", :js, feature_category: :groups
deploy_key_title = attributes_for(:key)[:title]
deploy_key_body = attributes_for(:key)[:key]
+ click_button("Add new key")
fill_in("deploy_key_title", with: deploy_key_title)
fill_in("deploy_key_key", with: deploy_key_body)
@@ -102,6 +103,16 @@ RSpec.describe "User interacts with deploy keys", :js, feature_category: :groups
expect(page).to have_content(deploy_key_title)
end
end
+
+ it "click on cancel hides the form" do
+ click_button("Add new key")
+
+ expect(page).to have_css('.gl-new-card-add-form')
+
+ click_button("Cancel")
+
+ expect(page).not_to have_css('.gl-new-card-add-form')
+ end
end
context "attaching existing keys" do
diff --git a/spec/fixtures/lib/backup/design_repo.refs b/spec/fixtures/lib/backup/design_repo.refs
new file mode 100644
index 00000000000..0df0c6916cb
--- /dev/null
+++ b/spec/fixtures/lib/backup/design_repo.refs
@@ -0,0 +1,2 @@
+c3cd4d7bd73a51a0f22045c3a4c871c435dc959d HEAD
+c3cd4d7bd73a51a0f22045c3a4c871c435dc959d refs/heads/master
diff --git a/spec/fixtures/lib/backup/personal_snippet_repo.refs b/spec/fixtures/lib/backup/personal_snippet_repo.refs
new file mode 100644
index 00000000000..ece8aa8f40f
--- /dev/null
+++ b/spec/fixtures/lib/backup/personal_snippet_repo.refs
@@ -0,0 +1,2 @@
+3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e HEAD
+3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e refs/heads/master
diff --git a/spec/fixtures/lib/backup/project_repo.refs b/spec/fixtures/lib/backup/project_repo.refs
new file mode 100644
index 00000000000..a075e52264c
--- /dev/null
+++ b/spec/fixtures/lib/backup/project_repo.refs
@@ -0,0 +1,2 @@
+393a7d860a5a4c3cc736d7eb00604e3472bb95ec HEAD
+393a7d860a5a4c3cc736d7eb00604e3472bb95ec refs/heads/master
diff --git a/spec/fixtures/lib/backup/project_snippet_repo.refs b/spec/fixtures/lib/backup/project_snippet_repo.refs
new file mode 100644
index 00000000000..5a2c600a876
--- /dev/null
+++ b/spec/fixtures/lib/backup/project_snippet_repo.refs
@@ -0,0 +1,2 @@
+6e44ba56a4748be361a841e759c20e421a1651a1 HEAD
+6e44ba56a4748be361a841e759c20e421a1651a1 refs/heads/master
diff --git a/spec/fixtures/lib/backup/wiki_repo.refs b/spec/fixtures/lib/backup/wiki_repo.refs
new file mode 100644
index 00000000000..dab2adaf520
--- /dev/null
+++ b/spec/fixtures/lib/backup/wiki_repo.refs
@@ -0,0 +1,2 @@
+c74b9948d0088d703ee1fafeddd9ed9add2901ea HEAD
+c74b9948d0088d703ee1fafeddd9ed9add2901ea refs/heads/master
diff --git a/spec/frontend/admin/abuse_report/components/report_actions_spec.js b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
index ec7dd31a046..6dd6d0e55c5 100644
--- a/spec/frontend/admin/abuse_report/components/report_actions_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
@@ -154,7 +154,7 @@ describe('ReportActions', () => {
beforeEach(async () => {
jest.spyOn(axios, 'put');
- axiosMock.onPut(report.updatePath).replyOnce(responseStatus, responseData);
+ axiosMock.onPut(report.moderateUserPath).replyOnce(responseStatus, responseData);
selectAction(params.user_action);
setCloseReport(params.close);
@@ -169,7 +169,7 @@ describe('ReportActions', () => {
});
it('does a put call with the right data', () => {
- expect(axios.put).toHaveBeenCalledWith(report.updatePath, params);
+ expect(axios.put).toHaveBeenCalledWith(report.moderateUserPath, params);
});
it('closes the drawer', () => {
@@ -191,4 +191,31 @@ describe('ReportActions', () => {
);
});
});
+
+ describe('when moderateUserPath is not present', () => {
+ it('sends the request to updatePath', async () => {
+ jest.spyOn(axios, 'put');
+ axiosMock.onPut(report.updatePath).replyOnce(HTTP_STATUS_OK, {});
+
+ const reportWithoutModerateUserPath = { ...report };
+ delete reportWithoutModerateUserPath.moderateUserPath;
+
+ createComponent({ report: reportWithoutModerateUserPath });
+
+ clickActionsButton();
+
+ await nextTick();
+
+ selectAction(params.user_action);
+ selectReason(params.reason);
+
+ await nextTick();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(axios.put).toHaveBeenCalledWith(report.updatePath, expect.any(Object));
+ });
+ });
});
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index 8c0ae223c87..8ff0c7d507a 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -51,5 +51,6 @@ export const mockAbuseReport = {
screenshot:
'/uploads/-/system/abuse_report/screenshot/27/Screenshot_2023-03-30_at_16.56.37.png',
updatePath: '/admin/abuse_reports/27',
+ moderateUserPath: '/admin/abuse_reports/27/moderate_user',
},
};
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index e0f86aadad4..e63b269fe23 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -41,10 +41,10 @@ describe('Deploy keys panel', () => {
it('renders help box if keys are empty', () => {
mountComponent({ keys: [] });
- expect(wrapper.find('.settings-message').exists()).toBe(true);
+ expect(wrapper.find('.gl-new-card-empty').exists()).toBe(true);
- expect(wrapper.find('.settings-message').text().trim()).toBe(
- 'No deploy keys found. Create one with the form above.',
+ expect(wrapper.find('.gl-new-card-empty').text().trim()).toBe(
+ 'No deploy keys found, start by adding a new one above.',
);
});
diff --git a/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js b/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js
index bf4951c7310..c67f9588ed4 100644
--- a/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js
@@ -13,6 +13,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
canAdminIssues: true,
isServiceDeskEnabled: true,
serviceDeskEmailAddress: 'email@address.com',
+ serviceDeskHelpPath: 'service/desk/help/path',
};
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -42,9 +43,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
});
it('renders description with service desk docs link', () => {
- expect(findIssuesHelpPageLink().attributes('href')).toBe(
- EmptyStateWithoutAnyIssues.serviceDeskHelpPagePath,
- );
+ expect(findIssuesHelpPageLink().attributes('href')).toBe(defaultProvide.serviceDeskHelpPath);
});
it('renders email address, when user can admin issues and service desk is enabled', () => {
@@ -80,9 +79,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
});
it('renders service desk docs link', () => {
- expect(findGlLink().attributes('href')).toBe(
- EmptyStateWithoutAnyIssues.serviceDeskHelpPagePath,
- );
+ expect(findGlLink().attributes('href')).toBe(defaultProvide.serviceDeskHelpPath);
expect(findGlLink().text()).toBe(learnMore);
});
});
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index f5b47e39fdb..1105f39124b 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -162,21 +162,27 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
end
context 'restore' do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository, :design_repo) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) }
- def copy_bundle_to_backup_path(bundle_name, destination)
- FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination)))
- FileUtils.cp(Rails.root.join('spec/fixtures/lib/backup', bundle_name), File.join(Gitlab.config.backup.path, 'repositories', destination))
+ def copy_fixture_to_backup_path(backup_name, repo_disk_path)
+ FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(repo_disk_path)))
+
+ %w[.bundle .refs].each do |filetype|
+ FileUtils.cp(
+ Rails.root.join('spec/fixtures/lib/backup', backup_name + filetype),
+ File.join(Gitlab.config.backup.path, 'repositories', repo_disk_path + filetype)
+ )
+ end
end
it 'restores from repository bundles', :aggregate_failures do
- copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle')
- copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle')
- copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle')
- copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle')
- copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle')
+ copy_fixture_to_backup_path('project_repo', project.disk_path)
+ copy_fixture_to_backup_path('wiki_repo', project.wiki.disk_path)
+ copy_fixture_to_backup_path('design_repo', project.design_repository.disk_path)
+ copy_fixture_to_backup_path('personal_snippet_repo', personal_snippet.disk_path)
+ copy_fixture_to_backup_path('project_snippet_repo', project_snippet.disk_path)
expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer').and_call_original
@@ -200,7 +206,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do
it 'clears specified storages when remove_all_repositories is set' do
expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-layout', 'pointer', '-remove-all-repositories', 'default').and_call_original
- copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle')
+ copy_fixture_to_backup_path('project_repo', project.disk_path)
subject.start(:restore, destination, backup_id: backup_id, remove_all_repositories: %w[default])
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
subject.finish!
diff --git a/spec/models/concerns/enum_inheritance_spec.rb b/spec/models/concerns/enum_inheritance_spec.rb
new file mode 100644
index 00000000000..492503dad36
--- /dev/null
+++ b/spec/models/concerns/enum_inheritance_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module EnumInheritableTestCase
+ class Animal < ActiveRecord::Base
+ include EnumInheritance
+
+ def self.table_name = '_test_animals'
+ def self.inheritance_column = 'species'
+
+ enum species: {
+ dog: 1,
+ cat: 2,
+ bird: 3
+ }
+
+ def self.inheritance_column_to_class_map = {
+ dog: 'EnumInheritableTestCase::Dog',
+ cat: 'EnumInheritableTestCase::Cat'
+ }.freeze
+ end
+
+ class Dog < Animal; end
+ class Cat < Animal; end
+end
+
+RSpec.describe EnumInheritance, feature_category: :shared do
+ describe '.sti_class_to_enum_map' do
+ it 'is the inverse of sti_class_to_enum_map' do
+ expect(EnumInheritableTestCase::Animal.sti_class_to_enum_map).to include({
+ 'EnumInheritableTestCase::Dog' => :dog,
+ 'EnumInheritableTestCase::Cat' => :cat
+ })
+ end
+ end
+
+ describe '.sti_class_for' do
+ it 'is the base class if no mapping for type is provided' do
+ expect(EnumInheritableTestCase::Animal.sti_class_for('bird')).to be(EnumInheritableTestCase::Animal)
+ end
+
+ it 'is class if mapping for type is provided' do
+ expect(EnumInheritableTestCase::Animal.sti_class_for('dog')).to be(EnumInheritableTestCase::Dog)
+ end
+ end
+
+ describe '.sti_name' do
+ it 'is nil if map does not exist' do
+ expect(EnumInheritableTestCase::Animal.sti_name).to eq("")
+ end
+
+ it 'is nil if map exists' do
+ expect(EnumInheritableTestCase::Dog.sti_name).to eq("dog")
+ end
+ end
+
+ describe 'querying' do
+ before_all do
+ EnumInheritableTestCase::Animal.connection.execute(<<~SQL)
+ CREATE TABLE _test_animals (
+ id bigserial primary key not null,
+ species bigint not null
+ );
+ SQL
+ end
+
+ let_it_be(:dog) { EnumInheritableTestCase::Dog.create! }
+ let_it_be(:cat) { EnumInheritableTestCase::Cat.create! }
+ let_it_be(:bird) { EnumInheritableTestCase::Animal.create!(species: :bird) }
+
+ describe 'object class when querying' do
+ context 'when mapping for type exists' do
+ it 'is the super class', :aggregate_failures do
+ queried_dog = EnumInheritableTestCase::Animal.find_by(id: dog.id)
+ expect(queried_dog).to eq(dog)
+ # Test below is already part of the test above, but it makes the desired behavior explicit
+ expect(queried_dog.class).to eq(EnumInheritableTestCase::Dog)
+
+ queried_cat = EnumInheritableTestCase::Animal.find_by(id: cat.id)
+ expect(queried_cat).to eq(cat)
+ expect(queried_cat.class).to eq(EnumInheritableTestCase::Cat)
+ end
+ end
+
+ context 'when mapping does not exist' do
+ it 'is the base class' do
+ expect(EnumInheritableTestCase::Animal.find_by(id: bird.id).class).to eq(EnumInheritableTestCase::Animal)
+ end
+ end
+ end
+
+ it 'finds by type' do
+ expect(EnumInheritableTestCase::Animal.where(species: :dog).first!).to eq(dog)
+ end
+ end
+end
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
index 8d033a2e147..c443a441af8 100644
--- a/spec/requests/admin/abuse_reports_controller_spec.rb
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -53,16 +53,16 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
end
end
- describe 'PUT #update' do
+ shared_examples 'moderates user' do
let(:report) { create(:abuse_report) }
let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
let(:expected_params) { ActionController::Parameters.new(params).permit! }
let(:message) { 'Service response' }
- subject(:request) { put admin_abuse_report_path(report, params) }
+ subject(:request) { put path }
- it 'invokes the Admin::AbuseReportUpdateService' do
- expect_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ it 'invokes the Admin::AbuseReports::ModerateUserService' do
+ expect_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
expect(service).to receive(:execute).and_call_original
end
@@ -71,7 +71,7 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
context 'when the service response is a success' do
before do
- allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ allow_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.success(message: message))
end
@@ -86,7 +86,7 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
context 'when the service response is an error' do
before do
- allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ allow_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: message))
end
@@ -100,6 +100,18 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
end
end
+ describe 'PUT #update' do
+ let(:path) { admin_abuse_report_path(report, params) }
+
+ it_behaves_like 'moderates user'
+ end
+
+ describe 'PUT #moderate_user' do
+ let(:path) { moderate_user_admin_abuse_report_path(report, params) }
+
+ it_behaves_like 'moderates user'
+ end
+
describe 'DELETE #destroy' do
let!(:report) { create(:abuse_report) }
let(:params) { {} }
diff --git a/spec/rubocop/cop/database/avoid_inheritance_column_spec.rb b/spec/rubocop/cop/database/avoid_inheritance_column_spec.rb
new file mode 100644
index 00000000000..e009cde8551
--- /dev/null
+++ b/spec/rubocop/cop/database/avoid_inheritance_column_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/database/avoid_inheritance_column'
+
+RSpec.describe RuboCop::Cop::Database::AvoidInheritanceColumn, feature_category: :shared do
+ it 'flags when :inheritance_column is used' do
+ src = <<~SRC
+ self.inheritance_column = 'some_column'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use Single Table Inheritance https://docs.gitlab.com/ee/development/database/single_table_inheritance.html
+ SRC
+
+ expect_offense(src)
+ end
+
+ it 'does not flag when :inheritance_column is set to :_type_disabled' do
+ src = <<~SRC
+ self.inheritance_column = :_type_disabled
+ SRC
+
+ expect_no_offenses(src)
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_details_entity_spec.rb b/spec/serializers/admin/abuse_report_details_entity_spec.rb
index 08bfa57b062..727716d76a4 100644
--- a/spec/serializers/admin/abuse_report_details_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_entity_spec.rb
@@ -134,7 +134,8 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
:content,
:url,
:screenshot,
- :update_path
+ :update_path,
+ :moderate_user_path
])
end
end
diff --git a/spec/services/admin/abuse_report_update_service_spec.rb b/spec/services/admin/abuse_reports/moderate_user_service_spec.rb
index 7069d8ee5c1..6e8a59f4e49 100644
--- a/spec/services/admin/abuse_report_update_service_spec.rb
+++ b/spec/services/admin/abuse_reports/moderate_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::AbuseReportUpdateService, feature_category: :instance_resiliency do
+RSpec.describe Admin::AbuseReports::ModerateUserService, feature_category: :instance_resiliency do
let_it_be_with_reload(:abuse_report) { create(:abuse_report) }
let(:action) { 'ban_user' }
let(:close) { true }