Clean up the UI for the protected environments settings
Problem to solve
The UI of the protected environments settings is a bit messy when items are missing (group inheritance toggle, the lack of photo for maintainers). We need to clean it up for these cases:
UX Proposal
Move the Approval Rules section into a table and remove the space for when the first column has no avatar:
| Design |
|---|
![]() |
Add a loading state to the table as approval rules load:
| Design |
|---|
![]() |
Implementation guide
- Update
edit_protected_environments_list.vueto replace the current list-based layout with GlTableLite component.- use the current template as a guide on adding table cell templates
- use not labeled cell for the actions (example:
<template #cell(actions)="{ item: rule }">) - keep the
empty-statetemplate to show a message if the table has no content
Example of the updated code
diff --git a/ee/app/assets/javascripts/protected_environments/edit_protected_environments_list.vue b/ee/app/assets/javascripts/protected_environments/edit_protected_environments_list.vue
index 6538ceb2b1d1..f9ab78c51b82 100644
--- a/ee/app/assets/javascripts/protected_environments/edit_protected_environments_list.vue
+++ b/ee/app/assets/javascripts/protected_environments/edit_protected_environments_list.vue
@@ -8,6 +8,7 @@ import {
GlTooltipDirective as GlTooltip,
GlToggle,
GlSprintf,
+ GlTableLite,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
@@ -16,7 +17,14 @@ import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
import GroupsAccessDropdown from '~/groups/settings/components/access_dropdown.vue';
import ShowMore from '~/vue_shared/components/show_more.vue';
import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue';
-import { ACCESS_LEVELS, DEPLOYER_RULE_KEY, APPROVER_RULE_KEY, INHERITED_GROUPS } from './constants';
+import {
+ ACCESS_LEVELS,
+ DEPLOYER_RULE_KEY,
+ APPROVER_RULE_KEY,
+ INHERITED_GROUPS,
+ APPROVER_FIELDS,
+ DEPLOYER_FIELDS,
+} from './constants';
import EditProtectedEnvironmentRulesCard from './edit_protected_environment_rules_card.vue';
import AddRuleModal from './add_rule_modal.vue';
import AddApprovers from './add_approvers.vue';
@@ -30,6 +38,7 @@ export default {
GlFormInput,
GlIcon,
GlToggle,
+ GlTableLite,
AccessDropdown,
GroupsAccessDropdown,
ProtectedEnvironments,
@@ -127,6 +136,8 @@ export default {
DEPLOYER_RULE_KEY,
APPROVER_RULE_KEY,
AVATAR_LIMIT: 5,
+ approverFields: APPROVER_FIELDS,
+ deployerFields: DEPLOYER_FIELDS,
};
</script>
<template>
@@ -181,47 +192,55 @@ export default {
:environment="environment"
:rule-key="$options.DEPLOYER_RULE_KEY"
:data-testid="`protected-environment-${environment.name}-deployers`"
- class="gl-border gl-rounded-t-base gl-border-b-initial"
+ class="gl-border-t"
@addRule="addRule"
>
- <template #card-header>
- <span class="gl-w-3/10">{{ $options.i18n.deployersHeader }}</span>
- <span class="">{{ $options.i18n.usersHeader }}</span>
- </template>
- <template #rule="{ rule, ruleKey }">
- <span class="gl-w-3/10" data-testid="rule-description">
- {{ rule.access_level_description }}
- </span>
+ <template #table>
+ <gl-table-lite
+ :fields="$options.deployerFields"
+ :items="environment[$options.DEPLOYER_RULE_KEY]"
+ stacked="md"
+ >
+ <template #cell(deployers)="{ item: rule }">
+ <span data-testid="rule-description">
+ {{ rule.access_level_description }}
+ </span>
+ </template>
- <div class="gl-w-1/2">
- <show-more
- #default="{ item }"
- :limit="$options.AVATAR_LIMIT"
- :items="getUsersForRule(rule, ruleKey)"
- >
- <gl-avatar
- :key="item.id"
- v-gl-tooltip
- :src="item.avatar_url"
- :title="item.name"
- :size="24"
- class="gl-mr-2"
- />
- </show-more>
- </div>
+ <template #cell(users)="{ item: rule }">
+ <show-more
+ #default="{ item }"
+ :limit="$options.AVATAR_LIMIT"
+ :items="getUsersForRule(rule, $options.DEPLOYER_RULE_KEY)"
+ >
+ <gl-avatar
+ :key="item.id"
+ v-gl-tooltip
+ :src="item.avatar_url"
+ :title="item.name"
+ :size="24"
+ class="gl-mr-2"
+ />
+ </show-more>
+ </template>
- <gl-button
- v-if="canDeleteDeployerRules(environment)"
- v-gl-tooltip
- category="secondary"
- variant="danger"
- icon="remove"
- :loading="loading"
- :title="$options.i18n.deployerDeleteButtonTitle"
- :aria-label="$options.i18n.deployerDeleteButtonTitle"
- class="gl-ml-auto"
- @click="deleteRule({ environment, rule, ruleKey })"
- />
+ <template #cell(actions)="{ item: rule }">
+ <div class="gl-justify-content-end gl-flex">
+ <gl-button
+ v-if="canDeleteDeployerRules(environment)"
+ v-gl-tooltip
+ category="secondary"
+ variant="danger"
+ icon="remove"
+ :loading="loading"
+ :title="$options.i18n.deployerDeleteButtonTitle"
+ :aria-label="$options.i18n.deployerDeleteButtonTitle"
+ class="gl-ml-auto"
+ @click="deleteRule({ environment, rule, ruleKey: $options.DEPLOYER_RULE_KEY })"
+ />
+ </div>
+ </template>
+ </gl-table-lite>
</template>
</edit-protected-environment-rules-card>
<edit-protected-environment-rules-card
@@ -230,23 +249,131 @@ export default {
:environment="environment"
:rule-key="$options.APPROVER_RULE_KEY"
:data-testid="`protected-environment-${environment.name}-approvers`"
- class="gl-border gl-rounded-bl-base gl-rounded-br-base"
+ class="gl-border-t"
@addRule="addRule"
>
- <template #card-header>
- <span class="gl-w-3/10">{{ $options.i18n.approversHeader }}</span>
- <span class="gl-w-2/10">{{ $options.i18n.usersHeader }}</span>
- <span class="gl-w-2/10">{{ $options.i18n.approvalsHeader }}</span>
- <div class="gl-w-3/10">
- <span>{{ $options.i18n.inheritanceLabel }}</span>
- <gl-icon
- v-gl-tooltip
- :title="$options.i18n.inheritanceTooltip"
- :aria-label="$options.i18n.inheritanceTooltip"
- name="question-o"
- class="gl-ml-2"
- />
- </div>
+ <template #table>
+ <gl-table-lite
+ :fields="$options.approverFields"
+ :items="environment[$options.APPROVER_RULE_KEY]"
+ stacked="md"
+ show-empty
+ >
+ <template #head(inheritance)="{ label }">
+ <span>{{ $options.i18n.inheritanceLabel }}</span>
+ <gl-icon
+ v-gl-tooltip
+ :title="$options.i18n.inheritanceTooltip"
+ :aria-label="$options.i18n.inheritanceTooltip"
+ name="question-o"
+ class="gl-ml-2"
+ />
+ </template>
+
+ <template #cell(approvers)="{ item: rule }">
+ <span data-testid="rule-description">
+ {{ rule.access_level_description }}
+ </span>
+ </template>
+
+ <template #cell(users)="{ item: rule }">
+ <show-more
+ #default="{ item }"
+ :limit="$options.AVATAR_LIMIT"
+ :items="getUsersForRule(rule, $options.APPROVER_RULE_KEY)"
+ >
+ <gl-avatar
+ :key="item.id"
+ v-gl-tooltip
+ :src="item.avatar_url"
+ :title="item.name"
+ :size="24"
+ class="gl-mr-2"
+ />
+ </show-more>
+ </template>
+
+ <template #cell(approvals)="{ item: rule }">
+ <template v-if="editingRules[rule.id]">
+ <gl-form-group
+ :label-for="`approval-count-${rule.id}`"
+ :label="$options.i18n.approvalCount"
+ label-sr-only
+ class="gl-mb-0"
+ >
+ <gl-form-input
+ :id="`approval-count-${rule.id}`"
+ v-model="editingRules[rule.id].required_approvals"
+ :name="`approval-count-${rule.id}`"
+ class="gl-text-center"
+ />
+ </gl-form-group>
+ </template>
+
+ <template v-else>
+ <span class="gl-text-center">{{ rule.required_approvals }}</span>
+ </template>
+ </template>
+
+ <template #cell(inheritance)="{ item: rule }">
+ <template v-if="editingRules[rule.id]">
+ <gl-toggle
+ v-if="isGroupRule(rule)"
+ :id="`approval-inheritance-${rule.id}`"
+ :label="$options.i18n.inheritanceLabel"
+ :name="`approval-inheritance-${rule.id}`"
+ :value="isUsingGroupInheritance(editingRules[rule.id])"
+ label-position="hidden"
+ class="gl-align-items-center gl-ml-11"
+ @change="updateApproverInheritance({ rule, value: $event })"
+ />
+ </template>
+
+ <template v-else>
+ <gl-toggle
+ v-if="isGroupRule(rule)"
+ :label="$options.i18n.inheritanceLabel"
+ :name="`approval-inheritance-${rule.id}`"
+ :value="isUsingGroupInheritance(rule)"
+ class="gl-align-items-center gl-ml-11"
+ label-position="hidden"
+ disabled
+ />
+ </template>
+ </template>
+
+ <template #cell(actions)="{ item: rule }">
+ <div class="gl-justify-content-end gl-flex">
+ <gl-button
+ v-if="editingRules[rule.id]"
+ class="gl-ml-auto gl-mr-4"
+ @click="updateRule({ rule, environment, ruleKey: $options.APPROVER_RULE_KEY })"
+ >
+ {{ $options.i18n.saveApproverButton }}
+ </gl-button>
+
+ <gl-button
+ v-else-if="!isUserRule(rule)"
+ class="gl-ml-auto gl-mr-4"
+ @click="editRule(rule)"
+ >
+ {{ $options.i18n.editApproverButton }}
+ </gl-button>
+
+ <gl-button
+ v-gl-tooltip
+ category="secondary"
+ variant="danger"
+ icon="remove"
+ :class="{ 'gl-ml-auto': isUserRule(rule) }"
+ :loading="loading"
+ :title="$options.i18n.approverDeleteButtonTitle"
+ :aria-label="$options.i18n.approverDeleteButtonTitle"
+ @click="deleteRule({ environment, rule, ruleKey: $options.APPROVER_RULE_KEY })"
+ />
+ </div>
+ </template>
+ </gl-table-lite>
</template>
<template #empty-state>
<gl-sprintf :message="$options.i18n.approvalRulesEmptyStateMessage">
@@ -257,96 +384,6 @@ export default {
</template>
</gl-sprintf>
</template>
- <template #rule="{ rule, ruleKey }">
- <span class="gl-w-3/10" data-testid="rule-description">
- {{ rule.access_level_description }}
- </span>
-
- <div class="gl-w-2/10">
- <show-more
- #default="{ item }"
- :limit="$options.AVATAR_LIMIT"
- :items="getUsersForRule(rule, ruleKey)"
- >
- <gl-avatar
- :key="item.id"
- v-gl-tooltip
- :src="item.avatar_url"
- :title="item.name"
- :size="24"
- class="gl-mr-2"
- />
- </show-more>
- </div>
-
- <template v-if="editingRules[rule.id]">
- <gl-form-group
- :label-for="`approval-count-${rule.id}`"
- :label="$options.i18n.approvalCount"
- label-sr-only
- class="gl-mb-0 gl-w-2/10"
- >
- <gl-form-input
- :id="`approval-count-${rule.id}`"
- v-model="editingRules[rule.id].required_approvals"
- :name="`approval-count-${rule.id}`"
- class="gl-text-center"
- />
- </gl-form-group>
-
- <gl-toggle
- v-if="isGroupRule(rule)"
- :id="`approval-inheritance-${rule.id}`"
- :label="$options.i18n.inheritanceLabel"
- :name="`approval-inheritance-${rule.id}`"
- :value="isUsingGroupInheritance(editingRules[rule.id])"
- label-position="hidden"
- class="gl-ml-11 gl-items-center"
- @change="updateApproverInheritance({ rule, value: $event })"
- />
- <span v-else></span>
- <gl-button
- class="gl-ml-auto gl-mr-4"
- @click="updateRule({ rule, environment, ruleKey })"
- >
- {{ $options.i18n.saveApproverButton }}
- </gl-button>
- </template>
- <template v-else>
- <span class="gl-w-2/10 gl-text-center">{{ rule.required_approvals }}</span>
-
- <gl-toggle
- v-if="isGroupRule(rule)"
- :label="$options.i18n.inheritanceLabel"
- :name="`approval-inheritance-${rule.id}`"
- :value="isUsingGroupInheritance(rule)"
- class="gl-ml-11 gl-items-center"
- label-position="hidden"
- disabled
- />
- <span v-else></span>
-
- <gl-button
- v-if="!isUserRule(rule)"
- class="gl-ml-auto gl-mr-4"
- @click="editRule(rule)"
- >
- {{ $options.i18n.editApproverButton }}
- </gl-button>
- </template>
-
- <gl-button
- v-gl-tooltip
- category="secondary"
- variant="danger"
- icon="remove"
- :class="{ 'gl-ml-auto': isUserRule(rule) }"
- :loading="loading"
- :title="$options.i18n.approverDeleteButtonTitle"
- :aria-label="$options.i18n.approverDeleteButtonTitle"
- @click="deleteRule({ environment, rule, ruleKey })"
- />
- </template>
</edit-protected-environment-rules-card>
</template>
</protected-environments>
- Use
add_approvers.vueas an example. There's already a table representing approvers rule.
To avoid code duplication, store table fields in constants.js. Create another set of fields for the deployers table.
Example of the updated constants
diff --git a/ee/app/assets/javascripts/protected_environments/constants.js b/ee/app/assets/javascripts/protected_environments/constants.js
index 16746a5f992e..eb69f9bbff2e 100644
--- a/ee/app/assets/javascripts/protected_environments/constants.js
+++ b/ee/app/assets/javascripts/protected_environments/constants.js
@@ -1,3 +1,5 @@
+import { s__ } from '~/locale';
+
export const INHERITED_GROUPS = 1;
export const NON_INHERITED_GROUPS = 0;
export const GROUP_INHERITANCE_KEY = 'group_inheritance_type';
@@ -14,3 +16,39 @@ export const LEVEL_TYPES = {
USER: 'user',
GROUP: 'group',
};
+
+export const DEPLOYER_FIELDS = [
+ {
+ key: 'deployers',
+ label: s__('ProtectedEnvironments|Allowed to deploy'),
+ tdClass: 'md:gl-w-3/10',
+ },
+ {
+ key: 'users',
+ label: s__('ProtectedEnvironments|Users'),
+ },
+ {
+ key: 'actions',
+ label: '',
+ },
+];
+
+export const APPROVER_FIELDS = [
+ {
+ key: 'approvers',
+ label: s__('ProtectedEnvironments|Approvers'),
+ },
+ {
+ key: 'approvals',
+ label: s__('ProtectedEnvironments|Approvals required'),
+ },
+ {
+ key: 'inheritance',
+ label: s__('ProtectedEnvironments|Enable group inheritance'),
+ },
+ {
+ key: 'remove',
+ label: '',
+ tdClass: 'gl-text-right',
+ },
+];
- Update
edit_protected_rules_card.vue:- Remove unnecessary slots (such as
card-headerandrule)
- Remove unnecessary slots (such as
Example of the updated code
diff --git a/ee/app/assets/javascripts/protected_environments/edit_protected_environment_rules_card.vue b/ee/app/assets/javascripts/protected_environments/edit_protected_environment_rules_card.vue
index c0acafe79f82..81ee089445cf 100644
--- a/ee/app/assets/javascripts/protected_environments/edit_protected_environment_rules_card.vue
+++ b/ee/app/assets/javascripts/protected_environments/edit_protected_environment_rules_card.vue
@@ -41,20 +41,11 @@ export default {
</script>
<template>
<div>
- <div class="gl-flex gl-w-full gl-bg-gray-50 gl-p-5 gl-font-bold">
- <slot name="card-header"></slot>
- </div>
- <div v-if="!rules.length" data-testid="empty-state" class="gl-border-t gl-bg-white gl-p-5">
+ <slot name="table"></slot>
+ <div v-if="!rules.length" data-testid="empty-state" class="gl-bg-white gl-p-5">
<slot name="empty-state"></slot>
</div>
- <div
- v-for="rule in rules"
- :key="rule.id"
- :data-testid="`${ruleKey}-${rule.id}`"
- class="gl-border-t gl-flex gl-w-full gl-items-center gl-bg-white gl-p-5"
- >
- <slot name="rule" :rule="rule" :rule-key="ruleKey"></slot>
- </div>
+
<div class="gl-border-t gl-flex gl-items-center gl-p-5">
<gl-button
category="secondary"
- Fix layout shifts and border issues in the
protected_environments.vuecomponent
Example of the updated code
diff --git a/ee/app/assets/javascripts/protected_environments/protected_environments.vue b/ee/app/assets/javascripts/protected_environments/protected_environments.vue
index d348783a9d57..7ab5dba32472 100644
--- a/ee/app/assets/javascripts/protected_environments/protected_environments.vue
+++ b/ee/app/assets/javascripts/protected_environments/protected_environments.vue
@@ -69,9 +69,6 @@ export default {
},
methods: {
...mapActions(['setPage', 'fetchProtectedEnvironments']),
- isLast(index) {
- return index === this.environments.length - 1;
- },
toggleCollapse({ name }) {
this.expanded = {
...this.expanded,
@@ -160,54 +157,52 @@ export default {
{{ emptyMessage }}
</div>
<template v-else>
- <div class="-gl-mx-5 -gl-my-4">
- <div
- v-for="(environment, index) in environments"
- :key="environment.name"
- :class="{ 'gl-border-b': !isLast(index) }"
+ <div
+ v-for="(environment, index) in environments"
+ :key="environment.name"
+ class="gl-border-b"
+ >
+ <gl-button
+ block
+ category="tertiary"
+ variant="confirm"
+ class="!gl-rounded-none !gl-px-5 !gl-py-4"
+ button-text-classes="gl-flex gl-w-full gl-items-baseline"
+ :aria-label="environment.name"
+ data-testid="protected-environment-item-toggle"
+ @click="toggleCollapse(environment)"
+ >
+ <span class="gl-py-2 gl-text-gray-900">{{ environment.name }}</span>
+ <gl-badge v-if="!isExpanded(environment)" class="gl-ml-auto">
+ {{ deploymentRulesText(environment) }}
+ </gl-badge>
+ <gl-badge v-if="!isExpanded(environment)" class="gl-ml-3">
+ {{ approvalRulesText(environment) }}
+ </gl-badge>
+ <gl-icon
+ :name="icon(environment)"
+ :size="14"
+ :class="{
+ 'gl-ml-3': !isExpanded(environment),
+ 'gl-ml-auto': isExpanded(environment),
+ }"
+ class="gl-text-gray-500"
+ />
+ </gl-button>
+ <gl-collapse
+ :visible="isExpanded(environment)"
+ class="gl-flex gl-flex-col gl-rounded-b-base gl-bg-white gl-pb-5"
>
+ <slot :environment="environment"></slot>
<gl-button
- block
- category="tertiary"
- variant="confirm"
- class="!gl-rounded-none !gl-px-5 !gl-py-4"
- button-text-classes="gl-flex gl-w-full gl-items-baseline"
- :aria-label="environment.name"
- data-testid="protected-environment-item-toggle"
- @click="toggleCollapse(environment)"
+ category="secondary"
+ variant="danger"
+ class="gl-mr-5 gl-mt-5 gl-self-end"
+ @click="confirmUnprotect(environment)"
>
- <span class="gl-py-2 gl-text-gray-900">{{ environment.name }}</span>
- <gl-badge v-if="!isExpanded(environment)" class="gl-ml-auto">
- {{ deploymentRulesText(environment) }}
- </gl-badge>
- <gl-badge v-if="!isExpanded(environment)" class="gl-ml-3">
- {{ approvalRulesText(environment) }}
- </gl-badge>
- <gl-icon
- :name="icon(environment)"
- :size="14"
- :class="{
- 'gl-ml-3': !isExpanded(environment),
- 'gl-ml-auto': isExpanded(environment),
- }"
- class="gl-text-gray-500"
- />
+ {{ s__('ProtectedEnvironments|Unprotect') }}
</gl-button>
- <gl-collapse
- :visible="isExpanded(environment)"
- class="gl-mx-5 gl-mb-5 gl-mt-3 gl-flex gl-flex-col"
- >
- <slot :environment="environment"></slot>
- <gl-button
- category="secondary"
- variant="danger"
- class="gl-mt-5 gl-self-end"
- @click="confirmUnprotect(environment)"
- >
- {{ s__('ProtectedEnvironments|Unprotect') }}
- </gl-button>
- </gl-collapse>
- </div>
+ </gl-collapse>
</div>
</template>
<template v-if="showPagination" #pagination>
- Check and update the related specs at protected_environments
How to set up and validate locally
- Note: this is an EE feature, you'll need a license to verify it locally
- Validate the project-level view works as expected:
- Visit Project -> Settings -> CI/CD
- Select Protected environments section
- Verify the settings: you should be able to select an environment, the list of deployers and approvers (could be roles, related groups, or users), save / edit / unprotect an environment.
- Validate the group-level view works as expected:
- Visit Group -> Settings -> CI/CD
- Select Protected environments section
- Verify the settings: you should be able to select an environment tier, the list of deployer groups, and save / edit / unprotect an environment.
Edited by Anna Vovchenko


