Help us learn about your current experience with the documentation. Take the survey.

级联设置

您是否曾在 GitLab 项目和/或组中添加过设置,该设置具有从父级继承的默认值?

如果是这样:我们提供了您需要的框架!

级联设置框架允许组和项目从祖先(组层级中的父组及以上)和实例级应用设置中继承设置值。该框架还允许在层级较低的组上“锁定”(强制执行)设置值。

历史上,级联设置仅在 ApplicationSettingNamespaceSettingProjectSetting 上定义,尽管该框架未来可能会扩展到其他对象。

仅向组添加新的级联设置

设置默认不会级联。要定义级联设置,请执行以下步骤:

  1. NamespaceSetting 模型中,使用 cascading_attr 辅助方法定义新属性。您可以使用数组在一行上定义多个属性。

    class NamespaceSetting
      include CascadingNamespaceSettingAttribute
    
      cascading_attr :delayed_project_removal
    end
  2. 创建数据库列。

    对于全新的设置,您可以使用以下数据库迁移辅助工具。该工具会创建四列,分别在 namespace_settingsapplication_settings 中各两列。

    class AddDelayedProjectRemovalCascadingSetting < Gitlab::Database::Migration[2.1]
      include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings
    
      def up
        add_cascading_namespace_setting :delayed_project_removal, :boolean, default: false, null: false
      end
    
      def down
       remove_cascading_namespace_setting :delayed_project_removal
      end
    end

    将现有设置转换为级联设置需要单独的迁移来添加列和更改现有列。根据以下规范创建所需的迁移:

    1. namespace_settings 表中的列:
      • delayed_project_removal:无默认值。允许空值。使用任意列类型。
      • lock_delayed_project_removal:布尔列。默认值为 false。不允许空值。
    2. application_settings 表中的列:
      • delayed_project_removal:类型与在 namespace_settings 中创建的列匹配。根据需要设置默认值。不允许空值。
      • lock_delayed_project_removal:布尔列。默认值为 false。不允许空值。

便捷方法

通过使用 cascading_attr 方法定义属性,会自动生成多个便捷方法。

定义

cascading_attr :delayed_project_removal

