Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9c8e8b5ffc
commit
f602da84d1
|
@ -1 +1 @@
|
|||
76dabc8174f7978025f48adcfab0a19c85416531
|
||||
ca638e23ca921cf2f2f3cdc8a6ff033af667170b
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<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 default {
|
||||
name: 'MembersTabs',
|
||||
ACTIVE_TAB_QUERY_PARAM_NAME,
|
||||
TABS: [
|
||||
export const TABS = [
|
||||
{
|
||||
namespace: MEMBER_TYPES.user,
|
||||
title: __('Members'),
|
||||
|
@ -34,7 +36,13 @@ export default {
|
|||
canManageMembersPermissionsRequired: true,
|
||||
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
|
||||
},
|
||||
],
|
||||
...EE_TABS,
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'MembersTabs',
|
||||
ACTIVE_TAB_QUERY_PARAM_NAME,
|
||||
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);
|
||||
...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);
|
||||
},
|
||||
groupCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.group);
|
||||
},
|
||||
inviteCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.invite);
|
||||
},
|
||||
accessRequestCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.accessRequest);
|
||||
},
|
||||
}),
|
||||
};
|
||||
}, {}),
|
||||
),
|
||||
urlParams() {
|
||||
return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { MEMBER_TYPES } from '../../constants';
|
||||
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
|
||||
import {
|
||||
isGroup,
|
||||
isDirectMember,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
fragment PackagesCleanupPolicyFields on PackagesCleanupPolicy {
|
||||
keepNDuplicatedPackageFiles
|
||||
nextRunAt
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#import "../fragments/packages_cleanup_policy.fragment.graphql"
|
||||
|
||||
mutation updatePackagesCleanupPolicy($input: UpdatePackagesCleanupPolicyInput!) {
|
||||
updatePackagesCleanupPolicy(input: $input) {
|
||||
packagesCleanupPolicy {
|
||||
...PackagesCleanupPolicyFields
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#import "../fragments/packages_cleanup_policy.fragment.graphql"
|
||||
|
||||
query getProjectPackagesCleanupPolicy($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
packagesCleanupPolicy {
|
||||
...PackagesCleanupPolicyFields
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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', {});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateInsertOrUpdateVulnerabilityReadsFunction < Gitlab::Database::Migration[2.0]
|
||||
FUNCTION_NAME = 'insert_or_update_vulnerability_reads'
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
execute(<<~SQL)
|
||||
CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
severity smallint;
|
||||
state smallint;
|
||||
report_type smallint;
|
||||
resolved_on_default_branch boolean;
|
||||
present_on_default_branch boolean;
|
||||
BEGIN
|
||||
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
SELECT
|
||||
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch, vulnerabilities.present_on_default_branch
|
||||
INTO
|
||||
severity, state, report_type, resolved_on_default_branch, present_on_default_branch
|
||||
FROM
|
||||
vulnerabilities
|
||||
WHERE
|
||||
vulnerabilities.id = NEW.vulnerability_id;
|
||||
|
||||
IF present_on_default_branch IS NOT true THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
execute(<<~SQL)
|
||||
CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
severity smallint;
|
||||
state smallint;
|
||||
report_type smallint;
|
||||
resolved_on_default_branch boolean;
|
||||
BEGIN
|
||||
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
SELECT
|
||||
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
|
||||
INTO
|
||||
severity, state, report_type, resolved_on_default_branch
|
||||
FROM
|
||||
vulnerabilities
|
||||
WHERE
|
||||
vulnerabilities.id = NEW.vulnerability_id;
|
||||
|
||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateVulnerabilityReadsForAnExistingVulnerabilityRecord < Gitlab::Database::Migration[2.0]
|
||||
include Gitlab::Database::SchemaHelpers
|
||||
|
||||
FUNCTION_NAME = 'insert_vulnerability_reads_from_vulnerability'
|
||||
TRIGGER_NAME = 'trigger_insert_vulnerability_reads_from_vulnerability'
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
execute(<<~SQL)
|
||||
CREATE FUNCTION #{FUNCTION_NAME}() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
scanner_id bigint;
|
||||
uuid uuid;
|
||||
location_image text;
|
||||
cluster_agent_id text;
|
||||
casted_cluster_agent_id bigint;
|
||||
BEGIN
|
||||
SELECT
|
||||
v_o.scanner_id, v_o.uuid, v_o.location->>'image', v_o.location->'kubernetes_resource'->>'agent_id', CAST(v_o.location->'kubernetes_resource'->>'agent_id' AS bigint)
|
||||
INTO
|
||||
scanner_id, uuid, location_image, cluster_agent_id, casted_cluster_agent_id
|
||||
FROM
|
||||
vulnerability_occurrences v_o
|
||||
WHERE
|
||||
v_o.vulnerability_id = NEW.id
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
VALUES (NEW.id, NEW.project_id, scanner_id, NEW.report_type, NEW.severity, NEW.state, NEW.resolved_on_default_branch, uuid::uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
|
||||
execute(<<~SQL)
|
||||
CREATE TRIGGER #{TRIGGER_NAME}
|
||||
AFTER UPDATE ON vulnerabilities
|
||||
FOR EACH ROW
|
||||
WHEN (
|
||||
OLD.present_on_default_branch IS NOT true AND NEW.present_on_default_branch IS true
|
||||
)
|
||||
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||
drop_function(FUNCTION_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateTriggerUpdateVulnerabilityReadsOnVulnerabilityUpdate < Gitlab::Database::Migration[2.0]
|
||||
include Gitlab::Database::SchemaHelpers
|
||||
|
||||
TRIGGER_NAME = 'trigger_update_vulnerability_reads_on_vulnerability_update'
|
||||
FUNCTION_NAME = 'update_vulnerability_reads_from_vulnerability'
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||
|
||||
# If the vulnerability record was not already marked as `present_on_default_branch`,
|
||||
# we shouldn't try to update `vulnerability_records` since there will be no records
|
||||
# anyway.
|
||||
execute(<<~SQL)
|
||||
CREATE TRIGGER #{TRIGGER_NAME}
|
||||
AFTER UPDATE ON vulnerabilities
|
||||
FOR EACH ROW
|
||||
WHEN (
|
||||
OLD.present_on_default_branch IS TRUE AND
|
||||
(
|
||||
OLD.severity IS DISTINCT FROM NEW.severity OR
|
||||
OLD.state IS DISTINCT FROM NEW.state OR
|
||||
OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch
|
||||
)
|
||||
)
|
||||
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_trigger(:vulnerabilities, TRIGGER_NAME)
|
||||
|
||||
execute(<<~SQL)
|
||||
CREATE TRIGGER #{TRIGGER_NAME}
|
||||
AFTER UPDATE ON vulnerabilities
|
||||
FOR EACH ROW
|
||||
WHEN (
|
||||
OLD.severity IS DISTINCT FROM NEW.severity OR
|
||||
OLD.state IS DISTINCT FROM NEW.state OR
|
||||
OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch
|
||||
)
|
||||
EXECUTE PROCEDURE #{FUNCTION_NAME}();
|
||||
SQL
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
f0bba8e67c97d6dea461d8626a07820c52e20ab6578ad40e8873ad0031a2ce62
|
|
@ -0,0 +1 @@
|
|||
db2c19f15a03a6222627875d8bd27368de43fb6485961f866de61b3017796e28
|
|
@ -0,0 +1 @@
|
|||
42387b8524845aeb76d8b6584ffa480819f682538ca9578492eed53baa49bc09
|
|
@ -53,6 +53,7 @@ DECLARE
|
|||
state smallint;
|
||||
report_type smallint;
|
||||
resolved_on_default_branch boolean;
|
||||
present_on_default_branch boolean;
|
||||
BEGIN
|
||||
IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN
|
||||
RETURN NULL;
|
||||
|
@ -63,14 +64,18 @@ BEGIN
|
|||
END IF;
|
||||
|
||||
SELECT
|
||||
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch
|
||||
vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch, vulnerabilities.present_on_default_branch
|
||||
INTO
|
||||
severity, state, report_type, resolved_on_default_branch
|
||||
severity, state, report_type, resolved_on_default_branch, present_on_default_branch
|
||||
FROM
|
||||
vulnerabilities
|
||||
WHERE
|
||||
vulnerabilities.id = NEW.vulnerability_id;
|
||||
|
||||
IF present_on_default_branch IS NOT true THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id', CAST(NEW.location->'kubernetes_resource'->>'agent_id' AS bigint))
|
||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||
|
@ -89,6 +94,33 @@ RETURN NULL;
|
|||
END
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION insert_vulnerability_reads_from_vulnerability() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
scanner_id bigint;
|
||||
uuid uuid;
|
||||
location_image text;
|
||||
cluster_agent_id text;
|
||||
casted_cluster_agent_id bigint;
|
||||
BEGIN
|
||||
SELECT
|
||||
v_o.scanner_id, v_o.uuid, v_o.location->>'image', v_o.location->'kubernetes_resource'->>'agent_id', CAST(v_o.location->'kubernetes_resource'->>'agent_id' AS bigint)
|
||||
INTO
|
||||
scanner_id, uuid, location_image, cluster_agent_id, casted_cluster_agent_id
|
||||
FROM
|
||||
vulnerability_occurrences v_o
|
||||
WHERE
|
||||
v_o.vulnerability_id = NEW.id
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
VALUES (NEW.id, NEW.project_id, scanner_id, NEW.report_type, NEW.severity, NEW.state, NEW.resolved_on_default_branch, uuid::uuid, location_image, cluster_agent_id, casted_cluster_agent_id)
|
||||
ON CONFLICT(vulnerability_id) DO NOTHING;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION next_traversal_ids_sibling(traversal_ids integer[]) RETURNS integer[]
|
||||
LANGUAGE plpgsql IMMUTABLE STRICT
|
||||
AS $$
|
||||
|
@ -31645,6 +31677,8 @@ CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON integrations
|
|||
|
||||
CREATE TRIGGER trigger_insert_or_update_vulnerability_reads_from_occurrences AFTER INSERT OR UPDATE ON vulnerability_occurrences FOR EACH ROW EXECUTE FUNCTION insert_or_update_vulnerability_reads();
|
||||
|
||||
CREATE TRIGGER trigger_insert_vulnerability_reads_from_vulnerability AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.present_on_default_branch IS NOT TRUE) AND (new.present_on_default_branch IS TRUE))) EXECUTE FUNCTION insert_vulnerability_reads_from_vulnerability();
|
||||
|
||||
CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event();
|
||||
|
||||
CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event();
|
||||
|
@ -31659,7 +31693,7 @@ CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_update AFT
|
|||
|
||||
CREATE TRIGGER trigger_update_location_on_vulnerability_occurrences_update AFTER UPDATE ON vulnerability_occurrences FOR EACH ROW WHEN (((new.report_type = ANY (ARRAY[2, 7])) AND (((old.location ->> 'image'::text) IS DISTINCT FROM (new.location ->> 'image'::text)) OR (((old.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text) IS DISTINCT FROM ((new.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text))))) EXECUTE FUNCTION update_location_from_vulnerability_occurrences();
|
||||
|
||||
CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability();
|
||||
CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.present_on_default_branch IS TRUE) AND ((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch)))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability();
|
||||
|
||||
CREATE TRIGGER users_loose_fk_trigger AFTER DELETE ON users REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
|
||||
|
||||
|
|
|
@ -7,18 +7,32 @@ type: reference
|
|||
|
||||
# Password storage **(FREE)**
|
||||
|
||||
> PBKDF2 and SHA512 [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360658) in GitLab 15.2 [with flags](../administration/feature_flags.md) named `pbkdf2_password_encryption` and `pbkdf2_password_encryption_write`. Disabled by default.
|
||||
|
||||
GitLab stores user passwords in a hashed format to prevent passwords from being
|
||||
stored as plain text.
|
||||
|
||||
GitLab uses the [Devise](https://github.com/heartcombo/devise) authentication
|
||||
library to hash user passwords. Created password hashes have these attributes:
|
||||
|
||||
- **Hashing**: The [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
|
||||
- **Hashing**:
|
||||
- **BCrypt**: By default, the [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt) hashing
|
||||
function is used to generate the hash of the provided password. This is a
|
||||
strong, industry-standard cryptographic hashing function.
|
||||
- **PBKDF2 and SHA512**: Starting in GitLab 15.2, PBKDF2 and SHA512 are supported
|
||||
behind the following feature flags (disabled by default):
|
||||
- `pbkdf2_password_encryption` - Enables reading and comparison of PBKDF2 + SHA512
|
||||
hashed passwords and supports fallback for BCrypt hashed passwords.
|
||||
- `pbkdf2_password_encryption_write` - Enables new passwords to be saved
|
||||
using PBKDF2 and SHA512, and existing BCrypt passwords to be migrated when users sign in.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flags](../administration/feature_flags.md) named `pbkdf2_password_encryption` and `pbkdf2_password_encryption_write`.
|
||||
|
||||
- **Stretching**: Password hashes are [stretched](https://en.wikipedia.org/wiki/Key_stretching)
|
||||
to harden against brute-force attacks. By default, GitLab uses a stretching
|
||||
factor of 10.
|
||||
factor of 10 for BCrypt and 20,000 for PBKDF2 + SHA512.
|
||||
- **Salting**: A [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
|
||||
is added to each password to harden against pre-computed hash and dictionary
|
||||
attacks. To increase security, each salt is randomly generated for each
|
||||
|
|
|
@ -243,29 +243,13 @@ See [Multiple Kubernetes clusters for Auto DevOps](multiple_clusters_auto_devops
|
|||
|
||||
## Customizing the Kubernetes namespace
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27630) in GitLab 12.6.
|
||||
In GitLab 14.5 and earlier, you could use `environment:kubernetes:namespace`
|
||||
to specify a namespace for the environment.
|
||||
However, this feature was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8),
|
||||
along with certificate-based integration.
|
||||
|
||||
For clusters not managed by GitLab, you can customize the namespace in
|
||||
`.gitlab-ci.yml` by specifying
|
||||
[`environment:kubernetes:namespace`](../../ci/environments/index.md#configure-kubernetes-deployments-deprecated).
|
||||
For example, the following configuration overrides the namespace used for
|
||||
`production` deployments:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Auto-DevOps.gitlab-ci.yml
|
||||
|
||||
production:
|
||||
environment:
|
||||
kubernetes:
|
||||
namespace: production
|
||||
```
|
||||
|
||||
When deploying to a custom namespace with Auto DevOps, the service account
|
||||
provided with the cluster needs at least the `edit` role within the namespace.
|
||||
|
||||
- If the service account can create namespaces, then the namespace can be created on-demand.
|
||||
- Otherwise, the namespace must exist prior to deployment.
|
||||
You should now use the `KUBE_NAMESPACE` environment variable and
|
||||
[limit the environments it is available for](../../ci/environments/index.md#scope-environments-with-specs).
|
||||
|
||||
## Using components of Auto DevOps
|
||||
|
||||
|
|
|
@ -303,7 +303,9 @@ support `-fips`.
|
|||
Starting with GitLab 14.10, `-fips` is automatically added to `CS_ANALYZER_IMAGE` when FIPS mode is
|
||||
enabled in the GitLab instance.
|
||||
|
||||
Container scanning of images in authenticated registries is not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) is enabled.
|
||||
Container scanning of images in authenticated registries is not supported when [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode)
|
||||
is enabled. When `CI_GITLAB_FIPS_MODE` is `"true"`, and `DOCKER_USER` or `DOCKER_PASSWORD` is set,
|
||||
the analyzer exits with an error and does not perform the scan.
|
||||
|
||||
### Enable Container Scanning through an automatic merge request
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ Where `<namespace>` is the [namespace](../../../user/group/index.md#namespaces)
|
|||
To work with Terraform modules in [GitLab CI/CD](../../../ci/index.md), you can use
|
||||
`CI_JOB_TOKEN` in place of the personal access token in your commands.
|
||||
|
||||
For example:
|
||||
For example, this job uploads a new module for the `local` [system provider](https://registry.terraform.io/browse/providers) and uses the module version from the Git commit tag:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
|
@ -121,15 +121,18 @@ upload:
|
|||
TERRAFORM_MODULE_DIR: ${CI_PROJECT_DIR} # The path to your Terraform module
|
||||
TERRAFORM_MODULE_NAME: ${CI_PROJECT_NAME} # The name of your Terraform module
|
||||
TERRAFORM_MODULE_SYSTEM: local # The system or provider your Terraform module targets (ex. local, aws, google)
|
||||
TERRAFORM_MODULE_VERSION: ${CI_COMMIT_TAG} # The version of your Terraform module to be published to your project's registry
|
||||
TERRAFORM_MODULE_VERSION: ${CI_COMMIT_TAG} # Tag commits with SemVer for the version of your Terraform module to be published
|
||||
script:
|
||||
- tar -cvzf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git .
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/terraform/modules/${TERRAFORM_MODULE_NAME}/${TERRAFORM_MODULE_SYSTEM}/${TERRAFORM_MODULE_VERSION}/file'
|
||||
- TERRAFORM_MODULE_NAME=$(echo "${TERRAFORM_MODULE_NAME}" | tr " _" -) # module-name must not have spaces or underscores, so translate them to hyphens
|
||||
- tar -vczf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git .
|
||||
- 'curl --location --header "JOB-TOKEN: ${CI_JOB_TOKEN}"
|
||||
--upload-file ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz
|
||||
${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/terraform/modules/${TERRAFORM_MODULE_NAME}/${TERRAFORM_MODULE_SYSTEM}/${TERRAFORM_MODULE_VERSION}/file'
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
```
|
||||
|
||||
To trigger this upload job, add a Git tag to your commit. The `rules:if: $CI_COMMIT_TAG` defines this so that not every commit to your repo triggers the upload.
|
||||
To trigger this upload job, add a Git tag to your commit. Ensure the tag follows the [Semantic Versioning Specification](https://semver.org/) that Terraform requires. The `rules:if: $CI_COMMIT_TAG` ensures that only tagged commits to your repo trigger the module upload job.
|
||||
For other ways to control jobs in your CI/CD pipeline, refer to the [`.gitlab-ci.yml`](../../../ci/yaml/index.md) keyword reference.
|
||||
|
||||
## Example projects
|
||||
|
|
|
@ -5802,6 +5802,9 @@ msgstr ""
|
|||
msgid "BambooService|The user with API access to the Bamboo server."
|
||||
msgstr ""
|
||||
|
||||
msgid "Banned"
|
||||
msgstr ""
|
||||
|
||||
msgid "Banner message"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27707,6 +27710,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Error publishing"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Examples of assets include .pom & .jar files"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Failed to load the package data"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27764,6 +27770,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|License information located at %{link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Manage storage used by package assets"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Manually Published"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27782,6 +27791,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|NuGet Command"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Number of duplicate assets to keep"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Package Registry"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27907,6 +27919,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Unable to load package"
|
||||
msgstr ""
|
||||
|
||||
msgid "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."
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -37,6 +37,8 @@ module QA
|
|||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.disable(:simulate_pipeline)
|
||||
|
||||
Flow::Login.sign_in
|
||||
project.visit!
|
||||
Page::Project::Menu.perform(&:go_to_pipeline_editor)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe AwarenessChannel, :clean_gitlab_redis_shared_state, type: :channel do
|
||||
before do
|
||||
stub_action_cable_connection(current_user: user)
|
||||
end
|
||||
|
||||
context "with user" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe "when no path parameter given" do
|
||||
it "rejects subscription" do
|
||||
subscribe path: nil
|
||||
|
||||
expect(subscription).to be_rejected
|
||||
end
|
||||
end
|
||||
|
||||
describe "with valid path parameter" do
|
||||
it "successfully subscribes" do
|
||||
subscribe path: "/test"
|
||||
|
||||
session = AwarenessSession.for("/test")
|
||||
|
||||
expect(subscription).to be_confirmed
|
||||
# check if we can use session object instead
|
||||
expect(subscription).to have_stream_from("awareness:#{session.to_param}")
|
||||
end
|
||||
|
||||
it "broadcasts set of collaborators when subscribing" do
|
||||
session = AwarenessSession.for("/test")
|
||||
|
||||
freeze_time do
|
||||
collaborator = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url(size: 36),
|
||||
last_activity: Time.zone.now,
|
||||
last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
|
||||
Time.zone.now, Time.zone.now
|
||||
)
|
||||
}
|
||||
|
||||
expect do
|
||||
subscribe path: "/test"
|
||||
end.to have_broadcasted_to("awareness:#{session.to_param}")
|
||||
.with(collaborators: [collaborator])
|
||||
end
|
||||
end
|
||||
|
||||
it "transmits payload when user is touched" do
|
||||
subscribe path: "/test"
|
||||
|
||||
perform :touch
|
||||
|
||||
expect(transmissions.size).to be 1
|
||||
end
|
||||
|
||||
it "unsubscribes from channel" do
|
||||
subscribe path: "/test"
|
||||
session = AwarenessSession.for("/test")
|
||||
|
||||
expect { subscription.unsubscribe_from_channel }
|
||||
.to change { session.size}.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with guest" do
|
||||
let(:user) { nil }
|
||||
|
||||
it "rejects subscription" do
|
||||
subscribe path: "/test"
|
||||
|
||||
expect(subscription).to be_rejected
|
||||
end
|
||||
end
|
||||
end
|
|
@ -49,7 +49,7 @@ RSpec.describe 'Project navbar' do
|
|||
stub_config(pages: { enabled: true })
|
||||
|
||||
insert_after_sub_nav_item(
|
||||
_('CI/CD'),
|
||||
_('Packages & Registries'),
|
||||
within: _('Settings'),
|
||||
new_sub_nav_item_name: _('Pages')
|
||||
)
|
||||
|
@ -60,18 +60,22 @@ RSpec.describe 'Project navbar' do
|
|||
it_behaves_like 'verified navigation bar'
|
||||
end
|
||||
|
||||
context 'when package registry is available' do
|
||||
before do
|
||||
stub_config(packages: { enabled: true })
|
||||
|
||||
visit project_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'verified navigation bar'
|
||||
end
|
||||
|
||||
context 'when container registry is available' do
|
||||
before do
|
||||
stub_config(registry: { enabled: true })
|
||||
|
||||
insert_container_nav
|
||||
|
||||
insert_after_sub_nav_item(
|
||||
_('CI/CD'),
|
||||
within: _('Settings'),
|
||||
new_sub_nav_item_name: _('Packages & Registries')
|
||||
)
|
||||
|
||||
visit project_path(project)
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
|
||||
RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy', :js do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
|
||||
|
||||
|
@ -23,14 +23,15 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'shows available section' do
|
||||
subject
|
||||
|
||||
settings_block = find('[data-testid="registry-settings-app"]')
|
||||
settings_block = find('[data-testid="container-expiration-policy-project-settings"]')
|
||||
expect(settings_block).to have_text 'Clean up image tags'
|
||||
end
|
||||
|
||||
it 'saves cleanup policy submit the form' do
|
||||
subject
|
||||
|
||||
within '[data-testid="registry-settings-app"]' do
|
||||
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||
click_button('Expand')
|
||||
select('Every day', from: 'Run cleanup')
|
||||
select('50 tags per image name', from: 'Keep the most recent:')
|
||||
fill_in('Keep tags matching:', with: 'stable')
|
||||
|
@ -48,7 +49,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'does not save cleanup policy submit form with invalid regex' do
|
||||
subject
|
||||
|
||||
within '[data-testid="registry-settings-app"]' do
|
||||
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||
click_button('Expand')
|
||||
fill_in('Remove tags matching:', with: '*-production')
|
||||
|
||||
submit_button = find('[data-testid="save-button"')
|
||||
|
@ -73,7 +75,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'displays the related section' do
|
||||
subject
|
||||
|
||||
within '[data-testid="registry-settings-app"]' do
|
||||
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||
click_button('Expand')
|
||||
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
|
||||
end
|
||||
end
|
||||
|
@ -87,7 +90,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'does not display the related section' do
|
||||
subject
|
||||
|
||||
within '[data-testid="registry-settings-app"]' do
|
||||
within '[data-testid="container-expiration-policy-project-settings"]' do
|
||||
click_button('Expand')
|
||||
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
|
||||
end
|
||||
end
|
||||
|
@ -100,7 +104,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'does not exists' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
|
||||
expect(page).not_to have_selector('[data-testid="container-expiration-policy-project-settings"]')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -110,7 +114,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
it 'does not exists' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
|
||||
expect(page).not_to have_selector('[data-testid="container-expiration-policy-project-settings"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`]
|
|||
<expiration-dropdown-stub
|
||||
class="gl-mr-7 gl-mb-0!"
|
||||
data-testid="cadence-dropdown"
|
||||
description=""
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Run cleanup:"
|
||||
name="cadence"
|
||||
|
@ -22,6 +23,7 @@ exports[`Container Expiration Policy Settings Form Enable matches snapshot 1`] =
|
|||
exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="keep-n-dropdown"
|
||||
description=""
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Keep the most recent:"
|
||||
name="keep-n"
|
||||
|
@ -44,6 +46,7 @@ exports[`Container Expiration Policy Settings Form Keep Regex matches snapshot 1
|
|||
exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="older-than-dropdown"
|
||||
description=""
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
label="Remove tags older than:"
|
||||
name="older-than"
|
||||
|
|
|
@ -43,11 +43,6 @@ describe('Container expiration policy project settings', () => {
|
|||
GlSprintf,
|
||||
SettingsBlock,
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
|
@ -98,7 +93,7 @@ describe('Container expiration policy project settings', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(true);
|
||||
expect(findSettingsBlock().props('collapsible')).toBe(false);
|
||||
expect(findSettingsBlock().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('the form is disabled', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ describe('ExpirationDropdown', () => {
|
|||
|
||||
const findFormSelect = () => wrapper.find(GlFormSelect);
|
||||
const findFormGroup = () => wrapper.find(GlFormGroup);
|
||||
const findDescription = () => wrapper.find('[data-testid="description"]');
|
||||
const findOptions = () => wrapper.findAll('[data-testid="option"]');
|
||||
|
||||
const mountComponent = (props) => {
|
||||
|
@ -47,6 +48,14 @@ describe('ExpirationDropdown', () => {
|
|||
|
||||
expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
|
||||
});
|
||||
|
||||
it('renders the description if passed', () => {
|
||||
mountComponent({
|
||||
description: 'test description',
|
||||
});
|
||||
|
||||
expect(findDescription().html()).toContain('test description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('model', () => {
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
|
||||
import component from '~/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue';
|
||||
import {
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
|
||||
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
|
||||
import Tracking from '~/tracking';
|
||||
import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Packages Cleanup Policy Settings Form', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const defaultProvidedValues = {
|
||||
projectPath: 'path',
|
||||
};
|
||||
|
||||
const {
|
||||
data: {
|
||||
project: { packagesCleanupPolicy },
|
||||
},
|
||||
} = packagesCleanupPolicyPayload();
|
||||
|
||||
const defaultProps = {
|
||||
value: { ...packagesCleanupPolicy },
|
||||
};
|
||||
|
||||
const trackingPayload = {
|
||||
label: 'packages_cleanup_policies',
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'form-element' });
|
||||
const findSaveButton = () => wrapper.findByTestId('save-button');
|
||||
const findKeepNDuplicatedPackageFilesDropdown = () =>
|
||||
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
|
||||
|
||||
const submitForm = async () => {
|
||||
findForm().trigger('submit');
|
||||
return waitForPromises();
|
||||
};
|
||||
|
||||
const mountComponent = ({
|
||||
props = defaultProps,
|
||||
data,
|
||||
config,
|
||||
provide = defaultProvidedValues,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(component, {
|
||||
stubs: {
|
||||
GlLoadingIcon,
|
||||
},
|
||||
propsData: { ...props },
|
||||
provide,
|
||||
data() {
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
const mountComponentWithApollo = ({
|
||||
provide = defaultProvidedValues,
|
||||
mutationResolver,
|
||||
queryPayload = packagesCleanupPolicyPayload(),
|
||||
} = {}) => {
|
||||
const requestHandlers = [[updatePackagesCleanupPolicyMutation, mutationResolver]];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
|
||||
const {
|
||||
data: {
|
||||
project: { packagesCleanupPolicy: value },
|
||||
},
|
||||
} = queryPayload;
|
||||
|
||||
mountComponent({
|
||||
provide,
|
||||
props: {
|
||||
...defaultProps,
|
||||
value,
|
||||
},
|
||||
config: {
|
||||
apolloProvider: fakeApollo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('keepNDuplicatedPackageFiles', () => {
|
||||
it('renders dropdown', () => {
|
||||
mountComponent();
|
||||
|
||||
const element = findKeepNDuplicatedPackageFilesDropdown();
|
||||
|
||||
expect(element.exists()).toBe(true);
|
||||
expect(element.props('label')).toMatchInterpolatedText(KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL);
|
||||
expect(element.props('description')).toEqual(KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION);
|
||||
});
|
||||
|
||||
it('input event triggers a model update', () => {
|
||||
mountComponent();
|
||||
|
||||
findKeepNDuplicatedPackageFilesDropdown().vm.$emit('input', 'foo');
|
||||
expect(wrapper.emitted('input')[0][0]).toMatchObject({
|
||||
keepNDuplicatedPackageFiles: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the default option when none are selected', () => {
|
||||
mountComponent({ props: { value: {} } });
|
||||
expect(findKeepNDuplicatedPackageFilesDropdown().props('value')).toEqual('ALL_PACKAGE_FILES');
|
||||
});
|
||||
|
||||
it.each`
|
||||
isLoading | mutationLoading
|
||||
${true} | ${false}
|
||||
${true} | ${true}
|
||||
${false} | ${true}
|
||||
`(
|
||||
'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading',
|
||||
({ isLoading, mutationLoading }) => {
|
||||
mountComponent({
|
||||
props: { isLoading, value: {} },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
expect(findKeepNDuplicatedPackageFilesDropdown().props('disabled')).toEqual(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('has the correct formOptions', () => {
|
||||
mountComponent();
|
||||
expect(findKeepNDuplicatedPackageFilesDropdown().props('formOptions')).toEqual(
|
||||
wrapper.vm.$options.formOptions.keepNDuplicatedPackageFiles,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form', () => {
|
||||
describe('actions', () => {
|
||||
describe('submit button', () => {
|
||||
it('has type submit', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findSaveButton().attributes('type')).toBe('submit');
|
||||
});
|
||||
|
||||
it.each`
|
||||
isLoading | mutationLoading | disabled
|
||||
${true} | ${true} | ${true}
|
||||
${true} | ${false} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${false} | ${false} | ${false}
|
||||
`(
|
||||
'when isLoading is $isLoading and mutationLoading is $mutationLoading is disabled',
|
||||
({ isLoading, mutationLoading, disabled }) => {
|
||||
mountComponent({
|
||||
props: { ...defaultProps, isLoading },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
|
||||
expect(findSaveButton().props('disabled')).toBe(disabled);
|
||||
expect(findKeepNDuplicatedPackageFilesDropdown().props('disabled')).toBe(disabled);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
isLoading | mutationLoading | showLoading
|
||||
${true} | ${true} | ${true}
|
||||
${true} | ${false} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${false} | ${false} | ${false}
|
||||
`(
|
||||
'when isLoading is $isLoading and mutationLoading is $mutationLoading is $showLoading that the loading icon is shown',
|
||||
({ isLoading, mutationLoading, showLoading }) => {
|
||||
mountComponent({
|
||||
props: { ...defaultProps, isLoading },
|
||||
data: { mutationLoading },
|
||||
});
|
||||
|
||||
expect(findSaveButton().props('loading')).toBe(showLoading);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form submit event', () => {
|
||||
it('dispatches the correct apollo mutation', () => {
|
||||
const mutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(packagesCleanupPolicyMutationPayload());
|
||||
mountComponentWithApollo({
|
||||
mutationResolver,
|
||||
});
|
||||
|
||||
findForm().trigger('submit');
|
||||
|
||||
expect(mutationResolver).toHaveBeenCalledWith({
|
||||
input: {
|
||||
keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES',
|
||||
projectPath: 'path',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks the submit event', () => {
|
||||
mountComponentWithApollo({
|
||||
mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()),
|
||||
});
|
||||
|
||||
findForm().trigger('submit');
|
||||
|
||||
expect(Tracking.event).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
'submit_packages_cleanup_form',
|
||||
trackingPayload,
|
||||
);
|
||||
});
|
||||
|
||||
it('show a success toast when submit succeed', async () => {
|
||||
mountComponentWithApollo({
|
||||
mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()),
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
describe('when submit fails', () => {
|
||||
it('shows an error', async () => {
|
||||
mountComponentWithApollo({
|
||||
mutationResolver: jest.fn().mockRejectedValue(packagesCleanupPolicyMutationPayload()),
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import component from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
|
||||
import PackagesCleanupPolicyForm from '~/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue';
|
||||
import { FETCH_SETTINGS_ERROR_MESSAGE } 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 { packagesCleanupPolicyPayload, packagesCleanupPolicyData } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Packages cleanup policy project settings', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const defaultProvidedValues = {
|
||||
projectPath: 'path',
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findFormComponent = () => wrapper.findComponent(PackagesCleanupPolicyForm);
|
||||
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
|
||||
|
||||
const mountComponent = (provide = defaultProvidedValues, config) => {
|
||||
wrapper = shallowMount(component, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
SettingsBlock,
|
||||
},
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
|
||||
const requestHandlers = [[packagesCleanupPolicyQuery, resolver]];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
mountComponent(provide, {
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
it('renders the setting form', async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockResolvedValue(packagesCleanupPolicyPayload()),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(true);
|
||||
expect(findFormComponent().props('value')).toEqual(packagesCleanupPolicyData);
|
||||
expect(findSettingsBlock().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('fetchSettingsError', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('the form is hidden', () => {
|
||||
expect(findFormComponent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an alert', () => {
|
||||
expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,19 +1,41 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
|
||||
import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
|
||||
import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
|
||||
|
||||
describe('Registry Settings app', () => {
|
||||
let wrapper;
|
||||
|
||||
const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
|
||||
const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('renders container expiration policy component', () => {
|
||||
wrapper = shallowMount(component);
|
||||
|
||||
expect(findContainerExpirationPolicy().exists()).toBe(true);
|
||||
const mountComponent = (provide) => {
|
||||
wrapper = shallowMount(component, {
|
||||
provide,
|
||||
});
|
||||
};
|
||||
|
||||
it.each`
|
||||
showContainerRegistrySettings | showPackageRegistrySettings
|
||||
${true} | ${false}
|
||||
${true} | ${true}
|
||||
${false} | ${true}
|
||||
${false} | ${false}
|
||||
`(
|
||||
'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
|
||||
({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
|
||||
mountComponent({
|
||||
showContainerRegistrySettings,
|
||||
showPackageRegistrySettings,
|
||||
});
|
||||
|
||||
expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings);
|
||||
expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,3 +40,33 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {})
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const packagesCleanupPolicyData = {
|
||||
keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES',
|
||||
nextRunAt: '2020-11-19T07:37:03.941Z',
|
||||
};
|
||||
|
||||
export const packagesCleanupPolicyPayload = (override) => ({
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
packagesCleanupPolicy: {
|
||||
__typename: 'PackagesCleanupPolicy',
|
||||
...packagesCleanupPolicyData,
|
||||
...override,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const packagesCleanupPolicyMutationPayload = ({ override, errors = [] } = {}) => ({
|
||||
data: {
|
||||
updatePackagesCleanupPolicy: {
|
||||
packagesCleanupPolicy: {
|
||||
...packagesCleanupPolicyData,
|
||||
...override,
|
||||
},
|
||||
errors,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
members: present_members(members_collection),
|
||||
invited: present_members(invited),
|
||||
access_requests: present_members(access_requests),
|
||||
banned: [],
|
||||
include_relations: [:inherited, :direct],
|
||||
search: nil
|
||||
)
|
||||
|
@ -117,6 +118,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
members: present_members(members_collection),
|
||||
invited: present_members(invited),
|
||||
access_requests: present_members(access_requests),
|
||||
banned: [],
|
||||
include_relations: include_relations,
|
||||
search: nil
|
||||
)
|
||||
|
|
|
@ -31,16 +31,14 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
end
|
||||
|
||||
context 'with available_runner_releases configured' do
|
||||
let(:runner_releases_double) { instance_double(Gitlab::Ci::RunnerReleases) }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double)
|
||||
allow(runner_releases_double).to receive(:releases)
|
||||
.and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) })
|
||||
allow(runner_releases_double).to receive(:releases_by_minor)
|
||||
.and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) }
|
||||
.group_by(&:without_patch)
|
||||
.transform_values(&:max))
|
||||
url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
|
||||
|
||||
WebMock.stub_request(:get, url).to_return(
|
||||
body: available_runner_releases.map { |v| { name: v } }.to_json,
|
||||
status: 200,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
context 'with no available runner releases' do
|
||||
|
|
|
@ -135,18 +135,20 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
|
|||
|
||||
describe 'Packages & Registries' do
|
||||
let(:item_id) { :packages_and_registries }
|
||||
let(:packages_enabled) { false }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(enabled: container_enabled)
|
||||
stub_config(packages: { enabled: packages_enabled })
|
||||
end
|
||||
|
||||
describe 'when config registry setting is disabled' do
|
||||
describe 'when container registry setting is disabled' do
|
||||
let(:container_enabled) { false }
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
describe 'when config registry setting is enabled' do
|
||||
describe 'when container registry setting is enabled' do
|
||||
let(:container_enabled) { true }
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
|
@ -157,6 +159,19 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
|
|||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when package registry setting is enabled' do
|
||||
let(:container_enabled) { false }
|
||||
let(:packages_enabled) { true }
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
|
||||
describe 'when the user does not have access' do
|
||||
let(:user) { nil }
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Usage Quotas' do
|
||||
|
|
|
@ -2,14 +2,24 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe AwarenessSession do
|
||||
RSpec.describe AwarenessSession, :clean_gitlab_redis_shared_state do
|
||||
subject { AwarenessSession.for(session_id) }
|
||||
|
||||
let!(:user) { create(:user) }
|
||||
let(:session_id) { 1 }
|
||||
|
||||
after do
|
||||
redis_shared_state_cleanup!
|
||||
describe "when initiating a session" do
|
||||
it "provides a string representation of the model instance" do
|
||||
expected = "awareness_session=6b86b273ff34fce"
|
||||
|
||||
expect(subject.to_s).to eql(expected)
|
||||
end
|
||||
|
||||
it "provides a parameterized version of the session identifier" do
|
||||
expected = "6b86b273ff34fce"
|
||||
|
||||
expect(subject.to_param).to eql(expected)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a user joins a session" do
|
||||
|
@ -103,6 +113,26 @@ RSpec.describe AwarenessSession do
|
|||
expect(ttl_user).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
it "fetches user(s) from database" do
|
||||
subject.join(user)
|
||||
|
||||
expect(subject.users.first).to eql(user)
|
||||
end
|
||||
|
||||
it "fetches and filters online user(s) from database" do
|
||||
subject.join(user)
|
||||
|
||||
travel 2.hours do
|
||||
subject.join(user2)
|
||||
|
||||
online_users = subject.online_users_with_last_activity
|
||||
online_user, _ = online_users.first
|
||||
|
||||
expect(online_users.size).to be 1
|
||||
expect(online_user).to eql(user2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a user leaves a session" do
|
||||
|
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Awareness do
|
||||
RSpec.describe Awareness, :clean_gitlab_redis_shared_state do
|
||||
subject { create(:user) }
|
||||
|
||||
let(:session) { AwarenessSession.for(1) }
|
||||
|
||||
after do
|
||||
redis_shared_state_cleanup!
|
||||
end
|
||||
|
||||
describe "when joining a session" do
|
||||
it "increases the number of sessions" do
|
||||
expect { subject.join(session) }
|
||||
|
|
|
@ -1480,6 +1480,11 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
|
||||
describe 'view_package_registry_project_settings' do
|
||||
context 'with packages disabled and' do
|
||||
before do
|
||||
stub_config(packages: { enabled: false })
|
||||
end
|
||||
|
||||
context 'with registry enabled' do
|
||||
before do
|
||||
stub_config(registry: { enabled: true })
|
||||
|
@ -1541,6 +1546,100 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with registry disabled and' do
|
||||
before do
|
||||
stub_config(registry: { enabled: false })
|
||||
end
|
||||
|
||||
context 'with packages enabled' do
|
||||
before do
|
||||
stub_config(packages: { enabled: true })
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[owner maintainer].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_allowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[developer reporter guest non_member anonymous].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with packages disabled' do
|
||||
before do
|
||||
stub_config(packages: { enabled: false })
|
||||
end
|
||||
|
||||
context 'with admin user' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with registry & packages both disabled' do
|
||||
before do
|
||||
stub_config(registry: { enabled: false })
|
||||
stub_config(packages: { enabled: false })
|
||||
end
|
||||
|
||||
context 'with admin user' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[owner maintainer developer reporter guest non_member anonymous].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_disallowed(:view_package_registry_project_settings) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_feature_flag' do
|
||||
subject { described_class.new(current_user, project) }
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
|
|||
create(:ci_runner_version, version: '14.0.1', status: :not_available)
|
||||
end
|
||||
|
||||
context 'with RunnerUpgradeCheck recommending 14.0.2' do
|
||||
before do
|
||||
stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)
|
||||
|
||||
|
@ -108,6 +109,35 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
|
|||
result = nil
|
||||
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
|
||||
|
||||
expect(result).to eq({
|
||||
status: :success,
|
||||
total_inserted: 0,
|
||||
total_updated: 0,
|
||||
total_deleted: 0
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
|
||||
let(:available_runner_releases) do
|
||||
%w[14.0.0 14.0.1]
|
||||
end
|
||||
|
||||
before do
|
||||
url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
|
||||
|
||||
WebMock.stub_request(:get, url).to_return(
|
||||
body: available_runner_releases.map { |v| { name: v } }.to_json,
|
||||
status: 200,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not modify ci_runner_versions entries', :aggregate_failures do
|
||||
result = nil
|
||||
expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
|
||||
|
||||
expect(result).to eq({
|
||||
status: :success,
|
||||
total_inserted: 0,
|
||||
|
|
|
@ -110,6 +110,7 @@ RSpec.shared_context 'project navbar structure' do
|
|||
_('Access Tokens'),
|
||||
_('Repository'),
|
||||
_('CI/CD'),
|
||||
_('Packages & Registries'),
|
||||
_('Monitor'),
|
||||
s_('UsageQuota|Usage Quotas')
|
||||
]
|
||||
|
|
|
@ -37,4 +37,16 @@ RSpec.describe 'groups/group_members/index', :aggregate_failures do
|
|||
expect(rendered).not_to have_content('You can invite a new member')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @banned is nil' do
|
||||
before do
|
||||
assign(:banned, nil)
|
||||
end
|
||||
|
||||
it 'calls group_members_app_data with { banned: [] }' do
|
||||
expect(view).to receive(:group_members_app_data).with(group, a_hash_including(banned: []))
|
||||
|
||||
render
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -909,8 +909,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
|||
end
|
||||
|
||||
describe 'Packages & Registries' do
|
||||
let(:packages_enabled) { false }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(enabled: registry_enabled)
|
||||
stub_config(packages: { enabled: packages_enabled })
|
||||
end
|
||||
|
||||
context 'when registry is enabled' do
|
||||
|
@ -932,6 +935,17 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
|||
expect(rendered).not_to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when packages config is enabled' do
|
||||
let(:registry_enabled) { false }
|
||||
let(:packages_enabled) { true }
|
||||
|
||||
it 'has a link to the Packages & Registries settings' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Usage Quotas' do
|
||||
|
|
Loading…
Reference in New Issue