Skip to content

Revamp project deletion setting options for clearer interpretation

Problem to solve

Discerning how and when projects are deleted can be difficult to understand.

Proposal

No major functionality will be changing. Just small tweaks to streamline the user journey, and reducing the opportunity for ambiguous conflicts.

Admin Area

  • Rename field Default project deletion protection to Allowed to delete projects
    • Use radio buttons instead of checkbox
  • Combine Default delayed project deletion and Default deletion delay into Deletion protection
    • Use radio buttons to change state
    • Use a dropdown button to change selection
      • ️ groups and projects
      • groups only
    • Use an input counter that can't be set to ≤ 0
  • When set to none, delete immediately, hide the radio buttons from groups and adjust the help text
Before/After
Before After
Admin_Area_-Full_Preview__Before Admin_Area_-Full_Preview__After

Groups

  • Move setting outside of Permissions into its own group Deletion protection
  • Change the description based on who is allowed to delete projects
  • Use radio buttons to change selection rather than a checkbox
    • 🔘 Keep project for X days
    • ️ None, delete immediately
  • When enforcement is ️ for subgroups, disable the radio buttons and show a locked setting.
Before/After
Before After
Group_-Full_Preview__Before Group_-Full_Preview__After

Open Figma →

Enforced by group Set to none in Admin Area
Enforced_deletion None_delete_immediately

What does success look like, and how can we measure that?

Users should be able to understand what specific settings are impacting and configure their environments easily to match their expectations.

Measurement (proposal): Uptick in the number of GitLab Premium users keeping deletion protection enabled.

Documentation

  • doc/user/admin_area/settings/visibility_and_access_controls.md, docs page, docs page
  • doc/api/projects.md
  • doc/user/admin_area/settings/visibility_and_access_controls.md, docs page

References

Solution Validation (Synthesis)

3 unmoderated usability tests were conducted in UserTesting to validate the ease-of-use associated with the design decisions.

After testing with 15 participants in total, it was clear the tasks were easily completed. However, the ability to switch back to a null state was still confusing to users. This will be removed from the scope of this issue and followed-up with its own research in #350087.

Implementation plan

WIP 🚧

Admin settings

  1. backend Add a new instance setting for delayed_group_deletion (default true) that is seperate from delayed_project_deletion. This is needed because deletion_adjourned_period should only be greater than 1 and thus no longer be used to check if delayed group deletion should be enabled / disabled. We also don't want to override the user specified deletion_adjourned_period even when the user opted to None, delete immediately. Additional context #348332 (comment 840063606).

    1. Add a migration plan to set the value of delayed_group_deletion to deletion_adjourned_period > 0.
    2. When deletion_adjourned_period is set, also set the delayed_group_deletion to deletion_adjourned_period > 0.
    3. Update the Remove group button under Group > Advanced Settings to only show the delayed deletion message and modal when both delayed_group_deletion is true and deletion_adjourned_period is >0. Currently group delayed deletion only depends on deletion_adjourned_period > 0.
    4. Update specs.
    5. Update the applications settings API doc to add delayed_group_deletion setting.
  2. frontend Update _default_project_deletion_protection_setting.html.haml:

    1. Use gitlab_ui_radio_component instead of check_box
    2. Update strings
  3. frontend Update _default_delayed_project_deletion_setting.html.haml:

    1. Update strings
    2. Add help text under form group label
    3. Use gitlab_ui_radio_component instead of check_box for the setting delayed_group_delection
      • Keep deleted = true
      • None, delete immediately = false
    4. Add a select component with the options [['groups only', false], ['groups and projects', true]] to toggle the delayed_project_deletion setting.
    5. Move the deletion_adjourned_period to be inline with the delayed_project_deletion option.
    6. Delete the _default_project_deletion_adjourned_period_setting.html.haml partial.
  4. Update docs.

  5. Update specs.

Note: Here is a rough patch to update the settings HAML. admin_settings.patch

Patch contents
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index e56c898b236..2f1003b1f80 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -7,8 +7,7 @@
 
     = render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
     = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
-    = render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f
-    = render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
+    = render_if_exists 'admin/application_settings/deletion_protection_setting', form: f
     .form-group.visibility-level-setting
       = f.label :default_project_visibility, class: 'label-bold'
       = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
