gitlab-org--gitlab-foss/doc/development/cascading_settings.md

247 lines
18 KiB
Markdown
Raw Normal View History

---
stage: Manage
group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Cascading Settings
> Introduced in [GitLab 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/321724).
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.
```ruby
class NamespaceSetting
include CascadingNamespaceSettingAttribute
cascading_attr :delayed_project_removal
end
```
1. 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`.
```ruby
class AddDelayedProjectRemovalCascadingSetting < Gitlab::Database::Migration[1.0]
include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings
enable_lock_retries!
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.
1. 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:**
```ruby
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.
1. Return locked ancestor value.
1. Return locked instance-level application settings value.
1. Return this namespace's attribute, if not nil.
1. Return value from nearest ancestor where value is not nil.
1. 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?`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/helpers/namespaces_helper.rb#L86)
Calls through to the [`_locked?` method](#_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`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/group.rb) | `true` |
| `**args` | Additional arguments to pass through to the [`_locked?` method](#_locked-method) | | `false` |
### HAML partials
[`_enforcement_checkbox.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/views/shared/namespaces/cascading_settings/_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` |
| `group` | Current group. | [`Group`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/group.rb) | `true` |
| `form` | [Rails FormBuilder object](https://apidock.com/rails/ActionView/Helpers/FormBuilder). | [`ActionView::Helpers::FormBuilder`](https://apidock.com/rails/ActionView/Helpers/FormBuilder) | `true` |
| `setting_locked` | If the setting is locked by an ancestor group or administrator setting. Can be calculated with [`cascading_namespace_setting_locked?`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/helpers/namespaces_helper.rb#L86). | `Boolean` | `true` |
| `help_text` | Text shown below the checkbox. | `String` | `false` (Subgroups cannot change this setting.) |
[`_setting_label_checkbox.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/views/shared/namespaces/cascading_settings/_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` |
| `group` | Current group. | [`Group`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/group.rb) | `true` |
| `form` | [Rails FormBuilder object](https://apidock.com/rails/ActionView/Helpers/FormBuilder). | [`ActionView::Helpers::FormBuilder`](https://apidock.com/rails/ActionView/Helpers/FormBuilder) | `true` |
| `setting_locked` | If the setting is locked by an ancestor group or administrator setting. Can be calculated with [`cascading_namespace_setting_locked?`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/helpers/namespaces_helper.rb#L86). | `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`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/views/shared/namespaces/cascading_settings/_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` |
| `group` | Current group. | [`Group`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/group.rb) | `true` |
| `setting_locked` | If the setting is locked. Can be calculated with [`cascading_namespace_setting_locked?`](https://gitlab.com/gitlab-org/gitlab/-/blob/c2736823b8e922e26fd35df4f0cd77019243c858/app/helpers/namespaces_helper.rb#L86). | `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`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/views/shared/namespaces/cascading_settings/_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`](https://gitlab.com/gitlab-org/gitlab/-/blob/b73353e47e283a7d9c9eda5bdedb345dcfb685b6/app/assets/javascripts/namespaces/cascading_settings/index.js#L4)
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](fe_guide/performance.md#page-specific-javascript).
### Put it all together
```haml
-# 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 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.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
```
```javascript
// app/assets/javascripts/pages/groups/edit/index.js
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
initCascadingSettingsLockPopovers();
```