可用的便捷方法

  • delayed_project_removal
  • delayed_project_removal=
  • delayed_project_removal_locked?
  • delayed_project_removal_locked_by_ancestor?
  • delayed_project_removal_locked_by_application_setting?
  • delayed_project_removal?(仅限布尔属性)
  • delayed_project_removal_locked_ancestor(返回被锁定的命名空间设置对象 [namespace_id]

属性读取方法 (delayed_project_removal)

属性读取方法 (delayed_project_removal) 使用以下标准返回正确的级联值:

  1. 如果属性已更改,则返回脏值。这允许在属性上使用标准的 Rails 验证器,但必须允许 nil 值。
  2. 返回被锁定的祖先值。
  3. 返回被锁定的实例级应用设置值。
  4. 返回此命名空间的属性(如果非空)。
  5. 返回值不为空的最近祖先的值。
  6. 返回实例级应用设置。

_locked? 方法

默认情况下,_locked? 方法 (delayed_project_removal_locked?) 如果组的祖先或应用设置锁定该属性,则返回 true。当从锁定该属性的组调用时,它返回 false

当指定 include_self: true 时,从锁定该属性的组调用时返回 true。例如,当从项目检查属性是否被锁定时,这会相关。

向项目添加新的级联设置

背景

级联设置框架的第一个迭代仅用于实例和组级设置。

后来,也需要将此设置添加到项目中。GitLab 中的项目也有命名空间,因此您可能会认为通过使用为组级设置添加的 namespace_settings 表中的同一列来扩展现有框架会很容易。但事实证明,将级联项目设置添加到 project_settings 表更有意义。

为什么您可能会问?嗯,事实证明:

  • GitLab 中的每个用户、项目和组都属于一个命名空间
  • 命名空间 has_one namespace_settings 记录
  • 创建组或用户时,其命名空间和命名空间设置通过服务对象创建(代码)。
  • 创建项目时,会创建命名空间,但不会创建命名空间设置。

此外,我们在 GitLab UI 的任何地方都不暴露项目级命名空间设置。相反,我们使用项目设置。有一天,我们希望能够将命名空间设置用于项目设置。但今天,将项目级设置添加到 project_settings 表更容易。

实现

向项目添加级联设置的示例在 MR 149931 中。

写入时的级联设置值

在新推荐的方式中,唯一在数据库级别实际级联值的级联设置是 duo_features_enabled。该设置从组级联到项目。问题 505335 描述了从应用级到组级添加此级联。

传统的级联设置写入

在级联设置框架的第一个迭代中,“级联”发生在应用代码级别,而不是数据库级别。其工作方式是 application_settings 表中的设置值具有默认值。在 namespace_settings 级别则没有。因此,命名空间在数据库级别具有 nil 值,但“继承” application_settings 值。

如果组更新为具有新的设置值,则该值优先于 application_settings 级别的默认值。任何子组将继承父组的设置值,因为它们在数据库级别也具有 nil 值,但从 namespace_settings 表继承父值。但是,如果其中一个子组更新了该设置,则会覆盖父组。

这引入了一些可能令人困惑的逻辑。

如果 application_settings 级别的设置值更改:

  • 任何设置为 nil 的顶级组将继承新值。
  • 任何设置为非 nil 值的顶级组将不会继承新值。

如果 namespace_settings 级别的设置值更改:

  • 任何设置为 nil 的子组或项目将从父组继承新值。
  • 任何设置为非 nil 值的子组或项目不会从父组继承新值。

由于数据库级别的值在 UI 或通过 API 中都不可见(因为两者都显示继承的值),实例或组管理员可能不理解哪些组/项目继承了该值。

不一致级联行为的例外是如果设置被 locked。这总是“强制”继承。

除了令人困惑的逻辑之外,这还会在读取值时造成性能问题:如果查询深度嵌套层级的设置值,可能需要读取整个层级的设置值才能知道该设置值。

未来级联设置写入的建议

为了提供更清晰的逻辑链并提高性能,您应该为新添加的级联设置添加默认值,并在设置值更新时对层级中的所有子对象执行写入。这需要启动一个作业,以便更新异步发生。如何执行此操作的示例在 MR 145876 中。

先前添加的级联设置仍然具有默认的 nil 值,并读取祖先层级以查找继承的设置值。但为了最小化混淆,我们应该更新它们以在写入时级联。问题 483143 描述了此维护任务。

在前端显示级联设置

有几个 Rails 视图辅助工具、HAML 部分和 JavaScript 函数可用于在前端显示级联设置。

Rails 视图辅助工具

cascading_namespace_setting_locked?

调用 _locked? 方法 检查设置是否被锁定。

参数 说明 类型 必需(默认值)
attribute 设置的名称。例如 :delayed_project_removal StringSymbol true
group 当前组。 Group true
**args 传递给 _locked? 方法 的额外参数。 false

HAML 部分

_enforcement_checkbox.html.haml

渲染强制复选框。

本地变量 说明 类型 必需(默认值)
attribute 设置的名称。例如 :delayed_project_removal StringSymbol true
group 当前组。 Group true
form Rails FormBuilder 对象 ActionView::Helpers::FormBuilder true
setting_locked 如果设置被祖先组或管理员设置锁定。可以使用 cascading_namespace_setting_locked? 计算。 Boolean true
help_text 复选框下方显示的文本。 String false(子组无法更改此设置。)

_setting_checkbox.html.haml

渲染复选框设置的标签。

本地变量 说明 类型 必需(默认值)
attribute 设置的名称。例如 :delayed_project_removal StringSymbol true
group 当前组。 Group true
form Rails FormBuilder 对象 ActionView::Helpers::FormBuilder true
setting_locked 如果设置被祖先组或管理员设置锁定。可以使用 cascading_namespace_setting_locked? 计算。 Boolean true
settings_path_helper 生成到祖先设置路径的 Lambda 函数。例如 settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') } Lambda true
help_text 复选框下方显示的文本。 String false (nil)

_setting_label_fieldset.html.haml

渲染 fieldset 设置的标签。

本地变量 说明 类型 必需(默认值)
attribute 设置的名称。例如 :delayed_project_removal StringSymbol true
group 当前组。 Group true
setting_locked 如果设置被锁定。可以使用 cascading_namespace_setting_locked? 计算。 Boolean true
settings_path_helper 生成到祖先设置路径的 Lambda 函数。例如 -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') } Lambda true
help_text 复选框下方显示的文本。 String false (nil)

_lock_tooltips.html.haml

渲染初始化 JavaScript 所需的挂载元素,用于在悬停锁图标时显示工具提示。此部分每个页面只需一次。

JavaScript

initCascadingSettingsLockTooltips

初始化在悬停锁图标( lock )时显示工具提示所需的 JavaScript。此函数应在页面特定 JavaScript 中导入并调用。

整合使用

-# app/views/groups/edit.html.haml

= render 'shared/namespaces/cascading_settings/lock_tooltips'

- delayed_project_removal_locked = cascading_namespace_setting_locked?(:delayed_project_removal, @group)
- merge_method_locked = cascading_namespace_setting_locked?(:merge_method, @group)

= form_for @group do |f|
  .form-group{ data: { testid: 'delayed-project-removal-form-group' } }
    = render 'shared/namespaces/cascading_settings/setting_checkbox', attribute: :delayed_project_removal,
        group: @group,
        form: f,
        setting_locked: delayed_project_removal_locked,
        settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') },
        help_text: s_('Settings|Projects will be permanently deleted after a 7-day delay. Inherited by subgroups.') do
      = s_('Settings|Enable delayed project deletion')
    = render 'shared/namespaces/cascading_settings/enforcement_checkbox',
        attribute: :delayed_project_removal,
        group: @group,
        form: f,
        setting_locked: delayed_project_removal_locked

  %fieldset.form-group
    = render 'shared/namespaces/cascading_settings/setting_label_fieldset', attribute: :merge_method,
        group: @group,
        setting_locked: merge_method_locked,
        settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') },
        help_text: s_('Settings|Determine what happens to the commit history when you merge a merge request.') do
      = s_('Settings|Merge method')

    .gl-form-radio.custom-control.custom-radio
      = f.gitlab_ui_radio_component :merge_method, :merge, s_('Settings|Merge commit'), help_text: s_('Settings|Every merge creates a merge commit.'), radio_options: { disabled: merge_method_locked }

    .gl-form-radio.custom-control.custom-radio
      = f.gitlab_ui_radio_component :merge_method, :rebase_merge, s_('Settings|Merge commit with semi-linear history'), help_text: s_('Settings|Every merge creates a merge commit.'), radio_options: { disabled: merge_method_locked }

    .gl-form-radio.custom-control.custom-radio
      = f.gitlab_ui_radio_component :merge_method, :ff, s_('Settings|Fast-forward merge'), help_text: s_('Settings|No merge commits are created.'), radio_options: { disabled: merge_method_locked }

    = render 'shared/namespaces/cascading_settings/enforcement_checkbox',
      attribute: :merge_method,
      group: @group,
      form: f,
      setting_locked: merge_method_locked
// app/assets/javascripts/pages/groups/edit/index.js

import { initCascadingSettingsLockTooltips } from '~/namespaces/cascading_settings';

initCascadingSettingsLockTooltips();

Vue

cascading_lock_icon.vue

本地变量 说明 类型 必需(默认值)
ancestorNamespace 关联组的祖先命名空间。 Object false (null)
isLockedByApplicationSettings 实例上是否设置了级联变量 locked_by_application_settings 的布尔值。 Boolean true
isLockedByGroupAncestor 组的级联变量 locked_by_ancestor 是否设置的布尔值。 Boolean true

使用 Vue

  1. 在您的 Ruby 辅助工具中,您需要调用以下内容来发送到您的 Vue 组件。请务必将 :replace_attribute_here 替换为您的级联属性。
# 从您的 Ruby 辅助工具方法(针对组)调用的示例
cascading_settings_data = cascading_namespace_settings_tooltip_data(:replace_attribute_here, @group, method(:edit_group_path))[:tooltip_data]
# 从您的 Ruby 辅助工具方法(针对项目)调用的示例
cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path)).to_json
  1. 从您的 Vue 的 index.js 文件中,请务必将数据转换为 JSON 和驼峰格式。这将使其在 Vue 中更易于使用。