diff --git a/ee/app/views/admin/application_settings/_default_delayed_project_deletion_setting.html.haml b/ee/app/views/admin/application_settings/_default_delayed_project_deletion_setting.html.haml
deleted file mode 100644
index 31b4c75d584..00000000000
--- a/ee/app/views/admin/application_settings/_default_delayed_project_deletion_setting.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- return unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
-
-- f = local_assigns.fetch(:form)
-
-.form-group
-  = f.label _('Default delayed project deletion'), class: 'label-bold'
-  .form-check
-    = f.check_box :delayed_project_deletion, class: 'form-check-input'
-    = f.label :delayed_project_deletion, class: 'form-check-label' do
-      = _('Enable delayed project deletion by default for newly-created groups.')
-      = _('Does not apply to projects in personal namespaces, which are deleted immediately on request.')
diff --git a/ee/app/views/admin/application_settings/_default_project_deletion_adjourned_period_setting.html.haml b/ee/app/views/admin/application_settings/_default_project_deletion_adjourned_period_setting.html.haml
deleted file mode 100644
index 75fe0e9f2f5..00000000000
--- a/ee/app/views/admin/application_settings/_default_project_deletion_adjourned_period_setting.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- return unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
-
-- f = local_assigns.fetch(:form)
-
-.form-group
-  = f.label _('Default deletion delay'), class: 'label-bold'
-  = f.select :deletion_adjourned_period, options_for_select(0..90, @application_setting.deletion_adjourned_period), {}, class: 'form-control gl-form-input'
-  = f.label :deletion_adjourned_period, class: 'form-check-label' do
-    = _('How many days need to pass between marking entity for deletion and actual removing it.')
diff --git a/ee/app/views/admin/application_settings/_default_project_deletion_protection_setting.html.haml b/ee/app/views/admin/application_settings/_default_project_deletion_protection_setting.html.haml
index 44c066335f8..5baf5178375 100644
--- a/ee/app/views/admin/application_settings/_default_project_deletion_protection_setting.html.haml
+++ b/ee/app/views/admin/application_settings/_default_project_deletion_protection_setting.html.haml
@@ -3,8 +3,6 @@
 - f = local_assigns.fetch(:form)
 
 .form-group
-  = f.label _('Default project deletion protection'), class: 'label-bold'
-  .form-check
-    = f.check_box :default_project_deletion_protection, class: 'form-check-input'
-    = f.label :default_project_deletion_protection, class: 'form-check-label' do
-      = _('Only admins can delete project')
+  = f.label _('Allowed to delete projects'), class: 'label-bold'
+  = f.gitlab_ui_radio_component :default_project_deletion_protection, :false, _('Owners and administrators')
+  = f.gitlab_ui_radio_component :default_project_deletion_protection, :true, _('Administrators')
diff --git a/ee/app/views/admin/application_settings/_deletion_protection_setting.html.haml b/ee/app/views/admin/application_settings/_deletion_protection_setting.html.haml
new file mode 100644
index 00000000000..ab92f033e9f
--- /dev/null
+++ b/ee/app/views/admin/application_settings/_deletion_protection_setting.html.haml
@@ -0,0 +1,24 @@
+- return unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
+- deletion_choices = [[_('groups and projects'), :true], [_('only groups'), :false]]
+
+- f = local_assigns.fetch(:form)
+
+.form-group
+  = f.label _('Deletion protection'), class: 'label-bold gl-mb-2'
+  %p.form-text.text-muted
+    = _('Retention period that deleted groups and projects will remain restorable. Personal projects are always deleted immediately. Some groups can opt-out their projects.')
+    = link_to _('Learn more.'), help_page_path('user/admin_area/settings/visibility_and_access_controls', anchor: 'default-delayed-project-deletion'), target: '_blank', rel: 'noopener noreferrer'
+  .gl-mt-4
+    .gl-display-flex.gl-flex-direction-row.gl-align-items-baseline.gl-mb-3
+      -# TODO: Change this to delayed_group_delection true
+      = f.gitlab_ui_radio_component :delayed_project_deletion, :true, _('Keep deleted'), label_options: { class: 'gl-white-space-nowrap gl-mr-3' }
+      = f.select :delayed_project_deletion, deletion_choices, {}, { disabled: false, class: 'form-control gl-mr-3 gl-w-auto gl-p-0 gl-h-6!' }
+      %span.gl-mr-3=_('for')
+      .input-group.gl-w-auto.input-group-sm
+        = f.number_field :deletion_adjourned_period, min: 1, class: 'form-control gl-form-input gl-py-2 gl-px-3 gl-w-11! gl-h-6!'
+        .input-group-append
+          .input-group-text{ class: 'gl-h-6!' }
+            %span.gl-line-height-normal=_('days')
+    -# TODO: Change this to delayed_group_delection false
+    = f.gitlab_ui_radio_component :delayed_project_deletion, :false, _('None, delete immediately')
+

