summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/projects/settings
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/projects/settings')
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js6
-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
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue38
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue170
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js2
-rw-r--r--app/assets/javascripts/projects/settings/mount_default_branch_selector.js22
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue9
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue37
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js12
22 files changed, 738 insertions, 118 deletions
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 060178a3cfb..335545c802a 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import { escape, find, countBy } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { n__, s__, __, sprintf } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
@@ -326,12 +326,12 @@ export default class AccessDropdown {
);
})
.catch(() => {
- createFlash({ message: __('Failed to load groups, users and deploy keys.') });
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
getDeployKeys(query)
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
}
}
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
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 2209172c06d..cc47496971d 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
@@ -163,7 +163,7 @@ export default {
this.setSelected({ initial });
})
.catch(() =>
- createFlash({ message: __('Failed to load groups, users and deploy keys.') }),
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.initialLoading = false;
@@ -175,7 +175,7 @@ export default {
this.consolidateData(deployKeysResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }))
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
new file mode 100644
index 00000000000..fee2f591216
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -0,0 +1,38 @@
+<script>
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES } from '~/ref/constants';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ RefSelector,
+ },
+ props: {
+ persistedDefaultBranch: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ },
+ refTypes: [REF_TYPE_BRANCHES],
+ i18n: {
+ dropdownHeader: __('Select default branch'),
+ searchPlaceholder: __('Search branch'),
+ },
+};
+</script>
+<template>
+ <ref-selector
+ :value="persistedDefaultBranch"
+ class="gl-w-full"
+ :project-id="projectId"
+ :enabled-ref-types="$options.refTypes"
+ :translations="$options.i18n"
+ name="project[default_branch]"
+ data-testid="default-branch-dropdown"
+ data-qa-selector="default_branch_dropdown"
+ />
+</template>
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index c13753da00b..55420c9c732 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -1,13 +1,14 @@
<script>
-import { GlFormGroup } from '@gitlab/ui';
-import produce from 'immer';
+import { GlFormGroup, GlAlert } from '@gitlab/ui';
+import { debounce } from 'lodash';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getTransferLocations } from '~/api/projects_api';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanTransferProjects from '../graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
-
-const GROUPS_PER_PAGE = 25;
+import { s__, __ } from '~/locale';
+import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql';
export default {
name: 'TransferProjectForm',
@@ -15,7 +16,15 @@ export default {
GlFormGroup,
NamespaceSelect,
ConfirmDanger,
+ GlAlert,
+ },
+ i18n: {
+ errorMessage: s__(
+ 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
+ ),
+ alertDismissAlert: __('Dismiss'),
},
+ inject: ['projectId'],
props: {
confirmationPhrase: {
type: String,
@@ -26,93 +35,131 @@ export default {
required: true,
},
},
- apollo: {
- currentUser: {
- query: searchNamespacesWhereUserCanTransferProjects,
- debounce: DEBOUNCE_DELAY,
- variables() {
- return {
- search: this.searchTerm,
- after: null,
- first: GROUPS_PER_PAGE,
- };
- },
- result() {
- this.isLoadingMoreGroups = false;
- this.isSearchLoading = false;
- },
- },
- },
data() {
return {
- currentUser: {},
+ userNamespaces: [],
+ groupNamespaces: [],
+ initialNamespacesLoaded: false,
selectedNamespace: null,
- isLoadingMoreGroups: false,
+ hasError: false,
+ isLoading: false,
isSearchLoading: false,
searchTerm: '',
+ page: 1,
+ totalPages: 1,
};
},
computed: {
hasSelectedNamespace() {
return Boolean(this.selectedNamespace?.id);
},
- groupNamespaces() {
- return this.currentUser.groups?.nodes?.map(this.formatNamespace) || [];
- },
- userNamespaces() {
- const { namespace } = this.currentUser;
-
- return namespace ? [this.formatNamespace(namespace)] : [];
- },
hasNextPageOfGroups() {
- return this.currentUser.groups?.pageInfo?.hasNextPage || false;
+ return this.page < this.totalPages;
},
},
methods: {
+ async handleShow() {
+ if (this.initialNamespacesLoaded) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ [this.groupNamespaces, this.userNamespaces] = await Promise.all([
+ this.getGroupNamespaces(),
+ this.getUserNamespaces(),
+ ]);
+
+ this.isLoading = false;
+ this.initialNamespacesLoaded = true;
+ },
handleSelect(selectedNamespace) {
this.selectedNamespace = selectedNamespace;
this.$emit('selectNamespace', selectedNamespace.id);
},
- handleLoadMoreGroups() {
- this.isLoadingMoreGroups = true;
+ async getGroupNamespaces() {
+ try {
+ const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, {
+ page: this.page,
+ search: this.searchTerm,
+ });
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ this.totalPages = totalPages;
- this.$apollo.queries.currentUser.fetchMore({
- variables: {
- after: this.currentUser.groups.pageInfo.endCursor,
- first: GROUPS_PER_PAGE,
- },
- updateQuery(
- previousResult,
+ return groupNamespaces.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }));
+ } catch (error) {
+ this.hasError = true;
+
+ return [];
+ }
+ },
+ async getUserNamespaces() {
+ try {
+ const {
+ data: {
+ currentUser: { namespace },
+ },
+ } = await this.$apollo.query({
+ query: currentUserNamespace,
+ });
+
+ if (!namespace) {
+ return [];
+ }
+
+ return [
{
- fetchMoreResult: {
- currentUser: { groups: newGroups },
- },
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
},
- ) {
- const previousGroups = previousResult.currentUser.groups;
+ ];
+ } catch (error) {
+ this.hasError = true;
- return produce(previousResult, (draftData) => {
- draftData.currentUser.groups.nodes = [...previousGroups.nodes, ...newGroups.nodes];
- draftData.currentUser.groups.pageInfo = newGroups.pageInfo;
- });
- },
- });
+ return [];
+ }
},
- handleSearch(searchTerm) {
+ async handleLoadMoreGroups() {
+ this.isLoading = true;
+ this.page += 1;
+
+ const groupNamespaces = await this.getGroupNamespaces();
+ this.groupNamespaces.push(...groupNamespaces);
+
+ this.isLoading = false;
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
this.isSearchLoading = true;
+
+ this.groupNamespaces = await this.getGroupNamespaces();
+
+ this.isSearchLoading = false;
+ }, DEBOUNCE_DELAY),
+ handleSearch(searchTerm) {
this.searchTerm = searchTerm;
+ this.page = 1;
+
+ this.debouncedSearch();
},
- formatNamespace({ id, fullName }) {
- return {
- id: getIdFromGraphQLId(id),
- humanName: fullName,
- };
+ handleAlertDismiss() {
+ this.hasError = false;
},
},
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="hasError"
+ variant="danger"
+ :dismiss-label="$options.i18n.alertDismissLabel"
+ @dismiss="handleAlertDismiss"
+ >{{ $options.i18n.errorMessage }}</gl-alert
+ >
<gl-form-group>
<namespace-select
data-testid="transfer-project-namespace"
@@ -121,12 +168,13 @@ export default {
:user-namespaces="userNamespaces"
:selected-namespace="selectedNamespace"
:has-next-page-of-groups="hasNextPageOfGroups"
- :is-loading-more-groups="isLoadingMoreGroups"
+ :is-loading="isLoading"
:is-search-loading="isSearchLoading"
:should-filter-namespaces="false"
@select="handleSelect"
@load-more-groups="handleLoadMoreGroups"
@search="handleSearch"
+ @show="handleShow"
/>
</gl-form-group>
<confirm-danger
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
new file mode 100644
index 00000000000..7ae6ee1428b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
@@ -0,0 +1,9 @@
+query currentUserNamespace {
+ currentUser {
+ id
+ namespace {
+ id
+ fullName
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
deleted file mode 100644
index d4bcb8c869c..00000000000
--- a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-
-query searchNamespacesWhereUserCanTransferProjects(
- $search: String = ""
- $after: String = ""
- $first: Int = null
-) {
- currentUser {
- id
- groups(permissionScope: TRANSFER_PROJECTS, search: $search, after: $after, first: $first) {
- nodes {
- id
- fullName
- }
- pageInfo {
- ...PageInfo
- }
- }
- namespace {
- id
- fullName
- }
- }
-}
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
index bc1aff640d2..89c158a9ba8 100644
--- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -12,6 +12,7 @@ export default () => {
Vue.use(VueApollo);
const {
+ projectId,
targetFormId = null,
targetHiddenInputId = null,
buttonText: confirmButtonText = '',
@@ -26,6 +27,7 @@ export default () => {
}),
provide: {
confirmDangerMessage,
+ projectId,
},
render(createElement) {
return createElement(TransferProjectForm, {
diff --git a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
new file mode 100644
index 00000000000..611561e38f2
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DefaultBranchSelector from './components/default_branch_selector.vue';
+
+export default (el) => {
+ if (!el) {
+ return null;
+ }
+
+ const { projectId, defaultBranch } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(DefaultBranchSelector, {
+ props: {
+ persistedDefaultBranch: defaultBranch,
+ projectId,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index e8eaf0a70b2..94793a535cc 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import branchRulesQuery from './graphql/queries/branch_rules.query.graphql';
import BranchRule from './components/branch_rule.vue';
@@ -31,14 +31,13 @@ export default {
return data.project?.branchRules?.nodes || [];
},
error() {
- createFlash({ message: this.$options.i18n.queryError });
+ createAlert({ message: this.$options.i18n.queryError });
},
},
},
- props: {
+ inject: {
projectPath: {
- type: String,
- required: true,
+ default: '',
},
},
data() {
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 68750318029..2b88f8561d7 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
+ detailsButtonLabel: s__('BranchRules|Details'),
};
export default {
@@ -12,6 +13,12 @@ export default {
i18n,
components: {
GlBadge,
+ GlButton,
+ },
+ inject: {
+ branchRulesPath: {
+ default: '',
+ },
},
props: {
name: {
@@ -38,24 +45,30 @@ export default {
hasApprovalDetails() {
return this.approvalDetails && this.approvalDetails.length;
},
+ detailsPath() {
+ return `${this.branchRulesPath}?branch=${this.name}`;
+ },
},
};
</script>
<template>
- <div class="gl-border-b gl-pt-5 gl-pb-5">
- <strong class="gl-font-monospace">{{ name }}</strong>
+ <div class="gl-border-b gl-pt-5 gl-pb-5 gl-display-flex gl-justify-content-space-between">
+ <div>
+ <strong class="gl-font-monospace">{{ name }}</strong>
- <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
- $options.i18n.defaultLabel
- }}</gl-badge>
+ <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
+ $options.i18n.defaultLabel
+ }}</gl-badge>
- <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
- $options.i18n.protectedLabel
- }}</gl-badge>
+ <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
+ $options.i18n.protectedLabel
+ }}</gl-badge>
- <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
- <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
- </ul>
+ <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
+ <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
+ </ul>
+ </div>
+ <gl-button :href="detailsPath"> {{ $options.i18n.detailsButtonLabel }}</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
index 35322e2e466..042be089e09 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
@@ -12,17 +12,17 @@ const apolloProvider = new VueApollo({
export default function mountBranchRules(el) {
if (!el) return null;
- const { projectPath } = el.dataset;
+ const { projectPath, branchRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ branchRulesPath,
+ },
render(createElement) {
- return createElement(BranchRulesApp, {
- props: {
- projectPath,
- },
- });
+ return createElement(BranchRulesApp);
},
});
}