let cascadingSettingsDataParsed;
try {
  cascadingSettingsDataParsed = convertObjectPropsToCamelCase(JSON.parse(cascadingSettingsData), {
    deep: true,
  });
} catch {
  cascadingSettingsDataParsed = null;
}
  1. 从您的 Vue 组件中,通过 provide/inject 或将 cascadingSettingsDataParsed 变量传递给组件。您还需要一个辅助方法,如果级联数据返回 null 或空对象,则不显示 cascading-lock-icon 组件。
// ./ee/my_component.vue

<script>
export default {
  computed: {
    showCascadingIcon() {
      return (
        this.cascadingSettingsData &&
        Object.keys(this.cascadingSettingsData).length
      );
    },
  },
}
</script>

<template>
  <cascading-lock-icon
    v-if="showCascadingIcon"
    :is-locked-by-group-ancestor="cascadingSettingsData.lockedByAncestor"
    :is-locked-by-application-settings="cascadingSettingsData.lockedByApplicationSetting"
    :ancestor-namespace="cascadingSettingsData.ancestorNamespace"
    class="gl-ml-1"
  />
</template>

您可以查看以下 MR 示例,了解如何将 cascading_lock_icon.vue 实现到其他 Vue 组件中:

同时支持 HAML 和 Vue 的原因

目标是用 Vue 构建所有新的前端功能,并最终停止在 HAML 中构建功能。但是,仍然有使用级联设置的 HAML 前端功能,因此支持将保留 initCascadingSettingsLockTooltips,直到这些组件迁移到 Vue 为止。