Group settings

  1. frontend Update _delayed_project_removal.html.haml to match the designs:

    1. Add the subheading Deletion protection
    2. When the settings are locked, render a lock icon with a popover next to the subheading.
      1. When locked by an admin, show the admin message.
      2. When locked by a group owner, show the group message.
    3. Add the help text Owners and administrators can delete projects.
      1. When locked by admin set the text to Only administrators can delete projects.. And when set to delete immediately, add All projects are deleted immediately. to the help text.
      2. Add a Learn more. link to the help path.
    4. When deletion is not limited to just administrators:
      1. Render the delayed_project_removal setting with radio buttons.
      2. Render the enforcement_checkbox and when the setting is locked disable the checkbox instead.
  2. Update docs.

  3. Update specs.

Note: Here is a rough patch with the interface changes. group_settings.patch

Patch contents
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
index cfa87351689..bbcb4f3a3cd 100644
--- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -2,7 +2,7 @@
 - group = local_assigns.fetch(:group, nil)
 - form = local_assigns.fetch(:form, nil)
 - setting_locked = local_assigns.fetch(:setting_locked, false)
-- help_text = local_assigns.fetch(:help_text, s_('CascadingSettings|Subgroups cannot change this setting.'))
+- disabled = local_assigns.fetch(:disabled, false)
 
 - return unless attribute && group && form
 - return if setting_locked
@@ -10,9 +10,7 @@
 - lock_attribute = "lock_#{attribute}"
 
 .gl-form-checkbox.custom-control.custom-checkbox
-  = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
-  = form.label lock_attribute, class: 'custom-control-label' do
+  = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }, disabled: disabled
+  = form.label lock_attribute, class: 'custom-control-label', aria: { disabled: disabled } do
     %span
-      = yield.presence || s_('CascadingSettings|Enforce for all subgroups')
-    %p.help-text
-      = help_text
+      = yield.presence || s_('CascadingSettings|Enforce deletion protection for all subgroups')
diff --git a/ee/app/views/groups/settings/_delayed_project_removal.html.haml b/ee/app/views/groups/settings/_delayed_project_removal.html.haml
index e6b625a6a51..4e8948f5c56 100644
--- a/ee/app/views/groups/settings/_delayed_project_removal.html.haml
+++ b/ee/app/views/groups/settings/_delayed_project_removal.html.haml
@@ -2,18 +2,29 @@
 
 - setting_locked = cascading_namespace_setting_locked?(:delayed_project_removal, group)
 
+%h5.gl-mb-2.gl-display-flex
+  = _('Deletion protection')
+  - if setting_locked
+    %span.gl-outline-0.gl-ml-3{ tabindex: "0", data: { container: "body",
+      toggle: "popover",
+      placement: "top",
+      html: "true",
+      trigger: "focus",
+      title: _('Setting enforced'),
+      content: _('This setting has been enforced by an instance administrator.')
+    } }
+      = sprite_icon('lock', size: 16)
+%p.form-text.text-muted{ class: 'gl-mb-4!' }
+  = delayed_project_removal_help_text
+  = link_to _('Learn more.'), '#', target: '_blank', rel: 'noopener noreferrer'
 .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: setting_locked, class: 'custom-control-input', data: { testid: 'delayed-project-removal-checkbox' }
-    = render 'shared/namespaces/cascading_settings/setting_label_checkbox', attribute: :delayed_project_removal,
-        group: group,
-        form: f,
-        setting_locked: setting_locked,
-        settings_path_helper: -> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') },
-        help_text: delayed_project_removal_help_text do
-      = s_('GroupSettings|Enable delayed project deletion')
-    = render 'shared/namespaces/cascading_settings/enforcement_checkbox',
-        attribute: :delayed_project_removal,
-        group: group,
-        form: f,
-        setting_locked: setting_locked
+  = render 'shared/namespaces/cascading_settings/deletion_protection',
+    attribute: :delayed_project_removal,
+    group: group,
+    form: f,
+    setting_locked: setting_locked
+  = render 'shared/namespaces/cascading_settings/enforcement_checkbox',
+      attribute: :delayed_project_removal,
+      group: group,
+      form: f,
+      disabled: setting_locked
Edited by Jiaan Louw