summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-19 18:09:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-19 18:09:21 +0000
commitf602da84d10c36889714e46040f26cdfef5dce60 (patch)
tree6835a37866865775596881c5e3a35115f0ac8a49 /app
parent9c8e8b5ffc6e11d827fa42f2dce5f90c4dc19493 (diff)
downloadgitlab-ce-f602da84d10c36889714e46040f26cdfef5dce60.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue85
-rw-r--r--app/assets/javascripts/members/components/table/member_avatar.vue8
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue2
-rw-r--r--app/assets/javascripts/members/constants.js6
-rw-r--r--app/assets/javascripts/members/index.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue68
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue137
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js25
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/utils.js9
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue5
-rw-r--r--app/channels/awareness_channel.rb84
-rw-r--r--app/helpers/groups/group_members_helper.rb2
-rw-r--r--app/helpers/packages_helper.rb10
-rw-r--r--app/models/awareness_session.rb25
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/projects/settings/packages_and_registries/show.html.haml2
25 files changed, 484 insertions, 55 deletions
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 98995730df4..b824a013f3b 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -1,40 +1,48 @@
<script>
import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES, ACTIVE_TAB_QUERY_PARAM_NAME } from '../constants';
+import { queryToObject } from '~/lib/utils/url_utility';
+import {
+ MEMBER_TYPES,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+ TAB_QUERY_PARAM_VALUES,
+ EE_TABS,
+} from 'ee_else_ce/members/constants';
import MembersApp from './app.vue';
const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
+export const TABS = [
+ {
+ namespace: MEMBER_TYPES.user,
+ title: __('Members'),
+ },
+ {
+ namespace: MEMBER_TYPES.group,
+ title: __('Groups'),
+ attrs: { 'data-qa-selector': 'groups_list_tab' },
+ queryParamValue: TAB_QUERY_PARAM_VALUES.group,
+ },
+ {
+ namespace: MEMBER_TYPES.invite,
+ title: __('Invited'),
+ canManageMembersPermissionsRequired: true,
+ queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
+ },
+ {
+ namespace: MEMBER_TYPES.accessRequest,
+ title: __('Access requests'),
+ canManageMembersPermissionsRequired: true,
+ queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
+ },
+ ...EE_TABS,
+];
+
export default {
name: 'MembersTabs',
ACTIVE_TAB_QUERY_PARAM_NAME,
- TABS: [
- {
- namespace: MEMBER_TYPES.user,
- title: __('Members'),
- },
- {
- namespace: MEMBER_TYPES.group,
- title: __('Groups'),
- attrs: { 'data-qa-selector': 'groups_list_tab' },
- queryParamValue: TAB_QUERY_PARAM_VALUES.group,
- },
- {
- namespace: MEMBER_TYPES.invite,
- title: __('Invited'),
- canManageMembersPermissionsRequired: true,
- queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
- },
- {
- namespace: MEMBER_TYPES.accessRequest,
- title: __('Access requests'),
- canManageMembersPermissionsRequired: true,
- queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
- },
- ],
+ TABS,
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
data() {
@@ -43,20 +51,17 @@ export default {
};
},
computed: {
- ...mapState({
- userCount(state) {
- return countComputed(state, MEMBER_TYPES.user);
- },
- groupCount(state) {
- return countComputed(state, MEMBER_TYPES.group);
- },
- inviteCount(state) {
- return countComputed(state, MEMBER_TYPES.invite);
- },
- accessRequestCount(state) {
- return countComputed(state, MEMBER_TYPES.accessRequest);
- },
- }),
+ ...mapState(
+ Object.values(MEMBER_TYPES).reduce((getters, memberType) => {
+ return {
+ ...getters,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [`${memberType}Count`](state) {
+ return countComputed(state, memberType);
+ },
+ };
+ }, {}),
+ ),
urlParams() {
return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
},
diff --git a/app/assets/javascripts/members/components/table/member_avatar.vue b/app/assets/javascripts/members/components/table/member_avatar.vue
index 92b757ffcba..966eb90e402 100644
--- a/app/assets/javascripts/members/components/table/member_avatar.vue
+++ b/app/assets/javascripts/members/components/table/member_avatar.vue
@@ -6,7 +6,13 @@ import UserAvatar from '../avatars/user_avatar.vue';
export default {
name: 'MemberAvatar',
- components: { UserAvatar, InviteAvatar, GroupAvatar, AccessRequestAvatar: UserAvatar },
+ components: {
+ UserAvatar,
+ InviteAvatar,
+ GroupAvatar,
+ AccessRequestAvatar: UserAvatar,
+ BannedAvatar: UserAvatar,
+ },
props: {
memberType: {
type: String,
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 3436bcab2fc..51eff428d63 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -1,5 +1,5 @@
<script>
-import { MEMBER_TYPES } from '../../constants';
+import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import {
isGroup,
isDirectMember,
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 8c40cc3f29d..2fe816c7ea2 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -3,6 +3,12 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+// Overridden in EE
+export const EE_APP_OPTIONS = {};
+
+// Overridden in EE
+export const EE_TABS = [];
+
export const FIELD_KEY_ACCOUNT = 'account';
export const FIELD_KEY_SOURCE = 'source';
export const FIELD_KEY_GRANTED = 'granted';
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 0df876cabd7..34660f8f499 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -2,8 +2,8 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseDataAttributes } from '~/members/utils';
+import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import MembersTabs from './components/members_tabs.vue';
-import { MEMBER_TYPES } from './constants';
import membersStore from './store';
export const initMembersApp = (el, options) => {
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index fdc7bd39780..90a18d5cf5a 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -81,7 +81,7 @@ export default {
</script>
<template>
- <settings-block :collapsible="false">
+ <settings-block data-testid="container-expiration-policy-project-settings">
<template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
<template #description>
<span>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
index d75fb31fd98..7682754fdcb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
@@ -30,6 +30,11 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -46,5 +51,10 @@ export default {
{{ option.label }}
</option>
</gl-form-select>
+ <template v-if="description" #description>
+ <span data-testid="description" class="gl-text-gray-400">
+ {{ description }}
+ </span>
+ </template>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
new file mode 100644
index 00000000000..1170407a349
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import {
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ PACKAGES_CLEANUP_POLICY_TITLE,
+ PACKAGES_CLEANUP_POLICY_DESCRIPTION,
+} from '~/packages_and_registries/settings/project/constants';
+import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+import PackagesCleanupPolicyForm from './packages_cleanup_policy_form.vue';
+
+export default {
+ components: {
+ SettingsBlock,
+ GlAlert,
+ GlSprintf,
+ PackagesCleanupPolicyForm,
+ },
+ inject: ['projectPath'],
+ i18n: {
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ PACKAGES_CLEANUP_POLICY_TITLE,
+ PACKAGES_CLEANUP_POLICY_DESCRIPTION,
+ },
+ apollo: {
+ packagesCleanupPolicy: {
+ query: packagesCleanupPolicyQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update: (data) => data.project?.packagesCleanupPolicy || {},
+ error(e) {
+ this.fetchSettingsError = e;
+ },
+ },
+ },
+ data() {
+ return {
+ fetchSettingsError: false,
+ packagesCleanupPolicy: {},
+ };
+ },
+};
+</script>
+
+<template>
+ <settings-block>
+ <template #title> {{ $options.i18n.PACKAGES_CLEANUP_POLICY_TITLE }}</template>
+ <template #description>
+ <span data-testid="description">
+ <gl-sprintf :message="$options.i18n.PACKAGES_CLEANUP_POLICY_DESCRIPTION" />
+ </span>
+ </template>
+ <template #default>
+ <gl-alert v-if="fetchSettingsError" variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
+ </gl-alert>
+ <packages-cleanup-policy-form
+ v-else
+ v-model="packagesCleanupPolicy"
+ :is-loading="$apollo.queries.packagesCleanupPolicy.loading"
+ />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue
new file mode 100644
index 00000000000..b1751d5174a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import {
+ UPDATE_SETTINGS_ERROR_MESSAGE,
+ UPDATE_SETTINGS_SUCCESS_MESSAGE,
+ SET_CLEANUP_POLICY_BUTTON,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
+} from '~/packages_and_registries/settings/project/constants';
+import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
+import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils';
+import Tracking from '~/tracking';
+import ExpirationDropdown from './expiration_dropdown.vue';
+
+export default {
+ components: {
+ GlButton,
+ ExpirationDropdown,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['projectPath'],
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ formOptions: formOptionsGenerator(),
+ i18n: {
+ KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
+ SET_CLEANUP_POLICY_BUTTON,
+ },
+ data() {
+ return {
+ tracking: {
+ label: 'packages_cleanup_policies',
+ },
+ mutationLoading: false,
+ };
+ },
+ computed: {
+ prefilledForm() {
+ return {
+ ...this.value,
+ keepNDuplicatedPackageFiles: this.findDefaultOption(
+ KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
+ ),
+ };
+ },
+ showLoadingIcon() {
+ return this.isLoading || this.mutationLoading;
+ },
+ isSubmitButtonDisabled() {
+ return this.showLoadingIcon;
+ },
+ isFieldDisabled() {
+ return this.showLoadingIcon;
+ },
+ mutationVariables() {
+ return {
+ projectPath: this.projectPath,
+ keepNDuplicatedPackageFiles: this.prefilledForm.keepNDuplicatedPackageFiles,
+ };
+ },
+ },
+ methods: {
+ findDefaultOption(option) {
+ return this.value[option] || this.$options.formOptions[option].find((f) => f.default)?.key;
+ },
+ submit() {
+ this.track('submit_packages_cleanup_form');
+ this.mutationLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: updatePackagesCleanupPolicyMutation,
+ variables: {
+ input: this.mutationVariables,
+ },
+ })
+ .then(({ data }) => {
+ const [errorMessage] = data?.updatePackagesCleanupPolicy?.errors ?? [];
+ if (errorMessage) {
+ throw errorMessage;
+ } else {
+ this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ }
+ })
+ .catch(() => {
+ this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE);
+ })
+ .finally(() => {
+ this.mutationLoading = false;
+ });
+ },
+ onModelChange(newValue, model) {
+ this.$emit('input', { ...this.value, [model]: newValue });
+ },
+ },
+};
+</script>
+
+<template>
+ <form ref="form-element" @submit.prevent="submit">
+ <div class="gl-md-max-w-50p">
+ <expiration-dropdown
+ v-model="prefilledForm.keepNDuplicatedPackageFiles"
+ :disabled="isFieldDisabled"
+ :form-options="$options.formOptions.keepNDuplicatedPackageFiles"
+ :label="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL"
+ :description="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION"
+ name="keep-n-duplicated-package-files"
+ data-testid="keep-n-duplicated-package-files-dropdown"
+ @input="onModelChange($event, 'keepNDuplicatedPackageFiles')"
+ />
+ </div>
+ <div class="gl-mt-7 gl-display-flex gl-align-items-center">
+ <gl-button
+ data-testid="save-button"
+ type="submit"
+ :disabled="isSubmitButtonDisabled"
+ :loading="showLoadingIcon"
+ category="primary"
+ variant="confirm"
+ class="js-no-auto-disable gl-mr-4"
+ >
+ {{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
+ </gl-button>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 95af19e6d85..710cfe7b1eb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -1,15 +1,19 @@
<script>
import ContainerExpirationPolicy from './container_expiration_policy.vue';
+import PackagesCleanupPolicy from './packages_cleanup_policy.vue';
export default {
components: {
ContainerExpirationPolicy,
+ PackagesCleanupPolicy,
},
+ inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'],
};
</script>
<template>
- <section data-testid="registry-settings-app">
- <container-expiration-policy />
- </section>
+ <div>
+ <packages-cleanup-policy v-if="showPackageRegistrySettings" />
+ <container-expiration-policy v-if="showContainerRegistrySettings" />
+ </div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 40f980d15fb..948520151ce 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -55,6 +55,31 @@ export const EXPIRATION_POLICY_FOOTER_NOTE = s__(
'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time',
);
+export const PACKAGES_CLEANUP_POLICY_TITLE = s__(
+ 'PackageRegistry|Manage storage used by package assets',
+);
+export const PACKAGES_CLEANUP_POLICY_DESCRIPTION = s__(
+ 'PackageRegistry|When a package with same name and version is uploaded to the registry, more assets are added to the package. To save storage space, keep only the most recent assets.',
+);
+export const KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL = s__(
+ 'PackageRegistry|Number of duplicate assets to keep',
+);
+export const KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION = s__(
+ 'PackageRegistry|Examples of assets include .pom & .jar files',
+);
+
+export const KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME = 'keepNDuplicatedPackageFiles';
+
+export const KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS = [
+ { key: 'ONE_PACKAGE_FILE', label: 1, default: false },
+ { key: 'TEN_PACKAGE_FILES', label: 10, default: false },
+ { key: 'TWENTY_PACKAGE_FILES', label: 20, default: false },
+ { key: 'THIRTY_PACKAGE_FILES', label: 30, default: false },
+ { key: 'FORTY_PACKAGE_FILES', label: 40, default: false },
+ { key: 'FIFTY_PACKAGE_FILES', label: 50, default: false },
+ { key: 'ALL_PACKAGE_FILES', label: __('All'), default: true },
+];
+
export const KEEP_N_OPTIONS = [
{ key: 'ONE_TAG', variable: 1, default: false },
{ key: 'FIVE_TAGS', variable: 5, default: false },
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql
new file mode 100644
index 00000000000..a77ede37884
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/packages_cleanup_policy.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PackagesCleanupPolicyFields on PackagesCleanupPolicy {
+ keepNDuplicatedPackageFiles
+ nextRunAt
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql
new file mode 100644
index 00000000000..31cdd67e881
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/packages_cleanup_policy.fragment.graphql"
+
+mutation updatePackagesCleanupPolicy($input: UpdatePackagesCleanupPolicyInput!) {
+ updatePackagesCleanupPolicy(input: $input) {
+ packagesCleanupPolicy {
+ ...PackagesCleanupPolicyFields
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql
new file mode 100644
index 00000000000..0e9af253f2c
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/packages_cleanup_policy.fragment.graphql"
+
+query getProjectPackagesCleanupPolicy($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ packagesCleanupPolicy {
+ ...PackagesCleanupPolicyFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
index 17c33073668..daf1da6eac8 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
@@ -20,6 +20,8 @@ export default () => {
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
+ showContainerRegistrySettings,
+ showPackageRegistrySettings,
} = el.dataset;
return new Vue({
el,
@@ -34,6 +36,8 @@ export default () => {
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
+ showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),
+ showPackageRegistrySettings: parseBoolean(showPackageRegistrySettings),
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/utils.js b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
index b577a051862..847965454e9 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/utils.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
@@ -1,5 +1,11 @@
import { n__ } from '~/locale';
-import { KEEP_N_OPTIONS, CADENCE_OPTIONS, OLDER_THAN_OPTIONS } from './constants';
+import {
+ KEEP_N_OPTIONS,
+ CADENCE_OPTIONS,
+ OLDER_THAN_OPTIONS,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME,
+ KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS,
+} from './constants';
export const findDefaultOption = (options) => {
const item = options.find((o) => o.default);
@@ -25,5 +31,6 @@ export const formOptionsGenerator = () => {
olderThan: optionLabelGenerator(OLDER_THAN_OPTIONS, olderThanTranslationGenerator),
cadence: CADENCE_OPTIONS,
keepN: optionLabelGenerator(KEEP_N_OPTIONS, keepNTranslationGenerator),
+ [KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME]: KEEP_N_DUPLICATED_PACKAGE_FILES_OPTIONS,
};
};
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index c7c2f6f773e..62d47cb49b8 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -5,12 +5,11 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
import { initMembersApp } from '~/members';
-import { MEMBER_TYPES } from '~/members/constants';
+import { MEMBER_TYPES, EE_APP_OPTIONS } from 'ee_else_ce/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
-
-initMembersApp(document.querySelector('.js-group-members-list-app'), {
+const APP_OPTIONS = {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
@@ -61,7 +60,10 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: groupMemberRequestFormatter,
},
-});
+ ...EE_APP_OPTIONS,
+};
+
+initMembersApp(document.querySelector('.js-group-members-list-app'), APP_OPTIONS);
initInviteMembersModal();
initInviteGroupsModal();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index fe51591c32d..f2c30870a68 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -61,6 +61,10 @@ export default {
GlFormCheckbox,
GlToggle,
ConfirmDanger,
+ otherProjectSettings: () =>
+ import(
+ 'jh_component/pages/projects/shared/permissions/components/other_project_settings.vue'
+ ),
},
mixins: [settingsMixin, glFeatureFlagsMixin()],
@@ -905,6 +909,7 @@ export default {
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
</gl-form-checkbox>
</project-setting-row>
+ <other-project-settings />
<confirm-danger
v-if="isVisibilityReduced"
button-variant="confirm"
diff --git a/app/channels/awareness_channel.rb b/app/channels/awareness_channel.rb
new file mode 100644
index 00000000000..554e057ca83
--- /dev/null
+++ b/app/channels/awareness_channel.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+class AwarenessChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
+ REFRESH_INTERVAL = ENV.fetch("GITLAB_AWARENESS_REFRESH_INTERVAL_SEC", 60)
+ private_constant :REFRESH_INTERVAL
+
+ # Produces a refresh interval value, based of the
+ # GITLAB_AWARENESS_REFRESH_INTERVAL_SEC environment variable or the given
+ # default. Makes sure, that the interval after a jitter is applied, is never
+ # less than half the predefined interval.
+ def self.refresh_interval(range: -10..10)
+ min = REFRESH_INTERVAL / 2.to_f
+ [min.to_i, REFRESH_INTERVAL.to_i + rand(range)].max.seconds
+ end
+ private_class_method :refresh_interval
+
+ # keep clients updated about session membership
+ periodically every: self.refresh_interval do
+ transmit payload
+ end
+
+ def subscribed
+ reject unless valid_subscription?
+ return if subscription_rejected?
+
+ stream_for session, coder: ActiveSupport::JSON
+
+ session.join(current_user)
+ AwarenessChannel.broadcast_to(session, payload)
+ end
+
+ def unsubscribed
+ return if subscription_rejected?
+
+ session.leave(current_user)
+ AwarenessChannel.broadcast_to(session, payload)
+ end
+
+ # Allows a client to let the server know they are still around. This is not
+ # like a heartbeat mechanism. This can be triggered by any action that results
+ # in a meaningful "presence" update. Like scrolling the screen (debounce),
+ # window becoming active, user starting to type in a text field, etc.
+ def touch
+ session.touch!(current_user)
+
+ transmit payload
+ end
+
+ private
+
+ def valid_subscription?
+ current_user.present? && path.present?
+ end
+
+ def payload
+ { collaborators: collaborators }
+ end
+
+ def collaborators
+ session.online_users_with_last_activity.map do |user, last_activity|
+ collaborator(user, last_activity)
+ end
+ end
+
+ def collaborator(user, last_activity)
+ {
+ id: user.id,
+ name: user.name,
+ avatar_url: user.avatar_url(size: 36),
+ last_activity: last_activity,
+ last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
+ Time.zone.now, last_activity
+ )
+ }
+ end
+
+ def session
+ @session ||= AwarenessSession.for(path)
+ end
+
+ def path
+ params[:path]
+ end
+end
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index 37b23345d2a..2021961772a 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -9,7 +9,7 @@ module Groups::GroupMembersHelper
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
end
- def group_members_app_data(group, members:, invited:, access_requests:, include_relations:, search:)
+ def group_members_app_data(group, members:, invited:, access_requests:, banned:, include_relations:, search:)
{
user: group_members_list_data(group, members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }),
group: group_group_links_list_data(group, include_relations, search),
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index 20d40626449..ec64746d6b6 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -53,4 +53,14 @@ module PackagesHelper
project.container_expiration_policy.nil? &&
project.container_repositories.exists?
end
+
+ def show_container_registry_settings(project)
+ Gitlab.config.registry.enabled &&
+ Ability.allowed?(current_user, :admin_container_image, project)
+ end
+
+ def show_package_registry_settings(project)
+ Gitlab.config.packages.enabled &&
+ Ability.allowed?(current_user, :admin_package, project)
+ end
end
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
index 602888182b1..a84a3454a27 100644
--- a/app/models/awareness_session.rb
+++ b/app/models/awareness_session.rb
@@ -143,17 +143,34 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
end
end
+ def to_param
+ id&.to_s
+ end
+
+ def to_s
+ "awareness_session=#{id}"
+ end
+
+ def online_users_with_last_activity(threshold: PRESENCE_LIFETIME)
+ users_with_last_activity.filter do |_user, last_activity|
+ user_online?(last_activity, threshold: threshold)
+ end
+ end
+
def users
User.where(id: user_ids)
end
def users_with_last_activity
- # where in (x, y, [...z]) is a set and does not maintain any order, we need to
- # make sure to establish a stable order for both, the pairs returned from
+ # where in (x, y, [...z]) is a set and does not maintain any order, we need
+ # to make sure to establish a stable order for both, the pairs returned from
# redis and the ActiveRecord query. Using IDs in ascending order.
user_ids, last_activities = user_ids_with_last_activity
.sort_by(&:first)
.transpose
+
+ return [] if user_ids.blank?
+
users = User.where(id: user_ids).order(id: :asc)
users.zip(last_activities)
end
@@ -162,6 +179,10 @@ class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
attr_reader :id
+ def user_online?(last_activity, threshold:)
+ last_activity.to_i + threshold.to_i > Time.zone.now.to_i
+ end
+
# converts session id from hex to integer representation
def id_i
Integer(id, 16) if id.present?
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 850f25a6089..54270dc186e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -225,6 +225,10 @@ class ProjectPolicy < BasePolicy
Gitlab.config.registry.enabled
end
+ condition :packages_enabled do
+ Gitlab.config.packages.enabled
+ end
+
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
# not.
rule { guest | admin }.enable :read_project_for_iids
@@ -795,6 +799,10 @@ class ProjectPolicy < BasePolicy
enable :view_package_registry_project_settings
end
+ rule { packages_enabled & can?(:admin_package) }.policy do
+ enable :view_package_registry_project_settings
+ end
+
private
def user_is_user?
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 4150766d3d3..d9fef8940eb 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -26,6 +26,7 @@
members: @members,
invited: @invited_members,
access_requests: @requesters,
+ banned: @banned || [],
include_relations: @include_relations,
search: params[:search_groups]).to_json } }
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml
index 378bb0f9306..1a7821d3268 100644
--- a/app/views/projects/settings/packages_and_registries/show.html.haml
+++ b/app/views/projects/settings/packages_and_registries/show.html.haml
@@ -8,6 +8,8 @@
keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json,
is_admin: current_user&.admin.to_s,
+ show_container_registry_settings: show_container_registry_settings(@project).to_s,
+ show_package_registry_settings: show_package_registry_settings(@project).to_s,
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
enable_historic_entries: container_expiration_policies_historic_entry_enabled?.to_s,
help_page_path: help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),