summaryrefslogtreecommitdiff
path: root/doc/development/cascading_settings.md
blob: 631de54423809b6ffab84363fb9170bec295883d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
---
stage: Manage
group: Access
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 < 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.
    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`                                          |
| `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 admin 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`                   |
| `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 admin 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`                   |
| `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 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
```

```javascript
// app/assets/javascripts/pages/groups/edit/index.js

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

initCascadingSettingsLockPopovers();
```