Cascading Settings

Introduced in GitLab 13.11.

The cascading settings framework allows groups to essentially inherit settings values from ancestors (parent group on up the group hierarchy) and from instance-level application settings. The framework also allows settings values to be enforced on groups lower in the hierarchy.

Cascading settings can currently only be defined within NamespaceSetting, though the framework may be extended to other objects in the future.

Add a new cascading setting

Settings are not cascading by default. To define a cascading setting, take the following steps:

  1. In the NamespaceSetting model, define the new attribute using the cascading_attr helper method. You can use an array to define multiple attributes on a single line.

    class NamespaceSetting
      include CascadingNamespaceSettingAttribute
    
      cascading_attr :delayed_project_removal
    end
  2. Create the database columns.

    You can use the following database migration helper for a completely new setting. The helper creates four columns, two each in namespace_settings and application_settings.

    class AddDelayedProjectRemovalCascadingSetting < ActiveRecord::Migration[6.0]
      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

    Existing settings being converted to a cascading setting will require individual migrations to add columns and change existing columns. Use the specifications below to create migrations as required:

    1. Columns in namespace_settings table:
      • delayed_project_removal: No default value. Null values allowed. Use any column type.
      • lock_delayed_project_removal: Boolean column. Default value is false. Null values not allowed.
    2. Columns in application_settings table:
      • delayed_project_removal: Type matching for the column created in namespace_settings. Set default value as desired. Null values not allowed.
      • lock_delayed_project_removal: Boolean column. Default value is false. Null values not allowed.

Convenience methods

By defining an attribute using the cascading_attr method, a number of convenience methods are automatically defined.

Definition:

cascading_attr :delayed_project_removal

Convenience Methods Available:

  • 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? (Boolean attributes only)
  • delayed_project_removal_locked_ancestor (Returns locked namespace settings object [namespace_id])

Attribute reader method (delayed_project_removal)

The attribute reader method (delayed_project_removal) returns the correct cascaded value using the following criteria:

  1. Returns the dirty value, if the attribute has changed. This allows standard Rails validators to be used on the attribute, though nil values must be allowed.
  2. Return locked ancestor value.
  3. Return locked instance-level application settings value.
  4. Return this namespace's attribute, if not nil.
  5. Return value from nearest ancestor where value is not nil.
  6. Return instance-level application setting.

_locked? method

By default, the _locked? method (delayed_project_removal_locked?) returns true if an ancestor of the group or application setting locks the attribute. It returns false when called from the group that locked the attribute.

When include_self: true is specified, it returns true when called from the group that locked the attribute. This would be relevant, for example, when checking if an attribute is locked from a project.

Display cascading settings on the frontend

There are a few Rails view helpers, HAML partials, and JavaScript functions that can be used to display a cascading setting on the frontend.

Rails view helpers

cascading_namespace_setting_locked?

Calls through to the _locked? method to check if the setting is locked.

Argument Description Type Required (default value)
attribute Name of the setting. For example, :delayed_project_removal. String or Symbol true
group Current group. Group true
**args Additional arguments to pass through to the _locked? method false

HAML partials

_enforcement_checkbox.html.haml

Renders the enforcement checkbox.

Local Description Type Required (default value)
attribute Name of the setting. For example, :delayed_project_removal. String or Symbol true
form Rails FormBuilder object. ActionView::Helpers::FormBuilder true
setting_locked If the setting is locked by an ancestor group or admin setting. Can be calculated with cascading_namespace_setting_locked?. Boolean true
help_text Text shown below the checkbox. String false (Subgroups cannot change this setting.)

_setting_label_checkbox.html.haml

Renders the label for a checkbox setting.

Local Description Type Required (default value)
attribute Name of the setting. For example, :delayed_project_removal. String or Symbol true
form Rails FormBuilder object. ActionView::Helpers::FormBuilder true
setting_locked If the setting is locked by an ancestor group or admin setting. Can be calculated with cascading_namespace_setting_locked?. Boolean true
settings_path_helper Lambda function that generates a path to the ancestor setting. For example, settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') } Lambda true
help_text Text shown below the checkbox. String false (nil)

_setting_label_fieldset.html.haml

Renders the label for a fieldset setting.

Local Description Type Required (default value)
attribute Name of the setting. For example, :delayed_project_removal. String or Symbol true
setting_locked If the setting is locked. Can be calculated with cascading_namespace_setting_locked?. Boolean true
settings_path_helper Lambda function that generates a path to the ancestor setting. For example, -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') } Lambda true
help_text Text shown below the checkbox. String false (nil)

_lock_popovers.html.haml

Renders the mount element needed to initialize the JavaScript used to display the popover when hovering over the lock icon. This partial is only needed once per page.

JavaScript

initCascadingSettingsLockPopovers

Initializes the JavaScript needed to display the popover when hovering over the lock icon ({lock}). This function should be imported and called in the page-specific JavaScript.

Put it all together

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

= render 'shared/namespaces/cascading_settings/lock_popovers'

- 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' } }
    .gl-form-checkbox.custom-control.custom-checkbox
      = f.check_box :delayed_project_removal, checked: @group.namespace_settings.delayed_project_removal?, disabled: delayed_project_removal_locked, class: 'custom-control-input'
      = render 'shared/namespaces/cascading_settings/setting_label_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 removal')
      = 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.radio_button :merge_method, :merge, class: "custom-control-input", disabled: merge_method_locked
      = f.label :merge_method_merge, class: 'custom-control-label' do
        = s_('Settings|Merge commit')
        %p.help-text
          = s_('Settings|Every merge creates a merge commit.')

    .gl-form-radio.custom-control.custom-radio
      = f.radio_button :merge_method, :rebase_merge, class: "custom-control-input", disabled: merge_method_locked
      = f.label :merge_method_rebase_merge, class: 'custom-control-label' do
        = s_('Settings|Merge commit with semi-linear history')
        %p.help-text
          = s_('Settings|Every merge creates a merge commit.')

    .gl-form-radio.custom-control.custom-radio
      = f.radio_button :merge_method, :ff, class: "custom-control-input", disabled: merge_method_locked
      = f.label :merge_method_ff, class: 'custom-control-label' do
        = s_('Settings|Fast-forward merge')
        %p.help-text
          = s_('Settings|No merge commits are created.')

    = 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 { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';

initCascadingSettingsLockPopovers();