Cascading Settings
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:
-
In the
NamespaceSetting
model, define the new attribute using thecascading_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
-
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
andapplication_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
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:
- 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.
-
- Columns in
application_settings
table:-
delayed_project_removal
: Type matching for the column created innamespace_settings
. Set default value as desired. Null values not allowed. -
lock_delayed_project_removal
: Boolean column. Default value is false. Null values not allowed.
-
- Columns in
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]
)
delayed_project_removal
)
Attribute reader method (The attribute reader method (delayed_project_removal
) returns the correct
cascaded value using the following criteria:
- 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. - Return locked ancestor value.
- Return locked instance-level application settings value.
- Return this namespace's attribute, if not nil.
- Return value from nearest ancestor where value is not nil.
- 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 |
group |
Current group. | Group |
true |
form |
Rails FormBuilder object. | 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? . |
Boolean |
true |
help_text |
Text shown below the checkbox. | String |
false (Subgroups cannot change this setting.) |
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 |
true |
form |
Rails FormBuilder object. | 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? . |
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 |
group |
Current group. | Group |
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 ) |
Renders the mount element needed to initialize the JavaScript used to display the tooltip when hovering over the lock icon. This partial is only needed once per page.
JavaScript
initCascadingSettingsLockTooltips
Initializes the JavaScript needed to display the tooltip 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_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
Local | Description | Type | Required (default value) |
---|---|---|---|
ancestorNamespace |
The namespace for associated group's ancestor. | Object |
false (null ) |
isLockedByApplicationSettings |
Boolean for if the cascading variable locked_by_application_settings is set or not on the instance. |
Boolean |
true |
isLockedByGroupAncestor |
Boolean for if the cascading variable locked_by_ancestor is set or not for a group. |
Boolean |
true |
Using Vue
- In the your Ruby helper, you will need to call the following to send do your Vue component. Be sure to switch out
:replace_attribute_here
with your cascading attribute.
# Example call from your Ruby helper method for groups
cascading_settings_data = cascading_namespace_settings_tooltip_data(:replace_attribute_here, @group, method(:edit_group_path))[:tooltip_data]
# Example call from your Ruby helper method for projects
cascading_settings_data = project_cascading_namespace_settings_tooltip_data(:duo_features_enabled, project, method(:edit_group_path)).to_json
- From your Vue's
index.js
file, be sure to convert the data into JSON and camel case format. This will make it easier to use in Vue.
let cascadingSettingsDataParsed;
try {
cascadingSettingsDataParsed = convertObjectPropsToCamelCase(JSON.parse(cascadingSettingsData), {
deep: true,
});
} catch {
cascadingSettingsDataParsed = null;
}
- From your Vue component, either
provide/inject
or pass yourcascadingSettingsDataParsed
variable to the component. You will also want to have a helper method to not show thecascading-lock-icon
component if the cascading data returned is either null or an empty object.
// ./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>
You can look into the following examples of MRs for implementing cascading_lock_icon.vue
into other Vue components:
Reasoning for supporing both HAML and Vue
It is the goal to build all new frontend features in Vue and to eventually move away from building features in HAML. However there are still HAML frontend features that utilize cascading settings, so support will remain with initCascadingSettingsLockTooltips
until those components have been migrated into Vue.