summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/projects/settings/branch_rules
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/projects/settings/branch_rules')
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue)2
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js42
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue207
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue99
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue110
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js11
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql50
11 files changed, 517 insertions, 4 deletions
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
index 6ba2ef7da99..f2b1c749abc 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
@@ -10,7 +10,7 @@ import {
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
-import branchesQuery from '../queries/branches.query.graphql';
+import branchesQuery from '../../queries/branches.query.graphql';
export const i18n = {
fetchBranchesError: s__('BranchRules|An error occurred while fetching branches.'),
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
index ad3eb7d2899..ad3eb7d2899 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
index bcc0f64d667..bcc0f64d667 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
index 85f168af4a8..85f168af4a8 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
index 541923bb735..541923bb735 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
new file mode 100644
index 00000000000..264c2629433
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -0,0 +1,42 @@
+import { s__ } from '~/locale';
+
+export const I18N = {
+ manageProtectionsLinkTitle: s__('BranchRules|Manage in Protected Branches'),
+ targetBranch: s__('BranchRules|Target Branch'),
+ branchNameOrPattern: s__('BranchRules|Branch name or pattern'),
+ branch: s__('BranchRules|Target Branch'),
+ allBranches: s__('BranchRules|All branches'),
+ protectBranchTitle: s__('BranchRules|Protect branch'),
+ protectBranchDescription: s__(
+ 'BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}',
+ ),
+ wildcardsHelpText: s__(
+ 'BranchRules|%{linkStart}Wildcards%{linkEnd} such as *-stable or production/ are supported',
+ ),
+ forcePushTitle: s__('BranchRules|Force push'),
+ allowForcePushDescription: s__(
+ 'BranchRules|All users with push access are allowed to force push.',
+ ),
+ disallowForcePushDescription: s__('BranchRules|Force push is not allowed.'),
+ approvalsTitle: s__('BranchRules|Approvals'),
+ manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'),
+ approvalsDescription: s__(
+ 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}',
+ ),
+ statusChecksTitle: s__('BranchRules|Status checks'),
+ allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'),
+ allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'),
+ approvalsHeader: s__('BranchRules|Required approvals (%{total})'),
+ noData: s__('BranchRules|No data to display'),
+};
+
+export const BRANCH_PARAM_NAME = 'branch';
+
+export const ALL_BRANCHES_WILDCARD = '*';
+
+export const WILDCARDS_HELP_PATH =
+ 'user/project/protected_branches#configure-multiple-protected-branches-by-using-a-wildcard';
+
+export const PROTECTED_BRANCHES_HELP_PATH = 'user/project/protected_branches';
+
+export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index.md';
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
new file mode 100644
index 00000000000..318940478a8
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -0,0 +1,207 @@
+<script>
+import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import branchRulesQuery from '../../queries/branch_rules_details.query.graphql';
+import Protection from './protection.vue';
+import {
+ I18N,
+ ALL_BRANCHES_WILDCARD,
+ BRANCH_PARAM_NAME,
+ WILDCARDS_HELP_PATH,
+ PROTECTED_BRANCHES_HELP_PATH,
+ APPROVALS_HELP_PATH,
+} from './constants';
+
+const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH);
+const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH);
+const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH);
+
+export default {
+ name: 'RuleView',
+ i18n: I18N,
+ wildcardsHelpDocLink,
+ protectedBranchesHelpDocLink,
+ approvalsHelpDocLink,
+ components: { Protection, GlSprintf, GlLink, GlLoadingIcon },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ protectedBranchesPath: {
+ default: '',
+ },
+ approvalRulesPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ project: {
+ query: branchRulesQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update({ project: { branchRules } }) {
+ this.branchProtection = branchRules.nodes.find(
+ (rule) => rule.name === this.branch,
+ )?.branchProtection;
+ },
+ },
+ },
+ data() {
+ return {
+ branch: getParameterByName(BRANCH_PARAM_NAME),
+ branchProtection: {
+ approvalRules: {},
+ },
+ };
+ },
+ computed: {
+ forcePushDescription() {
+ return this.branchProtection?.allowForcePush
+ ? this.$options.i18n.allowForcePushDescription
+ : this.$options.i18n.disallowForcePushDescription;
+ },
+ mergeAccessLevels() {
+ const { mergeAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(mergeAccessLevels);
+ },
+ pushAccessLevels() {
+ const { pushAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(pushAccessLevels);
+ },
+ allowedToMergeHeader() {
+ return sprintf(this.$options.i18n.allowedToMergeHeader, {
+ total: this.mergeAccessLevels.total,
+ });
+ },
+ allowedToPushHeader() {
+ return sprintf(this.$options.i18n.allowedToPushHeader, {
+ total: this.pushAccessLevels.total,
+ });
+ },
+ approvalsHeader() {
+ const total = this.approvals.reduce(
+ (sum, { approvalsRequired }) => sum + approvalsRequired,
+ 0,
+ );
+ return sprintf(this.$options.i18n.approvalsHeader, {
+ total,
+ });
+ },
+ allBranches() {
+ return this.branch === ALL_BRANCHES_WILDCARD;
+ },
+ allBranchesLabel() {
+ return this.$options.i18n.allBranches;
+ },
+ branchTitle() {
+ return this.allBranches
+ ? this.$options.i18n.targetBranch
+ : this.$options.i18n.branchNameOrPattern;
+ },
+ approvals() {
+ return this.branchProtection?.approvalRules?.nodes || [];
+ },
+ },
+ methods: {
+ getAccessLevels(accessLevels = {}) {
+ const total = accessLevels.edges?.length;
+ const accessLevelTypes = { total, users: [], groups: [], roles: [] };
+
+ accessLevels.edges?.forEach(({ node }) => {
+ if (node.user) {
+ const src = node.user.avatarUrl;
+ accessLevelTypes.users.push({ src, ...node.user });
+ } else if (node.group) {
+ accessLevelTypes.groups.push(node);
+ } else {
+ accessLevelTypes.roles.push(node);
+ }
+ });
+
+ return accessLevelTypes;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="$apollo.loading" />
+ <div v-else-if="!branchProtection">{{ $options.i18n.noData }}</div>
+ <div v-else>
+ <strong data-testid="branch-title">{{ branchTitle }}</strong>
+ <p v-if="!allBranches" class="gl-mb-3 gl-text-gray-400">
+ <gl-sprintf :message="$options.i18n.wildcardsHelpText">
+ <template #link="{ content }">
+ <gl-link :href="$options.wildcardsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div v-if="allBranches" class="gl-mt-2" data-testid="branch">
+ {{ allBranchesLabel }}
+ </div>
+ <code v-else class="gl-mt-2" data-testid="branch">{{ branch }}</code>
+
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.protectBranchTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.protectBranchDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.protectedBranchesHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <!-- Allowed to push -->
+ <protection
+ class="gl-mt-3"
+ :header="allowedToPushHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="pushAccessLevels.roles"
+ :users="pushAccessLevels.users"
+ :groups="pushAccessLevels.groups"
+ />
+
+ <!-- Force push -->
+ <strong>{{ $options.i18n.forcePushTitle }}</strong>
+ <p>{{ forcePushDescription }}</p>
+
+ <!-- Allowed to merge -->
+ <protection
+ :header="allowedToMergeHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="mergeAccessLevels.roles"
+ :users="mergeAccessLevels.users"
+ :groups="mergeAccessLevels.groups"
+ />
+
+ <!-- Approvals -->
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.approvalsTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.approvalsDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.approvalsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <protection
+ class="gl-mt-3"
+ :header="approvalsHeader"
+ :header-link-title="$options.i18n.manageApprovalsLinkTitle"
+ :header-link-href="approvalRulesPath"
+ :approvals="approvals"
+ />
+
+ <!-- Status checks -->
+ <!-- Follow-up: add status checks section (https://gitlab.com/gitlab-org/gitlab/-/issues/372362) -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
new file mode 100644
index 00000000000..cfe2df0dbda
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlCard, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ProtectionRow from './protection_row.vue';
+
+export const i18n = {
+ rolesTitle: s__('BranchRules|Roles'),
+ usersTitle: s__('BranchRules|Users'),
+ groupsTitle: s__('BranchRules|Groups'),
+};
+
+export default {
+ name: 'ProtectionDetail',
+ i18n,
+ components: { GlCard, GlLink, ProtectionRow },
+ props: {
+ header: {
+ type: String,
+ required: true,
+ },
+ headerLinkTitle: {
+ type: String,
+ required: true,
+ },
+ headerLinkHref: {
+ type: String,
+ required: true,
+ },
+ roles: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ groups: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvals: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ showUsersDivider() {
+ return Boolean(this.roles.length);
+ },
+ showGroupsDivider() {
+ return Boolean(this.roles.length || this.users.length);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card class="gl-mb-5" body-class="gl-py-0">
+ <template #header>
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <strong>{{ header }}</strong>
+ <gl-link :href="headerLinkHref">{{ headerLinkTitle }}</gl-link>
+ </div>
+ </template>
+
+ <!-- Roles -->
+ <protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" />
+
+ <!-- Users -->
+ <protection-row
+ v-if="users.length"
+ :show-divider="showUsersDivider"
+ :users="users"
+ :title="$options.i18n.usersTitle"
+ />
+
+ <!-- Groups -->
+ <protection-row
+ v-if="groups.length"
+ :show-divider="showGroupsDivider"
+ :title="$options.i18n.groupsTitle"
+ :access-levels="groups"
+ />
+
+ <!-- Approvals -->
+ <protection-row
+ v-for="(approval, index) in approvals"
+ :key="approval.name"
+ :show-divider="index !== 0"
+ :title="approval.name"
+ :users="approval.eligibleApprovers.nodes"
+ :approvals-required="approval.approvalsRequired"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
new file mode 100644
index 00000000000..28a1c09fa82
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlAvatarsInline, GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+const AVATAR_TOOLTIP_MAX_CHARS = 100;
+export const MAX_VISIBLE_AVATARS = 4;
+export const AVATAR_SIZE = 32;
+
+export default {
+ name: 'ProtectionRow',
+ AVATAR_TOOLTIP_MAX_CHARS,
+ MAX_VISIBLE_AVATARS,
+ AVATAR_SIZE,
+ components: { GlAvatarsInline, GlAvatar, GlAvatarLink },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ accessLevels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showDivider: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvalsRequired: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ avatarBadgeSrOnlyText() {
+ return n__(
+ '%d additional user',
+ '%d additional users',
+ this.users.length - this.$options.MAX_VISIBLE_AVATARS,
+ );
+ },
+ commaSeparateList() {
+ return this.accessLevels.length > 1;
+ },
+ approvalsRequiredTitle() {
+ return this.approvalsRequired
+ ? n__('%d approval required', '%d approvals required', this.approvalsRequired)
+ : null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4"
+ :class="{ 'gl-border-t-solid': showDivider }"
+ >
+ <div class="gl-display-flex gl-w-half gl-justify-content-space-between">
+ <div class="gl-mr-7 gl-w-quarter">{{ title }}</div>
+
+ <gl-avatars-inline
+ v-if="users.length"
+ class="gl-w-quarter!"
+ :avatars="users"
+ :collapsed="true"
+ :max-visible="$options.MAX_VISIBLE_AVATARS"
+ :avatar-size="$options.AVATAR_SIZE"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="$options.AVATAR_TOOLTIP_MAX_CHARS"
+ :badge-sr-only-text="avatarBadgeSrOnlyText"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link
+ :key="avatar.username"
+ v-gl-tooltip
+ target="_blank"
+ :href="avatar.webUrl"
+ :title="avatar.name"
+ >
+ <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="$options.AVATAR_SIZE" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+
+ <div
+ v-for="(item, index) in accessLevels"
+ :key="index"
+ data-testid="access-level"
+ class="gl-w-quarter"
+ >
+ <span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
+ {{ item.accessLevelDescription }}
+ </div>
+
+ <div class="gl-ml-7 gl-w-quarter">{{ approvalsRequiredTitle }}</div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 8452542540e..07fd0a7080f 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RuleEdit from './components/rule_edit.vue';
+import View from './components/view/index.vue';
export default function mountBranchRules(el) {
if (!el) {
@@ -14,13 +14,18 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath } = el.dataset;
+ const { projectPath, protectedBranchesPath, approvalRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ protectedBranchesPath,
+ approvalRulesPath,
+ },
render(h) {
- return h(RuleEdit, { props: { projectPath } });
+ return h(View);
},
});
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
new file mode 100644
index 00000000000..3ac165498a1
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -0,0 +1,50 @@
+query getBranchRulesDetails($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ branchRules {
+ nodes {
+ name
+ branchProtection {
+ allowForcePush
+ codeOwnerApprovalRequired
+ mergeAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ pushAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}