Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
55583893ca
commit
08e8c5f723
|
|
@ -14,7 +14,7 @@ variables:
|
|||
NODE_VERSION: "20.12"
|
||||
OS_VERSION: "bookworm"
|
||||
RUBY_VERSION_DEFAULT: "3.2.6"
|
||||
RUBY_VERSION_NEXT: "3.3.6"
|
||||
RUBY_VERSION_NEXT: "3.3.7"
|
||||
RUBYGEMS_VERSION: "3.6"
|
||||
RUST_VERSION: "1.73"
|
||||
UBI_VERSION: "9.5"
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ After your merge request has been approved according to our [approval guidelines
|
|||
|-------------------------------------|------------|-----------------------------------------------------------|
|
||||
| Version affected | X.Y | |
|
||||
| Date introduced on .com | YYYY-MM-DD | #TODO for Engineering - please follow the format |
|
||||
| MR that introduced the bug | | #TODO for Engineering - Link to the MR that introduced the bug|
|
||||
| Date detected | YYYY-MM-DD | #TODO for AppSec - please follow the format |
|
||||
| GitLab EE only | Yes/No | |
|
||||
| Upgrade notes | | |
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const formatPipelinesGraphQLDataToREST = (project) => {
|
|||
stuck: pipeline.stuck,
|
||||
auto_devops: pipeline.configSource === SOURCE_AUTO_DEVOPS,
|
||||
merge_request: true,
|
||||
yaml_errors: pipeline.yamlErrors,
|
||||
yaml_errors: Boolean(pipeline.errorMessages?.nodes?.length),
|
||||
retryable: pipeline.retryable,
|
||||
cancelable: pipeline.cancelable,
|
||||
failure_reason: pipeline.failureReason,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export default {
|
|||
{
|
||||
key: 'name',
|
||||
label: this.$options.i18n.nameLabel,
|
||||
isRowHeader: true,
|
||||
tdClass,
|
||||
thClass,
|
||||
},
|
||||
|
|
@ -260,7 +261,9 @@ export default {
|
|||
data-testid="cluster-agent-list-table"
|
||||
>
|
||||
<template #cell(name)="{ item }">
|
||||
<div class="gl-flex gl-flex-wrap gl-justify-end gl-gap-3 md:gl-justify-start">
|
||||
<div
|
||||
class="gl-flex gl-flex-wrap gl-justify-end gl-gap-3 gl-font-normal md:gl-justify-start"
|
||||
>
|
||||
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link">{{
|
||||
item.name
|
||||
}}</gl-link
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export default {
|
|||
{
|
||||
key: 'configuration',
|
||||
label: this.$options.i18n.configurationLabel,
|
||||
isRowHeader: true,
|
||||
tdClass: `${tdClass} md:gl-w-4/5`,
|
||||
thClass,
|
||||
},
|
||||
|
|
@ -96,7 +97,7 @@ export default {
|
|||
class="!gl-mb-4"
|
||||
>
|
||||
<template #cell(configuration)="{ item }">
|
||||
<gl-link :href="item.webPath">{{ item.path }}</gl-link>
|
||||
<gl-link class="gl-font-normal" :href="item.webPath">{{ item.path }}</gl-link>
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ item }">
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export default {
|
|||
{
|
||||
key: 'name',
|
||||
label: __('Kubernetes cluster'),
|
||||
isRowHeader: true,
|
||||
tdClass,
|
||||
},
|
||||
{
|
||||
|
|
@ -252,7 +253,7 @@ export default {
|
|||
class="gl-flex gl-h-6 gl-w-6 gl-items-center"
|
||||
/>
|
||||
|
||||
<gl-link :href="item.path" class="gl-px-3">
|
||||
<gl-link :href="item.path" class="gl-px-3 gl-font-normal">
|
||||
{{ item.name }}
|
||||
</gl-link>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
GlDisclosureDropdown,
|
||||
GlBadge,
|
||||
GlLink,
|
||||
GlPopover,
|
||||
} from '@gitlab/ui';
|
||||
import { localeDateFormat, newDate } from '~/lib/utils/datetime_utility';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
|
|
@ -15,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
|||
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
REMOVE_TAG_BUTTON_TITLE,
|
||||
DIGEST_LABEL,
|
||||
|
|
@ -47,10 +49,12 @@ export default {
|
|||
TimeAgoTooltip,
|
||||
DetailsRow,
|
||||
SignatureDetailsModal,
|
||||
GlPopover,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
|
|
@ -157,6 +161,22 @@ export default {
|
|||
isDockerOrOciMediaType() {
|
||||
return this.tag.mediaType === DOCKER_MEDIA_TYPE || this.tag.mediaType === OCI_MEDIA_TYPE;
|
||||
},
|
||||
isProtected() {
|
||||
return (
|
||||
(this.tag.protection?.minimumAccessLevelForDelete != null ||
|
||||
this.tag.protection?.minimumAccessLevelForPush != null) &&
|
||||
this.glFeatures.containerRegistryProtectedTags
|
||||
);
|
||||
},
|
||||
tagRowId() {
|
||||
return `${this.tag.name}_badge`;
|
||||
},
|
||||
accessLevelForDelete() {
|
||||
return this.tag.protection?.minimumAccessLevelForDelete;
|
||||
},
|
||||
accessLevelForPush() {
|
||||
return this.tag.protection?.minimumAccessLevelForPush;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -183,6 +203,26 @@ export default {
|
|||
{{ tag.name }}
|
||||
</div>
|
||||
|
||||
<template v-if="isProtected">
|
||||
<gl-badge
|
||||
:id="tagRowId"
|
||||
boundary="viewport"
|
||||
class="gl-ml-4"
|
||||
data-testid="protected-badge"
|
||||
>
|
||||
{{ __('protected') }}
|
||||
</gl-badge>
|
||||
<gl-popover :target="tagRowId" data-testid="protected-popover">
|
||||
<strong>{{ s__('ContainerRegistry|This tag is protected') }}</strong>
|
||||
<br />
|
||||
<br />
|
||||
<strong>{{ s__('ContainerRegistry|Minimum role to push: ') }}</strong>
|
||||
{{ accessLevelForPush }}
|
||||
<strong>{{ s__('ContainerRegistry|Minimum role to delete: ') }}</strong>
|
||||
{{ accessLevelForDelete }}
|
||||
</gl-popover>
|
||||
</template>
|
||||
|
||||
<clipboard-button
|
||||
v-if="tag.location"
|
||||
:title="$options.i18n.COPY_IMAGE_PATH_TITLE"
|
||||
|
|
@ -204,7 +244,11 @@ export default {
|
|||
</template>
|
||||
|
||||
<template v-if="signatures.length" #left-after-toggle>
|
||||
<gl-badge v-gl-tooltip.d0="$options.i18n.SIGNATURE_BADGE_TOOLTIP" class="gl-ml-4">
|
||||
<gl-badge
|
||||
v-gl-tooltip.d0="$options.i18n.SIGNATURE_BADGE_TOOLTIP"
|
||||
class="gl-ml-4"
|
||||
data-testid="signed-badge"
|
||||
>
|
||||
{{ s__('ContainerRegistry|Signed') }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ query getContainerRepositoryTags(
|
|||
userPermissions {
|
||||
destroyContainerRepositoryTag
|
||||
}
|
||||
protection {
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
referrers {
|
||||
artifactType
|
||||
digest
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import {
|
|||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import createProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_container_protection_tag_rule.mutation.graphql';
|
||||
import updateProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_protection_tag_rule.mutation.graphql';
|
||||
import {
|
||||
MinimumAccessLevelOptions,
|
||||
GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
|
||||
|
|
@ -31,13 +32,22 @@ export default {
|
|||
},
|
||||
mixins: [InternalEvents.mixin()],
|
||||
inject: ['projectPath'],
|
||||
props: {
|
||||
rule: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alertErrorMessages: [],
|
||||
protectionRuleFormData: {
|
||||
tagNamePattern: '',
|
||||
minimumAccessLevelForPush: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
minimumAccessLevelForDelete: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
tagNamePattern: this.rule?.tagNamePattern ?? '',
|
||||
minimumAccessLevelForPush:
|
||||
this.rule?.minimumAccessLevelForPush ?? GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
minimumAccessLevelForDelete:
|
||||
this.rule?.minimumAccessLevelForDelete ?? GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
},
|
||||
showValidation: false,
|
||||
updateInProgress: false,
|
||||
|
|
@ -47,9 +57,7 @@ export default {
|
|||
createProtectionRuleMutationInput() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
tagNamePattern: this.protectionRuleFormData.tagNamePattern,
|
||||
minimumAccessLevelForPush: this.protectionRuleFormData.minimumAccessLevelForPush,
|
||||
minimumAccessLevelForDelete: this.protectionRuleFormData.minimumAccessLevelForDelete,
|
||||
...this.protectionRuleFormData,
|
||||
};
|
||||
},
|
||||
isTagNamePatternValid() {
|
||||
|
|
@ -64,9 +72,24 @@ export default {
|
|||
}
|
||||
return s__('ContainerRegistry|This field is required.');
|
||||
},
|
||||
mutation() {
|
||||
return this.rule ? updateProtectionTagRuleMutation : createProtectionTagRuleMutation;
|
||||
},
|
||||
mutationKey() {
|
||||
return this.rule ? 'updateContainerProtectionTagRule' : 'createContainerProtectionTagRule';
|
||||
},
|
||||
tagNamePattern() {
|
||||
return this.protectionRuleFormData.tagNamePattern;
|
||||
},
|
||||
submitButtonText() {
|
||||
return this.rule ? __('Save changes') : s__('ContainerRegistry|Add rule');
|
||||
},
|
||||
updateProtectionTagRuleMutationInput() {
|
||||
return {
|
||||
id: this.rule?.id,
|
||||
...this.protectionRuleFormData,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
|
|
@ -76,16 +99,19 @@ export default {
|
|||
|
||||
this.clearAlertErrorMessages();
|
||||
this.updateInProgress = true;
|
||||
const input = this.rule
|
||||
? this.updateProtectionTagRuleMutationInput
|
||||
: this.createProtectionRuleMutationInput;
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createProtectionTagRuleMutation,
|
||||
mutation: this.mutation,
|
||||
variables: {
|
||||
input: this.createProtectionRuleMutationInput,
|
||||
input,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const errorMessages = data?.createContainerProtectionTagRule?.errors ?? [];
|
||||
const errorMessages = data?.[this.mutationKey]?.errors ?? [];
|
||||
if (errorMessages?.length) {
|
||||
this.alertErrorMessages = Array.isArray(errorMessages)
|
||||
? errorMessages
|
||||
|
|
@ -93,8 +119,12 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.$emit('submit', data.createContainerProtectionTagRule.containerProtectionTagRule);
|
||||
this.trackEvent('container_protection_tag_rule_created');
|
||||
this.$emit('submit', data[this.mutationKey].containerProtectionTagRule);
|
||||
if (this.rule) {
|
||||
this.trackEvent('container_protection_tag_rule_created');
|
||||
} else {
|
||||
this.trackEvent('container_protection_tag_rule_updated');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.handleError(error);
|
||||
|
|
@ -174,10 +204,10 @@ export default {
|
|||
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Minimum role allowed to push')"
|
||||
label-for="input-minimum-access-level-for-push"
|
||||
label-for="select-minimum-access-level-for-push"
|
||||
>
|
||||
<gl-form-select
|
||||
id="input-minimum-access-level-for-push"
|
||||
id="select-minimum-access-level-for-push"
|
||||
v-model="protectionRuleFormData.minimumAccessLevelForPush"
|
||||
:options="$options.minimumAccessLevelOptions"
|
||||
/>
|
||||
|
|
@ -192,12 +222,13 @@ export default {
|
|||
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Minimum role allowed to delete')"
|
||||
label-for="input-minimum-access-level-for-delete"
|
||||
label-for="select-minimum-access-level-for-delete"
|
||||
>
|
||||
<gl-form-select
|
||||
id="input-minimum-access-level-for-delete"
|
||||
id="select-minimum-access-level-for-delete"
|
||||
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
|
||||
:options="$options.minimumAccessLevelOptions"
|
||||
data-testid="select-minimum-access-level-for-delete"
|
||||
/>
|
||||
<template #description>
|
||||
{{
|
||||
|
|
@ -213,9 +244,9 @@ export default {
|
|||
class="js-no-auto-disable"
|
||||
variant="confirm"
|
||||
type="submit"
|
||||
data-testid="add-rule-btn"
|
||||
data-testid="submit-btn"
|
||||
:loading="updateInProgress"
|
||||
>{{ s__('ContainerRegistry|Add rule') }}</gl-button
|
||||
>{{ submitButtonText }}</gl-button
|
||||
>
|
||||
<gl-button type="reset">{{ __('Cancel') }}</gl-button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,12 +77,18 @@ export default {
|
|||
protectionRulesQueryPayload: { nodes: [], pageInfo: {} },
|
||||
protectionRulesQueryPaginationParams: { first: MAX_LIMIT },
|
||||
showDrawer: false,
|
||||
showModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
containsTableItems() {
|
||||
return this.tagProtectionRulesCount > 0;
|
||||
},
|
||||
drawerTitle() {
|
||||
return this.protectionRuleMutationItem
|
||||
? s__('ContainerRegistry|Edit protection rule')
|
||||
: s__('ContainerRegistry|Add protection rule');
|
||||
},
|
||||
isLoadingProtectionRules() {
|
||||
return this.$apollo.queries.protectionRulesQueryPayload.loading;
|
||||
},
|
||||
|
|
@ -92,6 +98,9 @@ export default {
|
|||
rulesLimitReached() {
|
||||
return this.tagProtectionRulesCount === MAX_LIMIT;
|
||||
},
|
||||
mutationItemTagNamePattern() {
|
||||
return this.protectionRuleMutationItem?.tagNamePattern ?? '';
|
||||
},
|
||||
showTableLoading() {
|
||||
return this.protectionRuleMutationInProgress || this.isLoadingProtectionRules;
|
||||
},
|
||||
|
|
@ -99,10 +108,8 @@ export default {
|
|||
return this.protectionRulesQueryResult.map((protectionRule) => {
|
||||
return {
|
||||
id: protectionRule.id,
|
||||
minimumAccessLevelForPush:
|
||||
MinimumAccessLevelText[protectionRule.minimumAccessLevelForPush],
|
||||
minimumAccessLevelForDelete:
|
||||
MinimumAccessLevelText[protectionRule.minimumAccessLevelForDelete],
|
||||
minimumAccessLevelForPush: protectionRule.minimumAccessLevelForPush,
|
||||
minimumAccessLevelForDelete: protectionRule.minimumAccessLevelForDelete,
|
||||
tagNamePattern: protectionRule.tagNamePattern,
|
||||
};
|
||||
});
|
||||
|
|
@ -116,6 +123,11 @@ export default {
|
|||
}
|
||||
return s__('ContainerRegistry|Add protection rule');
|
||||
},
|
||||
toastMessage() {
|
||||
return this.protectionRuleMutationItem
|
||||
? s__('ContainerRegistry|Container protection rule updated.')
|
||||
: s__('ContainerRegistry|Container protection rule created.');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearAlertMessage() {
|
||||
|
|
@ -149,12 +161,20 @@ export default {
|
|||
this.resetProtectionRuleMutation();
|
||||
}
|
||||
},
|
||||
formatAccessLevel(level) {
|
||||
return MinimumAccessLevelText[level];
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$toast.show(s__('ContainerRegistry|Container protection rule created.'));
|
||||
this.$toast.show(this.toastMessage);
|
||||
this.closeDrawer();
|
||||
this.refetchProtectionRules();
|
||||
},
|
||||
openDrawer() {
|
||||
openEditFormDrawer(item) {
|
||||
this.protectionRuleMutationItem = item;
|
||||
this.showDrawer = true;
|
||||
},
|
||||
openNewFormDrawer() {
|
||||
this.protectionRuleMutationItem = null;
|
||||
this.showDrawer = true;
|
||||
},
|
||||
refetchProtectionRules() {
|
||||
|
|
@ -166,6 +186,7 @@ export default {
|
|||
},
|
||||
showProtectionRuleDeletionConfirmModal(protectionRule) {
|
||||
this.protectionRuleMutationItem = protectionRule;
|
||||
this.showModal = true;
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
|
|
@ -193,6 +214,7 @@ export default {
|
|||
],
|
||||
i18n: {
|
||||
deleteIconButton: __('Delete'),
|
||||
editIconButton: __('Edit'),
|
||||
title: s__('ContainerRegistry|Protected container image tags'),
|
||||
protectionRuleDeletionConfirmModal: {
|
||||
title: s__('ContainerRegistry|Delete protection rule'),
|
||||
|
|
@ -221,7 +243,7 @@ export default {
|
|||
:title="$options.i18n.title"
|
||||
:toggle-text="toggleText"
|
||||
data-testid="project-container-protection-tag-rules-settings"
|
||||
@showForm="openDrawer"
|
||||
@showForm="openNewFormDrawer"
|
||||
>
|
||||
<template v-if="containsTableItems" #count>
|
||||
<gl-badge>
|
||||
|
|
@ -284,7 +306,27 @@ export default {
|
|||
<gl-loading-icon size="sm" class="gl-my-5" data-testid="table-loading-icon" />
|
||||
</template>
|
||||
|
||||
<template #cell(minimumAccessLevelForPush)="{ item }">
|
||||
<span data-testid="minimum-access-level-push-value">
|
||||
{{ formatAccessLevel(item.minimumAccessLevelForPush) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell(minimumAccessLevelForDelete)="{ item }">
|
||||
<span data-testid="minimum-access-level-delete-value">
|
||||
{{ formatAccessLevel(item.minimumAccessLevelForDelete) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell(rowActions)="{ item }">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
category="tertiary"
|
||||
icon="pencil"
|
||||
:title="$options.i18n.editIconButton"
|
||||
:aria-label="$options.i18n.editIconButton"
|
||||
@click="openEditFormDrawer(item)"
|
||||
/>
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
v-gl-modal="$options.modal.id"
|
||||
|
|
@ -300,19 +342,23 @@ export default {
|
|||
{{ s__('ContainerRegistry|No container image tags are protected.') }}
|
||||
</p>
|
||||
|
||||
<gl-drawer :z-index="1400" :open="showDrawer" @close="closeDrawer">
|
||||
<gl-drawer :z-index="1039" :open="showDrawer" @close="closeDrawer">
|
||||
<template #title>
|
||||
<h2 class="gl-my-0 gl-text-size-h2 gl-leading-24">
|
||||
{{ s__('ContainerRegistry|Add protection rule') }}
|
||||
{{ drawerTitle }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<container-protection-tag-rule-form @cancel="closeDrawer" @submit="handleSubmit" />
|
||||
<container-protection-tag-rule-form
|
||||
:rule="protectionRuleMutationItem"
|
||||
@cancel="closeDrawer"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
|
||||
<gl-modal
|
||||
v-if="protectionRuleMutationItem"
|
||||
v-model="showModal"
|
||||
:modal-id="$options.modal.id"
|
||||
size="sm"
|
||||
:title="$options.i18n.protectionRuleDeletionConfirmModal.title"
|
||||
|
|
@ -323,7 +369,7 @@ export default {
|
|||
<p>
|
||||
<gl-sprintf :message="$options.i18n.protectionRuleDeletionConfirmModal.description">
|
||||
<template #tagNamePattern>
|
||||
<strong>{{ protectionRuleMutationItem.tagNamePattern }}</strong>
|
||||
<strong>{{ mutationItemTagNamePattern }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
mutation updateContainerProtectionTagRule($input: UpdateContainerProtectionTagRuleInput!) {
|
||||
updateContainerProtectionTagRule(input: $input) {
|
||||
containerProtectionTagRule {
|
||||
id
|
||||
tagNamePattern
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import initSourceCodeDropdowns from '~/vue_shared/components/download_dropdown/i
|
|||
import EmptyProject from '~/pages/projects/show/empty_project';
|
||||
import initHeaderApp from '~/repository/init_header_app';
|
||||
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
||||
import CompactCodeDropdown from '~/repository/components/code_dropdown/compact_code_dropdown.vue';
|
||||
import { initHomePanel } from '../home_panel';
|
||||
|
||||
// Project show page loads different overview content based on user preferences
|
||||
|
|
@ -71,10 +72,15 @@ const initCodeDropdown = () => {
|
|||
|
||||
const { sshUrl, httpUrl, kerberosUrl } = codeDropdownEl.dataset;
|
||||
|
||||
const CodeDropdownComponent =
|
||||
gon.features.directoryCodeDropdownUpdates && gon.features.blobRepositoryVueHeaderApp
|
||||
? CompactCodeDropdown
|
||||
: CodeDropdown;
|
||||
|
||||
return new Vue({
|
||||
el: codeDropdownEl,
|
||||
render(createElement) {
|
||||
return createElement(CodeDropdown, {
|
||||
return createElement(CodeDropdownComponent, {
|
||||
props: {
|
||||
sshUrl,
|
||||
httpUrl,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default {
|
|||
<local-storage-sync v-model="isCollapsed" :storage-key="`wiki:${page.path}:collapsed`" />
|
||||
<span
|
||||
ref="entry"
|
||||
class="wiki-list gl-relative gl-flex gl-cursor-pointer gl-items-center gl-rounded-base gl-px-3"
|
||||
class="wiki-list gl-relative gl-mx-2 gl-mb-px gl-flex gl-min-h-8 gl-cursor-pointer gl-items-center gl-rounded-base gl-px-3"
|
||||
data-testid="wiki-list"
|
||||
:class="{ active: page.path === currentPath }"
|
||||
@click="toggleCollapsed"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
|
||||
import { getHTTPProtocol } from '~/lib/utils/url_utility';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import CodeDropdownItem from '~/vue_shared/components/code_dropdown/code_dropdown_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
CodeDropdownItem,
|
||||
},
|
||||
props: {
|
||||
sshUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
httpUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
kerberosUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
httpLabel() {
|
||||
const protocol = getHTTPProtocol(this.httpUrl)?.toUpperCase();
|
||||
return sprintf(__('Clone with %{protocol}'), { protocol });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-disclosure-dropdown
|
||||
:toggle-text="__('Code')"
|
||||
variant="confirm"
|
||||
placement="bottom-end"
|
||||
class="code-dropdown"
|
||||
fluid-width
|
||||
:auto-close="false"
|
||||
data-testid="code-dropdown"
|
||||
>
|
||||
<gl-disclosure-dropdown-group v-if="sshUrl">
|
||||
<code-dropdown-item
|
||||
:label="__('Clone with SSH')"
|
||||
label-class="!gl-text-sm !gl-pt-2"
|
||||
:link="sshUrl"
|
||||
name="ssh_project_clone"
|
||||
input-id="copy-ssh-url-input"
|
||||
test-id="copy-ssh-url-button"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
|
||||
<gl-disclosure-dropdown-group v-if="httpUrl">
|
||||
<code-dropdown-item
|
||||
:label="httpLabel"
|
||||
label-class="!gl-text-sm !gl-pt-2"
|
||||
:link="httpUrl"
|
||||
name="http_project_clone"
|
||||
input-id="copy-http-url-input"
|
||||
test-id="copy-http-url-button"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
|
||||
<gl-disclosure-dropdown-group v-if="kerberosUrl">
|
||||
<code-dropdown-item
|
||||
:label="__('Clone with KRB5')"
|
||||
label-class="!gl-text-sm !gl-pt-2"
|
||||
:link="kerberosUrl"
|
||||
name="kerberos_project_clone"
|
||||
input-id="copy-http-url-input"
|
||||
test-id="copy-http-url-button"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</gl-disclosure-dropdown>
|
||||
</template>
|
||||
<style>
|
||||
/* Temporary override until we have
|
||||
* widths available in GlDisclosureDropdown
|
||||
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2501
|
||||
*/
|
||||
.code-dropdown .gl-new-dropdown-panel {
|
||||
width: 100%;
|
||||
max-width: 348px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,11 +9,13 @@ import { InternalEvents } from '~/tracking';
|
|||
import { FIND_FILE_BUTTON_CLICK, REF_SELECTOR_CLICK } from '~/tracking/constants';
|
||||
import { visitUrl, joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
|
||||
import BlobControls from '~/repository/components/header_area/blob_controls.vue';
|
||||
import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
|
||||
import CompactCodeDropdown from '~/repository/components/code_dropdown/compact_code_dropdown.vue';
|
||||
import SourceCodeDownloadDropdown from '~/vue_shared/components/download_dropdown/download_dropdown.vue';
|
||||
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
|
@ -31,6 +33,7 @@ export default {
|
|||
Breadcrumbs,
|
||||
BlobControls,
|
||||
CodeDropdown,
|
||||
CompactCodeDropdown,
|
||||
SourceCodeDownloadDropdown,
|
||||
CloneCodeDropdown,
|
||||
WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
|
||||
|
|
@ -40,6 +43,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: [
|
||||
'canCollaborate',
|
||||
'canEditTree',
|
||||
|
|
@ -160,6 +164,9 @@ export default {
|
|||
findFileShortcutKey() {
|
||||
return keysFor(START_SEARCH_PROJECT_FILE)[0];
|
||||
},
|
||||
showCompactCodeDropdown() {
|
||||
return this.glFeatures.directoryCodeDropdownUpdates;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(selectedRef) {
|
||||
|
|
@ -273,27 +280,35 @@ export default {
|
|||
/>
|
||||
<!-- code + mobile panel -->
|
||||
<div v-if="!isReadmeView" class="project-code-holder gl-w-full sm:gl-w-auto">
|
||||
<code-dropdown
|
||||
class="git-clone-holder js-git-clone-holder gl-hidden sm:gl-inline-block"
|
||||
<compact-code-dropdown
|
||||
v-if="showCompactCodeDropdown"
|
||||
:ssh-url="sshUrl"
|
||||
:http-url="httpUrl"
|
||||
:kerberos-url="kerberosUrl"
|
||||
:xcode-url="xcodeUrl"
|
||||
:current-path="currentPath"
|
||||
:directory-download-links="downloadLinks"
|
||||
/>
|
||||
<div class="gl-flex gl-items-stretch gl-gap-3 sm:gl-hidden">
|
||||
<source-code-download-dropdown
|
||||
:download-links="downloadLinks"
|
||||
:download-artifacts="downloadArtifacts"
|
||||
/>
|
||||
<clone-code-dropdown
|
||||
class="mobile-git-clone js-git-clone-holder !gl-w-full"
|
||||
<template v-else>
|
||||
<code-dropdown
|
||||
class="git-clone-holder js-git-clone-holder gl-hidden sm:gl-inline-block"
|
||||
:ssh-url="sshUrl"
|
||||
:http-url="httpUrl"
|
||||
:kerberos-url="kerberosUrl"
|
||||
:xcode-url="xcodeUrl"
|
||||
:current-path="currentPath"
|
||||
:directory-download-links="downloadLinks"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-flex gl-items-stretch gl-gap-3 sm:gl-hidden">
|
||||
<source-code-download-dropdown
|
||||
:download-links="downloadLinks"
|
||||
:download-artifacts="downloadArtifacts"
|
||||
/>
|
||||
<clone-code-dropdown
|
||||
class="mobile-git-clone js-git-clone-holder !gl-w-full"
|
||||
:ssh-url="sshUrl"
|
||||
:http-url="httpUrl"
|
||||
:kerberos-url="kerberosUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import createStore from '~/code_navigation/store';
|
|||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
|
||||
import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
|
||||
import CompactCodeDropdown from '~/repository/components/code_dropdown/compact_code_dropdown.vue';
|
||||
import App from './components/app.vue';
|
||||
import Breadcrumbs from './components/header_area/breadcrumbs.vue';
|
||||
import ForkInfo from './components/fork_info.vue';
|
||||
|
|
@ -187,11 +188,16 @@ export default function setupVueRepositoryList() {
|
|||
const { sshUrl, httpUrl, kerberosUrl, xcodeUrl, directoryDownloadLinks } =
|
||||
codeDropdownEl.dataset;
|
||||
|
||||
const CodeDropdownComponent =
|
||||
gon.features.directoryCodeDropdownUpdates && gon.features.blobRepositoryVueHeaderApp
|
||||
? CompactCodeDropdown
|
||||
: CodeDropdown;
|
||||
|
||||
return new Vue({
|
||||
el: codeDropdownEl,
|
||||
router,
|
||||
render(createElement) {
|
||||
return createElement(CodeDropdown, {
|
||||
return createElement(CodeDropdownComponent, {
|
||||
props: {
|
||||
sshUrl,
|
||||
httpUrl,
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export default {
|
|||
<template>
|
||||
<div class="gl-flex gl-items-start gl-px-2" data-testid="todo-item-container">
|
||||
<div class="gl-mr-3 gl-hidden sm:gl-inline-block">
|
||||
<gl-avatar-link :href="author.webUrl">
|
||||
<gl-avatar-link :href="author.webUrl" aria-hidden="true" tabindex="-1">
|
||||
<gl-avatar :size="24" :src="author.avatarUrl" role="none" />
|
||||
</gl-avatar-link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -322,14 +322,18 @@ export default {
|
|||
<div>
|
||||
<div class="gl-flex gl-flex-col">
|
||||
<gl-loading-icon v-if="isLoading && showSpinnerWhileLoading" size="lg" class="gl-mt-5" />
|
||||
<ul
|
||||
<div
|
||||
v-else
|
||||
data-testid="todo-item-list-container"
|
||||
class="gl-m-0 gl-border-collapse gl-list-none gl-p-0"
|
||||
@mouseenter="startedInteracting"
|
||||
@mouseleave="stoppedInteracting"
|
||||
>
|
||||
<transition-group name="todos">
|
||||
<transition-group
|
||||
name="todos"
|
||||
tag="ol"
|
||||
data-testid="todo-item-list"
|
||||
class="gl-m-0 gl-border-collapse gl-list-none gl-p-0"
|
||||
>
|
||||
<todo-item
|
||||
v-for="todo in todos"
|
||||
:key="todo.id"
|
||||
|
|
@ -338,7 +342,7 @@ export default {
|
|||
@change="handleItemChanged"
|
||||
/>
|
||||
</transition-group>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<todos-empty-state v-if="showEmptyState" :is-filtered="isFiltered" />
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export default {
|
|||
<template v-if="activePanel">
|
||||
<div data-testid="active-panel-template" class="gl-flex gl-items-center gl-py-5">
|
||||
<div class="col-auto">
|
||||
<img :alt="''" :src="activePanel.imageSrc" />
|
||||
<img aria-hidden="true" :src="activePanel.imageSrc" :alt="activePanel.title" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<h1 class="gl-heading-2-fixed gl-my-3">{{ activePanel.title }}</h1>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import {
|
|||
WIDGET_TYPE_DESIGNS,
|
||||
LINKED_ITEMS_ANCHOR,
|
||||
WORK_ITEM_REFERENCE_CHAR,
|
||||
WORK_ITEM_TYPE_VALUE_TASK,
|
||||
WORK_ITEM_TYPE_VALUE_EPIC,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_DEVELOPMENT,
|
||||
|
|
@ -350,15 +349,6 @@ export default {
|
|||
return Boolean(parentWorkItem);
|
||||
},
|
||||
shouldShowAncestors() {
|
||||
// TODO: This is a temporary check till the issue work item migration is completed
|
||||
// Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/468114
|
||||
if (
|
||||
this.workItemType === WORK_ITEM_TYPE_VALUE_TASK &&
|
||||
!this.glFeatures.namespaceLevelWorkItems
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks whether current work item has parent
|
||||
// or it is in hierarchy but there is no permission to view the parent
|
||||
return this.hasParent || this.workItemHierarchy?.hasParent;
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@
|
|||
}
|
||||
|
||||
li > .wiki-list {
|
||||
min-height: 30px;
|
||||
@apply gl-mx-2;
|
||||
margin-bottom: 1px;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply gl-bg-neutral-50 dark:gl-bg-neutral-900;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ module EnforcesAdminAuthentication
|
|||
|
||||
included do
|
||||
before_action :authenticate_admin!
|
||||
|
||||
def self.authorize!(ability, only:)
|
||||
actions = Array(only)
|
||||
|
||||
skip_before_action :authenticate_admin!, only: actions
|
||||
before_action -> { authorize_ability!(ability) }, only: actions
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_admin!
|
||||
|
|
@ -27,4 +34,12 @@ module EnforcesAdminAuthentication
|
|||
def storable_location?
|
||||
request.path != new_admin_session_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_ability!(ability)
|
||||
return authenticate_admin! if current_user.admin?
|
||||
|
||||
render_404 unless current_user.can?(ability)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
|
||||
push_frontend_feature_flag(:namespace_level_work_items, project&.group)
|
||||
push_frontend_feature_flag(:work_items_view_preference, current_user)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
end
|
||||
|
||||
feature_category :source_code_management
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:page_specific_styles, current_user)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
|
||||
if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
|
||||
push_licensed_feature(:security_orchestration_policies)
|
||||
|
|
@ -52,7 +53,6 @@ class ProjectsController < Projects::ApplicationController
|
|||
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
||||
push_force_frontend_feature_flag(:work_items_beta, @project&.work_items_beta_feature_flag_enabled?)
|
||||
push_force_frontend_feature_flag(:work_items_alpha, @project&.work_items_alpha_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:namespace_level_work_items, @project&.group)
|
||||
push_force_frontend_feature_flag(:custom_fields_feature, @project&.group)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Repositories
|
||||
class ProtectedBranchCreatedEvent < ::Gitlab::EventStore::Event
|
||||
PARENT_TYPES = {
|
||||
group: 'group',
|
||||
project: 'project'
|
||||
}.freeze
|
||||
|
||||
def schema
|
||||
{
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'protected_branch_id' => { 'type' => 'integer' },
|
||||
'parent_id' => { 'type' => 'integer' },
|
||||
'parent_type' => { 'type' => 'string', 'enum' => PARENT_TYPES.values }
|
||||
},
|
||||
'required' => %w[protected_branch_id parent_id parent_type]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Repositories
|
||||
class ProtectedBranchDestroyedEvent < ::Gitlab::EventStore::Event
|
||||
PARENT_TYPES = {
|
||||
group: 'group',
|
||||
project: 'project'
|
||||
}.freeze
|
||||
|
||||
def schema
|
||||
{
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'parent_id' => { 'type' => 'integer' },
|
||||
'parent_type' => { 'type' => 'string', 'enum' => PARENT_TYPES.values }
|
||||
},
|
||||
'required' => %w[parent_id parent_type]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,16 @@ module Namespaces
|
|||
module GroupsFilter
|
||||
private
|
||||
|
||||
def by_ids(items)
|
||||
ids = params[:ids]
|
||||
items = items.id_in(ids) if ids
|
||||
items
|
||||
end
|
||||
|
||||
def top_level_only(groups)
|
||||
params[:top_level_only].present? ? groups.by_parent(nil) : groups
|
||||
end
|
||||
|
||||
def by_search(groups)
|
||||
return groups unless params[:search].present?
|
||||
|
||||
|
|
|
|||
|
|
@ -113,12 +113,6 @@ class GroupsFinder < UnionFinder
|
|||
by_search(groups)
|
||||
end
|
||||
|
||||
def by_ids(items)
|
||||
ids = params[:ids]
|
||||
items = items.id_in(ids) if ids
|
||||
items
|
||||
end
|
||||
|
||||
def by_organization(groups)
|
||||
organization = params[:organization]
|
||||
return groups unless organization
|
||||
|
|
@ -136,10 +130,6 @@ class GroupsFinder < UnionFinder
|
|||
end
|
||||
end
|
||||
|
||||
def top_level_only(groups)
|
||||
params[:top_level_only].present? ? groups.by_parent(nil) : groups
|
||||
end
|
||||
|
||||
def by_parent_descendants(groups, parent)
|
||||
if include_parent_shared_groups?
|
||||
groups.descendants_with_shared_with_groups(parent)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ class Board < ApplicationRecord
|
|||
validates :name, presence: true
|
||||
validates :project, presence: true, if: :project_needed?
|
||||
validates :group, presence: true, unless: :project
|
||||
validates :group, absence: {
|
||||
message: ->(_object, _data) { _("can't be specified if a project was already provided") }
|
||||
}, if: :project
|
||||
|
||||
scope :with_associations, -> { preload(:destroyable_lists) }
|
||||
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ module Ci
|
|||
Gitlab::Ci::Variables::Collection.new
|
||||
.append(key: 'CI_RUNNER_ID', value: id.to_s)
|
||||
.append(key: 'CI_RUNNER_DESCRIPTION', value: description)
|
||||
.append(key: 'CI_RUNNER_TAGS', value: tag_list.to_s)
|
||||
.append(key: 'CI_RUNNER_TAGS', value: tag_list.to_a.to_s)
|
||||
end
|
||||
|
||||
def tick_runner_queue
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@
|
|||
"read_admin_monitoring": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"read_admin_subscription": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"read_code": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: directory_code_dropdown_updates
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/450667
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178890
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/514750
|
||||
milestone: '17.9'
|
||||
group: group::source code
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -889,6 +889,8 @@
|
|||
- 1
|
||||
- - security_sync_policy
|
||||
- 1
|
||||
- - security_sync_policy_event
|
||||
- 1
|
||||
- - security_sync_policy_violation_comment
|
||||
- 1
|
||||
- - security_sync_project_policies
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class AddAiSettingsTable < Gitlab::Database::Migration[2.2]
|
|||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
create_table :ai_settings do |t| # rubocop:disable Migration/EnsureFactoryForTable -- FactoryBot does not support Singleton classes https://github.com/thoughtbot/factory_bot/issues/642
|
||||
create_table :ai_settings do |t|
|
||||
t.text :ai_gateway_url, limit: 2048 # Most browsers support URLs up to 2048 characters
|
||||
t.boolean :singleton, null: false, default: true, comment: 'Always true, used for singleton enforcement'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddExternalUrlToComplianceRequirementsControls < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.9'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :compliance_requirements_controls, :external_url, :text, if_not_exists: true
|
||||
end
|
||||
|
||||
add_text_limit :compliance_requirements_controls, :external_url, 1024
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :compliance_requirements_controls, :external_url, if_exists: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTempBoardProjectGroupNullIndex < Gitlab::Database::Migration[2.2]
|
||||
INDEX_NAME = 'tmp_idx_boards_on_project_group_both_missing'
|
||||
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_concurrent_index :boards, :id, name: INDEX_NAME, where: 'group_id IS NULL AND project_id IS NULL'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :boards, :id, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeleteBoardsWithoutProjectAndGroup < Gitlab::Database::Migration[2.2]
|
||||
BATCH_SIZE = 100
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
batch_scope = ->(model) { model.where('group_id IS NULL AND project_id IS NULL') }
|
||||
|
||||
each_batch(:boards, scope: batch_scope, of: BATCH_SIZE) do |batch|
|
||||
batch.delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTempBoardProjectGroupIndex < Gitlab::Database::Migration[2.2]
|
||||
INDEX_NAME = 'tmp_idx_boards_on_project_group_both_present'
|
||||
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_concurrent_index :boards, :id, name: INDEX_NAME, where: 'group_id IS NOT NULL AND project_id IS NOT NULL'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :boards, :id, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixBoardsWithProjectAndGroup < Gitlab::Database::Migration[2.2]
|
||||
BATCH_SIZE = 100
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
batch_scope = ->(model) { model.where('project_id IS NOT NULL AND group_id IS NOT NULL') }
|
||||
|
||||
each_batch(:boards, scope: batch_scope, of: BATCH_SIZE) do |batch|
|
||||
batch.update_all(group_id: nil)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddBoardsUniqueParentConstraint < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_multi_column_not_null_constraint(:boards, :group_id, :project_id)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_multi_column_not_null_constraint(:boards, :group_id, :project_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropBoardsTempParentIndexBothMissing < Gitlab::Database::Migration[2.2]
|
||||
INDEX_NAME = 'tmp_idx_boards_on_project_group_both_missing'
|
||||
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
remove_concurrent_index :boards, :id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :boards, :id, name: INDEX_NAME, where: 'group_id IS NULL AND project_id IS NULL'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropBoardsTempParentIndex < Gitlab::Database::Migration[2.2]
|
||||
INDEX_NAME = 'tmp_idx_boards_on_project_group_both_present'
|
||||
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
remove_concurrent_index :boards, :id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :boards, :id, name: INDEX_NAME, where: 'group_id IS NOT NULL AND project_id IS NOT NULL'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c31bd7c693e5ee898ef1ef7c82776b76b4ecec0f213385506175ee43d10ed0f2
|
||||
|
|
@ -0,0 +1 @@
|
|||
10581a303c2bfb3368e373291feab98bba7407e42272b55696c8f08b84a8fef1
|
||||
|
|
@ -0,0 +1 @@
|
|||
7a24f0d4e789df4cb437a6ceca86d183cce64c81c166b67fd6be24b5215dd15a
|
||||
|
|
@ -0,0 +1 @@
|
|||
06e89359a432c09d0b56bb06b0f3ad88d67f37c4a940cf76c08a4e89393bc353
|
||||
|
|
@ -0,0 +1 @@
|
|||
93cd80fb8913498e97dc4df5e2ddac594881cb70c8d755bfa6d276ef79f8a3b4
|
||||
|
|
@ -0,0 +1 @@
|
|||
7c11d2dc13a5e56e0f3e6e437967fb7871c7adcd5293832076a18fbbc9a8b191
|
||||
|
|
@ -0,0 +1 @@
|
|||
0d190ed83358367421a8eabd32736fa3694340179e5b2565a1c73bd743086a6f
|
||||
|
|
@ -0,0 +1 @@
|
|||
936a8dae4bfec0189636d1cc395beb5152e42ca81963fb45e1c4bc7aa45e9787
|
||||
|
|
@ -9124,7 +9124,8 @@ CREATE TABLE boards (
|
|||
hide_backlog_list boolean DEFAULT false NOT NULL,
|
||||
hide_closed_list boolean DEFAULT false NOT NULL,
|
||||
iteration_id bigint,
|
||||
iteration_cadence_id bigint
|
||||
iteration_cadence_id bigint,
|
||||
CONSTRAINT check_a60857cc50 CHECK ((num_nonnulls(group_id, project_id) = 1))
|
||||
);
|
||||
|
||||
CREATE TABLE boards_epic_board_labels (
|
||||
|
|
@ -11436,7 +11437,9 @@ CREATE TABLE compliance_requirements_controls (
|
|||
expression text,
|
||||
encrypted_secret_token bytea,
|
||||
encrypted_secret_token_iv bytea,
|
||||
CONSTRAINT check_110c87ed8d CHECK ((char_length(expression) <= 255))
|
||||
external_url text,
|
||||
CONSTRAINT check_110c87ed8d CHECK ((char_length(expression) <= 255)),
|
||||
CONSTRAINT check_5020dd6745 CHECK ((char_length(external_url) <= 1024))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE compliance_requirements_controls_id_seq
|
||||
|
|
|
|||
|
|
@ -32538,7 +32538,6 @@ Project-level settings for product analytics provider.
|
|||
| <a id="projectsavedreplies"></a>`savedReplies` | [`ProjectSavedReplyConnection`](#projectsavedreplyconnection) | Saved replies available to the project. (see [Connections](#connections)) |
|
||||
| <a id="projectsecuritydashboardpath"></a>`securityDashboardPath` | [`String`](#string) | Path to project's security dashboard. |
|
||||
| <a id="projectsecuritypolicyproject"></a>`securityPolicyProject` | [`Project`](#project) | Security policy project assigned to the project, absent if assigned to a parent group. |
|
||||
| <a id="projectsecuritypolicyprojectlinkedgroups"></a>`securityPolicyProjectLinkedGroups` | [`GroupConnection`](#groupconnection) | Groups linked to the project, when used as Security Policy Project. (see [Connections](#connections)) |
|
||||
| <a id="projectsecuritypolicyprojectlinkednamespaces"></a>`securityPolicyProjectLinkedNamespaces` **{warning-solid}** | [`NamespaceConnection`](#namespaceconnection) | **Deprecated** in GitLab 17.4. This was renamed. Use: `security_policy_project_linked_groups`. |
|
||||
| <a id="projectsecuritypolicyprojectlinkedprojects"></a>`securityPolicyProjectLinkedProjects` | [`ProjectConnection`](#projectconnection) | Projects linked to the project, when used as Security Policy Project. (see [Connections](#connections)) |
|
||||
| <a id="projectsecurityscanners"></a>`securityScanners` | [`SecurityScanners`](#securityscanners) | Information about security analyzers used in the project. |
|
||||
|
|
@ -34214,6 +34213,24 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="projectsecurityexclusionsscanner"></a>`scanner` | [`ExclusionScannerEnum`](#exclusionscannerenum) | Filter entries by scanner. |
|
||||
| <a id="projectsecurityexclusionstype"></a>`type` | [`ExclusionTypeEnum`](#exclusiontypeenum) | Filter entries by exclusion type. |
|
||||
|
||||
##### `Project.securityPolicyProjectLinkedGroups`
|
||||
|
||||
Groups linked to the project, when used as Security Policy Project.
|
||||
|
||||
Returns [`GroupConnection`](#groupconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, and `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectsecuritypolicyprojectlinkedgroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. |
|
||||
| <a id="projectsecuritypolicyprojectlinkedgroupssearch"></a>`search` | [`String`](#string) | Search query for groups. |
|
||||
| <a id="projectsecuritypolicyprojectlinkedgroupstoplevelonly"></a>`topLevelOnly` | [`Boolean`](#boolean) | Only include top-level groups. |
|
||||
|
||||
##### `Project.securityPolicyProjectSuggestions`
|
||||
|
||||
Security policy project suggestions.
|
||||
|
|
@ -41034,6 +41051,7 @@ Member role admin permission.
|
|||
| <a id="memberroleadminpermissionread_admin_cicd"></a>`READ_ADMIN_CICD` | Read CI/CD details including runners and jobs. |
|
||||
| <a id="memberroleadminpermissionread_admin_dashboard"></a>`READ_ADMIN_DASHBOARD` | Read-only access to admin dashboard. |
|
||||
| <a id="memberroleadminpermissionread_admin_monitoring"></a>`READ_ADMIN_MONITORING` | Allows read access to system monitoring including system info, background migrations, health checks, audit logs, and gitaly in the Admin Area. |
|
||||
| <a id="memberroleadminpermissionread_admin_subscription"></a>`READ_ADMIN_SUBSCRIPTION` | Read subscription details in the Admin area. |
|
||||
|
||||
### `MemberRolePermission`
|
||||
|
||||
|
|
@ -41063,6 +41081,7 @@ Member role permission.
|
|||
| <a id="memberrolepermissionread_admin_cicd"></a>`READ_ADMIN_CICD` | Read CI/CD details including runners and jobs. |
|
||||
| <a id="memberrolepermissionread_admin_dashboard"></a>`READ_ADMIN_DASHBOARD` | Read-only access to admin dashboard. |
|
||||
| <a id="memberrolepermissionread_admin_monitoring"></a>`READ_ADMIN_MONITORING` | Allows read access to system monitoring including system info, background migrations, health checks, audit logs, and gitaly in the Admin Area. |
|
||||
| <a id="memberrolepermissionread_admin_subscription"></a>`READ_ADMIN_SUBSCRIPTION` | Read subscription details in the Admin area. |
|
||||
| <a id="memberrolepermissionread_code"></a>`READ_CODE` | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. |
|
||||
| <a id="memberrolepermissionread_compliance_dashboard"></a>`READ_COMPLIANCE_DASHBOARD` | Read compliance capabilities including adherence, violations, and frameworks for groups and projects. |
|
||||
| <a id="memberrolepermissionread_crm_contact"></a>`READ_CRM_CONTACT` | Read CRM contact. |
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ To do so, either:
|
|||
|
||||
When you upload an image, you can add text to the image and link it to the original graph.
|
||||
|
||||

|
||||

|
||||
|
||||
If you add a link, it is shown above the uploaded image.
|
||||
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ ci_access:
|
|||
- dev
|
||||
```
|
||||
|
||||
For more details, see [Access to Kubernetes from CI](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/b601fa21cac24f0cdedc8b8eb59ebcba0b70f459/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api).
|
||||
For more details, see [Access to Kubernetes from CI/CD](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api).
|
||||
|
||||
## Related topics
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ module Sidebars
|
|||
def render?
|
||||
return false unless context.current_user
|
||||
|
||||
context.current_user.can_admin_all_resources?
|
||||
render_with_abilities.any? { |ability| context.current_user.can?(ability) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_with_abilities
|
||||
%i[admin_all_resources]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15605,6 +15605,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Edit cleanup rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Edit protection rule"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Enable cleanup policy"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15683,6 +15686,12 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Minimum role allowed to push"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum role to delete: "
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum role to push: "
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15898,6 +15907,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|This project's cleanup policy for tags is not enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|This tag is protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|To widen your search, change or remove the filters above."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50971,7 +50983,7 @@ msgstr ""
|
|||
msgid "SecurityApprovals|Learn more about Coverage-Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}Learn more%{linkEnd}."
|
||||
msgid "SecurityApprovals|Requires approval for decreases in test coverage."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|%{featureName} merge request creation mutation failed"
|
||||
|
|
@ -67155,6 +67167,9 @@ msgstr ""
|
|||
msgid "can't be solely blank"
|
||||
msgstr ""
|
||||
|
||||
msgid "can't be specified if a project was already provided"
|
||||
msgstr ""
|
||||
|
||||
msgid "can't be the same as the source project"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -214,8 +214,6 @@ spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
|
|||
spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
|
||||
spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
|
||||
spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
|
||||
spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_repository_rules_spec.js
|
||||
spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
|
||||
spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
|
||||
spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
|
||||
spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe EnforcesAdminAuthentication do
|
||||
include AdminModeHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
|
|
@ -19,6 +17,49 @@ RSpec.describe EnforcesAdminAuthentication do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.authorize!' do
|
||||
controller(ApplicationController) do
|
||||
include EnforcesAdminAuthentication
|
||||
|
||||
authorize! :ability, only: :index
|
||||
|
||||
def index
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is an admin', :enable_admin_mode do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
it 'renders ok' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is a regular user' do
|
||||
it 'renders a 404' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'when an ability grants access' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :ability, :global).and_return(true)
|
||||
end
|
||||
|
||||
it 'renders ok' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'application setting :admin_mode is enabled' do
|
||||
describe 'authenticate_admin!' do
|
||||
context 'as an admin' do
|
||||
|
|
@ -31,11 +72,7 @@ RSpec.describe EnforcesAdminAuthentication do
|
|||
expect(assigns(:current_user_mode)&.admin_mode?).to be(false)
|
||||
end
|
||||
|
||||
context 'when admin mode is active' do
|
||||
before do
|
||||
enable_admin_mode!(user)
|
||||
end
|
||||
|
||||
context 'when admin mode is active', :enable_admin_mode do
|
||||
it 'renders ok' do
|
||||
get :index
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
|
|||
wait_for_requests # ensures page is fully loaded
|
||||
end
|
||||
|
||||
xit 'passes axe automated accessibility testing' do # rubocop:disable RSpec/PendingWithoutReason -- TODO: Fix violations in Vue components
|
||||
it 'passes axe automated accessibility testing' do
|
||||
expect(page).to be_axe_clean.within('#content-body')
|
||||
end
|
||||
end
|
||||
|
|
@ -53,7 +53,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
|
|||
wait_for_requests # ensures page is fully loaded
|
||||
end
|
||||
|
||||
xit 'passes axe automated accessibility testing' do # rubocop:disable RSpec/PendingWithoutReason -- TODO: Fix violations in Vue components
|
||||
it 'passes axe automated accessibility testing' do
|
||||
expect(page).to be_axe_clean.within('#content-body')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ RSpec.describe 'Dashboard > User sorts todos', :js, feature_category: :notificat
|
|||
|
||||
it 'updates sort order and direction' do
|
||||
# Default order is created_at DESC
|
||||
results_list = page.find('ul[data-testid=todo-item-list-container]')
|
||||
results_list = page.find('ol[data-testid="todo-item-list"]')
|
||||
expect(results_list.all('[data-testid=todo-title]')[0]).to have_content('merge_request_1')
|
||||
expect(results_list.all('[data-testid=todo-title]')[1]).to have_content('issue_1')
|
||||
expect(results_list.all('[data-testid=todo-title]')[2]).to have_content('issue_3')
|
||||
|
|
@ -53,7 +53,7 @@ RSpec.describe 'Dashboard > User sorts todos', :js, feature_category: :notificat
|
|||
|
||||
# Switch order to created_at ASC
|
||||
click_on_sort_direction
|
||||
results_list = page.find('ul[data-testid=todo-item-list-container]')
|
||||
results_list = page.find('ol[data-testid="todo-item-list"]')
|
||||
expect(results_list.all('[data-testid=todo-title]')[0]).to have_content('issue_4')
|
||||
expect(results_list.all('[data-testid=todo-title]')[1]).to have_content('issue_2')
|
||||
expect(results_list.all('[data-testid=todo-title]')[2]).to have_content('issue_3')
|
||||
|
|
@ -62,7 +62,7 @@ RSpec.describe 'Dashboard > User sorts todos', :js, feature_category: :notificat
|
|||
|
||||
# Change direction to 'Label priority' ASC
|
||||
click_on_sort_order 'Label priority'
|
||||
results_list = page.find('ul[data-testid=todo-item-list-container]')
|
||||
results_list = page.find('ol[data-testid="todo-item-list"]')
|
||||
expect(results_list.all('[data-testid=todo-title]')[0]).to have_content('issue_3')
|
||||
expect(results_list.all('[data-testid=todo-title]')[1]).to have_content('merge_request_1')
|
||||
expect(results_list.all('[data-testid=todo-title]')[2]).to have_content('issue_1')
|
||||
|
|
@ -72,7 +72,7 @@ RSpec.describe 'Dashboard > User sorts todos', :js, feature_category: :notificat
|
|||
# Change direction to updated_at DESC
|
||||
click_on_sort_order 'Updated'
|
||||
click_on_sort_direction
|
||||
results_list = page.find('ul[data-testid=todo-item-list-container]')
|
||||
results_list = page.find('ol[data-testid="todo-item-list"]')
|
||||
expect(results_list.all('[data-testid=todo-title]')[0]).to have_content('issue_3')
|
||||
expect(results_list.all('[data-testid=todo-title]')[1]).to have_content('merge_request_1')
|
||||
expect(results_list.all('[data-testid=todo-title]')[2]).to have_content('issue_1')
|
||||
|
|
@ -108,7 +108,7 @@ RSpec.describe 'Dashboard > User sorts todos', :js, feature_category: :notificat
|
|||
end
|
||||
|
||||
it "doesn't mix issues and merge requests label priorities" do
|
||||
results_list = page.find('ul[data-testid=todo-item-list-container]')
|
||||
results_list = page.find('ol[data-testid="todo-item-list"]')
|
||||
expect(results_list.all('[data-testid=todo-title]')[0]).to have_content('issue_1')
|
||||
expect(results_list.all('[data-testid=todo-title]')[1]).to have_content('issue_2')
|
||||
expect(results_list.all('[data-testid=todo-title]')[2]).to have_content('merge_request_1')
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
|
||||
visit dashboard_todos_path
|
||||
|
||||
expect(page).to have_selector('ul[data-testid="todo-item-list-container"] li', count: 1)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
end
|
||||
|
||||
it 'shows page with two Todos' do
|
||||
expect(page).to have_selector('ul[data-testid="todo-item-list-container"] li', count: 2)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
end
|
||||
|
||||
it 'has todo present' do
|
||||
expect(page).to have_selector('ul[data-testid="todo-item-list-container"] li', count: 1)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
sign_in(user)
|
||||
visit dashboard_todos_path
|
||||
|
||||
expect(page).to have_selector('ul[data-testid="todo-item-list-container"] li', count: 1)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 1)
|
||||
expect(page).to have_content "#{author.name} has requested access to #{target_type} #{target_name}"
|
||||
end
|
||||
end
|
||||
|
|
@ -499,7 +499,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
find('a.gl-toast-action', text: 'Undo').click
|
||||
end
|
||||
expect(page).to have_content 'Restored 3 to-dos'
|
||||
expect(page).to have_selector('ul[data-testid=todo-item-list-container] li', count: 3)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 3)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -510,7 +510,7 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
|
|||
|
||||
expect(page).to have_content 'Sorry, your filter produced no results'
|
||||
click_on 'Clear'
|
||||
expect(page).to have_selector('ul[data-testid=todo-item-list-container] li', count: 1)
|
||||
expect(page).to have_selector('ol[data-testid="todo-item-list"] > li', count: 1)
|
||||
expect(page).to have_content(other_assigned.author.name)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -251,6 +251,27 @@ RSpec.describe 'Project > Settings > Packages and registries',
|
|||
expect(settings_block).to have_content('Owner')
|
||||
end
|
||||
|
||||
it 'updates a rule' do
|
||||
visit_method
|
||||
|
||||
within_testid settings_block_id do
|
||||
click_button 'Edit'
|
||||
end
|
||||
|
||||
expect(page).to have_selector 'h2', text: 'Edit protection rule'
|
||||
fill_in 'Protect container tags matching', with: 'v1.*'
|
||||
select 'Maintainer', from: 'Minimum role allowed to push'
|
||||
select 'Maintainer', from: 'Minimum role allowed to delete'
|
||||
click_button 'Save changes'
|
||||
|
||||
settings_block = find_by_testid(settings_block_id)
|
||||
expect(page).not_to have_selector 'h2', text: 'Edit protection rule'
|
||||
expect(page).to have_content('Container protection rule updated.')
|
||||
expect(settings_block).to have_content('v1.*')
|
||||
expect(find_by_testid('minimum-access-level-push-value')).to have_content('Maintainer')
|
||||
expect(find_by_testid('minimum-access-level-delete-value')).to have_content('Maintainer')
|
||||
end
|
||||
|
||||
it 'deletes rule' do
|
||||
visit_method
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ RSpec.describe 'Project', feature_category: :source_code_management do
|
|||
let(:path) { project_path(project) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(directory_code_dropdown_updates: false)
|
||||
sign_in(project.first_owner)
|
||||
visit path
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
|
|||
"#{base_path}/mutations/create_container_protection_tag_rule.mutation.graphql"
|
||||
delete_container_protection_tag_rule_mutation_path =
|
||||
"#{base_path}/mutations/delete_container_protection_tag_rule.mutation.graphql"
|
||||
update_container_protection_tag_rule_mutation_path =
|
||||
"#{base_path}/mutations/update_container_protection_tag_rule.mutation.graphql"
|
||||
|
||||
context 'when user does not have access to the project' do
|
||||
it "graphql/#{project_container_protection_tag_rules_query_path}.null_project.json" do
|
||||
|
|
@ -199,6 +201,87 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
|
|||
.to include('Tag name pattern has already been taken')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'updating a rule' do
|
||||
let_it_be(:container_protection_tag_rule) do
|
||||
create(:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
|
||||
minimum_access_level_for_delete: Gitlab::Access::OWNER
|
||||
)
|
||||
end
|
||||
|
||||
context 'when there are no errors' do
|
||||
it "graphql/#{update_container_protection_tag_rule_mutation_path}.json" do
|
||||
mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
|
||||
|
||||
post_graphql(
|
||||
mutation,
|
||||
current_user: owner,
|
||||
variables: {
|
||||
input: {
|
||||
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
|
||||
tagNamePattern: 'v.*',
|
||||
minimumAccessLevelForPush: 'ADMIN',
|
||||
minimumAccessLevelForDelete: 'ADMIN'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are field errors' do
|
||||
it "graphql/#{update_container_protection_tag_rule_mutation_path}.server_errors.json" do
|
||||
mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
|
||||
|
||||
post_graphql(
|
||||
mutation,
|
||||
current_user: owner,
|
||||
variables: {
|
||||
input: {
|
||||
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
|
||||
tagNamePattern: '',
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'OWNER'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect_graphql_errors_to_include(
|
||||
"tagNamePattern can't be blank"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are errors' do
|
||||
before do
|
||||
create(:container_registry_protection_tag_rule, project: project,
|
||||
tag_name_pattern: "v.*")
|
||||
end
|
||||
|
||||
it "graphql/#{update_container_protection_tag_rule_mutation_path}.errors.json" do
|
||||
mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
|
||||
|
||||
post_graphql(
|
||||
mutation,
|
||||
current_user: owner,
|
||||
variables: {
|
||||
input: {
|
||||
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
|
||||
tagNamePattern: 'v.*',
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'OWNER'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(graphql_data_at('updateContainerProtectionTagRule', 'errors'))
|
||||
.to include('Tag name pattern has already been taken')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
GlIcon,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownItem,
|
||||
GlBadge,
|
||||
GlLink,
|
||||
} from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
|
|
@ -28,6 +27,10 @@ import { ListItem } from '../../stubs';
|
|||
describe('tags list row', () => {
|
||||
let wrapper;
|
||||
const tag = tagsMock[0];
|
||||
const protection = {
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'MAINTAINER',
|
||||
};
|
||||
const tagWithOCIMediaType = tagsMock[2];
|
||||
const tagWithListMediaType = tagsMock[3];
|
||||
|
||||
|
|
@ -49,12 +52,14 @@ describe('tags list row', () => {
|
|||
const findWarningIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findAdditionalActionsMenu = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
const findDeleteButton = () => wrapper.findComponent(GlDisclosureDropdownItem);
|
||||
const findSignedBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findSignedBadge = () => wrapper.findByTestId('signed-badge');
|
||||
const findIndexBadge = () => wrapper.findByTestId('index-badge');
|
||||
const findSignatureDetailsModal = () => wrapper.findComponent(SignatureDetailsModal);
|
||||
const getTooltipFor = (component) => getBinding(component.element, 'gl-tooltip');
|
||||
const findProtectedBadge = () => wrapper.findByTestId('protected-badge');
|
||||
const findProtectedPopover = () => wrapper.findByTestId('protected-popover');
|
||||
|
||||
const mountComponent = (propsData = defaultProps) => {
|
||||
const mountComponent = (propsData = defaultProps, protectedTagsFeatureFlagState = false) => {
|
||||
wrapper = shallowMountExtended(TagsListRow, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
|
|
@ -65,6 +70,11 @@ describe('tags list row', () => {
|
|||
},
|
||||
propsData,
|
||||
directives: { GlTooltip: createMockDirective('gl-tooltip') },
|
||||
provide: {
|
||||
glFeatures: {
|
||||
containerRegistryProtectedTags: protectedTagsFeatureFlagState,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -168,6 +178,53 @@ describe('tags list row', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('protected tag', () => {
|
||||
it('hidden if tag.protection does not exists', () => {
|
||||
mountComponent(defaultProps, true);
|
||||
|
||||
expect(findProtectedBadge().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('displays if tag.protection exists', () => {
|
||||
mountComponent(
|
||||
{
|
||||
...defaultProps,
|
||||
tag: {
|
||||
...tag,
|
||||
protection: {
|
||||
...protection,
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(findProtectedBadge().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has the correct text for the popover', () => {
|
||||
mountComponent(
|
||||
{
|
||||
...defaultProps,
|
||||
tag: {
|
||||
...tag,
|
||||
protection: {
|
||||
...protection,
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const popoverText = findProtectedPopover().text();
|
||||
|
||||
expect(popoverText).toContain('This tag is protected');
|
||||
expect(popoverText).toContain('Minimum role to push:');
|
||||
expect(popoverText).toContain('Minimum role to delete:');
|
||||
expect(popoverText).toContain('MAINTAINER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('warning icon', () => {
|
||||
it('is normally hidden', () => {
|
||||
mountComponent();
|
||||
|
|
|
|||
|
|
@ -200,6 +200,10 @@ export const tagsMock = [
|
|||
userPermissions: {
|
||||
destroyContainerRepositoryTag: true,
|
||||
},
|
||||
protection: {
|
||||
minimumAccessLevelForPush: null,
|
||||
minimumAccessLevelForDelete: null,
|
||||
},
|
||||
__typename: 'ContainerRepositoryTag',
|
||||
},
|
||||
{
|
||||
|
|
@ -217,6 +221,10 @@ export const tagsMock = [
|
|||
userPermissions: {
|
||||
destroyContainerRepositoryTag: true,
|
||||
},
|
||||
protection: {
|
||||
minimumAccessLevelForPush: null,
|
||||
minimumAccessLevelForDelete: null,
|
||||
},
|
||||
__typename: 'ContainerRepositoryTag',
|
||||
},
|
||||
{
|
||||
|
|
@ -234,6 +242,10 @@ export const tagsMock = [
|
|||
userPermissions: {
|
||||
destroyContainerRepositoryTag: true,
|
||||
},
|
||||
protection: {
|
||||
minimumAccessLevelForPush: null,
|
||||
minimumAccessLevelForDelete: null,
|
||||
},
|
||||
__typename: 'ContainerRepositoryTag',
|
||||
},
|
||||
{
|
||||
|
|
@ -251,6 +263,10 @@ export const tagsMock = [
|
|||
userPermissions: {
|
||||
destroyContainerRepositoryTag: true,
|
||||
},
|
||||
protection: {
|
||||
minimumAccessLevelForPush: null,
|
||||
minimumAccessLevelForDelete: null,
|
||||
},
|
||||
__typename: 'ContainerRepositoryTag',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlLoadingIcon, GlKeysetPagination, GlModal } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlKeysetPagination, GlModal, GlTable } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -120,7 +120,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTable().exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlTable).exists()).toBe(false);
|
||||
expect(findEmptyText().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -352,7 +352,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
expect(findComboboxInTableRow(0).props('value')).toBe(accessLevelValueMaintainer);
|
||||
expect(findComboboxInTableRow(1).props('value')).toBe(accessLevelValueMaintainer);
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(findComboboxInTableRow(0).props('value')).toBe(accessLevelValueOwner);
|
||||
expect(findComboboxInTableRow(1).props('value')).toBe(accessLevelValueMaintainer);
|
||||
|
|
@ -367,7 +367,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(updateContainerProtectionRepositoryRuleMutationResolver).toHaveBeenCalledTimes(
|
||||
1,
|
||||
|
|
@ -385,7 +385,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(findComboboxInTableRow(0).props('disabled')).toBe(true);
|
||||
expect(findTableRowButtonDelete(0).attributes('disabled')).toBe('disabled');
|
||||
|
|
@ -411,7 +411,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -434,7 +434,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -447,7 +447,7 @@ describe('Container protection repository rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@ import { GlForm } from '@gitlab/ui';
|
|||
import createContainerProtectionTagRuleMutationPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/create_container_protection_tag_rule.mutation.graphql.json';
|
||||
import createContainerProtectionTagRuleMutationErrorPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/create_container_protection_tag_rule.mutation.graphql.errors.json';
|
||||
import createContainerProtectionTagRuleMutationServerErrorPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/create_container_protection_tag_rule.mutation.graphql.server_errors.json';
|
||||
import updateContainerProtectionTagRuleMutationPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/update_container_protection_tag_rule.mutation.graphql.json';
|
||||
import updateContainerProtectionTagRuleMutationErrorPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/update_container_protection_tag_rule.mutation.graphql.errors.json';
|
||||
import updateContainerProtectionTagRuleMutationServerErrorPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/mutations/update_container_protection_tag_rule.mutation.graphql.server_errors.json';
|
||||
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ContainerProtectionTagRuleForm from '~/packages_and_registries/settings/project/components/container_protection_tag_rule_form.vue';
|
||||
import createContainerProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_container_protection_tag_rule.mutation.graphql';
|
||||
import updateContainerProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_protection_tag_rule.mutation.graphql';
|
||||
|
||||
import { createContainerProtectionTagRuleMutationInput } from '../mock_data';
|
||||
import { containerProtectionTagRuleMutationInput } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -24,6 +28,10 @@ describe('container Protection Rule Form', () => {
|
|||
projectPath: 'path',
|
||||
};
|
||||
|
||||
const rule =
|
||||
createContainerProtectionTagRuleMutationPayload.data.createContainerProtectionTagRule
|
||||
.containerProtectionTagRule;
|
||||
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findTagNamePatternInput = () =>
|
||||
wrapper.findByRole('textbox', { name: /protect container tags matching/i });
|
||||
|
|
@ -31,24 +39,36 @@ describe('container Protection Rule Form', () => {
|
|||
wrapper.findByRole('combobox', { name: /minimum role allowed to push/i });
|
||||
const findMinimumAccessLevelForDeleteSelect = () =>
|
||||
wrapper.findByRole('combobox', { name: /minimum role allowed to delete/i });
|
||||
const findSubmitButton = () => wrapper.findByTestId('add-rule-btn');
|
||||
const findCancelButton = () => wrapper.findByRole('button', { name: /cancel/i });
|
||||
const findSubmitButton = () => wrapper.findByTestId('submit-btn');
|
||||
|
||||
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
|
||||
const mountComponent = ({ config, provide = defaultProvidedValues, props } = {}) => {
|
||||
wrapper = mountExtended(ContainerProtectionTagRuleForm, {
|
||||
propsData: props,
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
const mountComponentWithApollo = ({
|
||||
props = {},
|
||||
provide = defaultProvidedValues,
|
||||
mutationResolver = jest.fn().mockResolvedValue(createContainerProtectionTagRuleMutationPayload),
|
||||
createMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionTagRuleMutationPayload),
|
||||
updateMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(updateContainerProtectionTagRuleMutationPayload),
|
||||
} = {}) => {
|
||||
const requestHandlers = [[createContainerProtectionTagRuleMutation, mutationResolver]];
|
||||
const requestHandlers = [
|
||||
[createContainerProtectionTagRuleMutation, createMutationResolver],
|
||||
[updateContainerProtectionTagRuleMutation, updateMutationResolver],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
|
||||
mountComponent({
|
||||
props,
|
||||
provide,
|
||||
config: {
|
||||
apolloProvider: fakeApollo,
|
||||
|
|
@ -65,18 +85,18 @@ describe('container Protection Rule Form', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
tagNamePattern | errorMessage
|
||||
${''} | ${'This field is required.'}
|
||||
${' '} | ${'This field is required.'}
|
||||
${createContainerProtectionTagRuleMutationInput.tagNamePattern.repeat(100)} | ${'Must be less than 100 characters.'}
|
||||
tagNamePattern | errorMessage
|
||||
${''} | ${'This field is required.'}
|
||||
${' '} | ${'This field is required.'}
|
||||
${containerProtectionTagRuleMutationInput.tagNamePattern.repeat(100)} | ${'Must be less than 100 characters.'}
|
||||
`('when tagNamePattern is "$tagNamePattern"', ({ tagNamePattern, errorMessage }) => {
|
||||
const mutationResolver = jest
|
||||
const createMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionTagRuleMutationPayload);
|
||||
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
mutationResolver,
|
||||
createMutationResolver,
|
||||
});
|
||||
|
||||
await findTagNamePatternInput().setValue(tagNamePattern);
|
||||
|
|
@ -89,7 +109,7 @@ describe('container Protection Rule Form', () => {
|
|||
it('when submitted does not make graphql request', async () => {
|
||||
await findForm().trigger('submit');
|
||||
|
||||
expect(mutationResolver).not.toHaveBeenCalled();
|
||||
expect(createMutationResolver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -133,7 +153,7 @@ describe('container Protection Rule Form', () => {
|
|||
mountComponentWithApollo();
|
||||
|
||||
await findTagNamePatternInput().setValue(
|
||||
createContainerProtectionTagRuleMutationInput.tagNamePattern,
|
||||
containerProtectionTagRuleMutationInput.tagNamePattern,
|
||||
);
|
||||
findForm().trigger('submit');
|
||||
});
|
||||
|
|
@ -144,27 +164,51 @@ describe('container Protection Rule Form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | props | submitButtonText
|
||||
${'when form has no prop "rule"'} | ${{}} | ${'Add rule'}
|
||||
${'when form has prop "rule"'} | ${{ rule }} | ${'Save changes'}
|
||||
`('$description', ({ props, submitButtonText }) => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
props,
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit button', () => {
|
||||
it(`renders text: ${submitButtonText}`, () => {
|
||||
expect(findSubmitButton().text()).toBe(submitButtonText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel button', () => {
|
||||
it('renders with text: "Cancel"', () => {
|
||||
expect(findCancelButton().text()).toBe('Cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('form events', () => {
|
||||
describe('reset', () => {
|
||||
const mutationResolver = jest
|
||||
const createMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionTagRuleMutationPayload);
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponentWithApollo({ mutationResolver });
|
||||
mountComponentWithApollo({ createMutationResolver });
|
||||
|
||||
findForm().trigger('reset');
|
||||
});
|
||||
|
||||
it('emits custom event "cancel"', () => {
|
||||
expect(mutationResolver).not.toHaveBeenCalled();
|
||||
expect(createMutationResolver).not.toHaveBeenCalled();
|
||||
|
||||
expect(wrapper.emitted('cancel')).toBeDefined();
|
||||
expect(wrapper.emitted('cancel')[0]).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not dispatch apollo mutation request', () => {
|
||||
expect(mutationResolver).not.toHaveBeenCalled();
|
||||
expect(createMutationResolver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not emit custom event "submit"', () => {
|
||||
|
|
@ -172,29 +216,31 @@ describe('container Protection Rule Form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
describe('submit a new rule', () => {
|
||||
const findAlert = () => extendedWrapper(wrapper.findByRole('alert'));
|
||||
|
||||
const submitForm = () => {
|
||||
findTagNamePatternInput().setValue(
|
||||
createContainerProtectionTagRuleMutationInput.tagNamePattern,
|
||||
);
|
||||
findTagNamePatternInput().setValue(containerProtectionTagRuleMutationInput.tagNamePattern);
|
||||
findForm().trigger('submit');
|
||||
return waitForPromises();
|
||||
};
|
||||
|
||||
it('dispatches correct apollo mutation', async () => {
|
||||
const mutationResolver = jest
|
||||
const createMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionTagRuleMutationPayload);
|
||||
const updateMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(updateContainerProtectionTagRuleMutationPayload);
|
||||
|
||||
mountComponentWithApollo({ mutationResolver });
|
||||
mountComponentWithApollo({ createMutationResolver, updateMutationResolver });
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(mutationResolver).toHaveBeenCalledWith({
|
||||
input: { projectPath: 'path', ...createContainerProtectionTagRuleMutationInput },
|
||||
expect(createMutationResolver).toHaveBeenCalledWith({
|
||||
input: { projectPath: 'path', ...containerProtectionTagRuleMutationInput },
|
||||
});
|
||||
expect(updateMutationResolver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits event "submit" when apollo mutation successful', async () => {
|
||||
|
|
@ -212,16 +258,89 @@ describe('container Protection Rule Form', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
description | mutationResolver | expectedErrorMessage
|
||||
description | createMutationResolver | expectedErrorMessage
|
||||
${'responds with field errors'} | ${jest.fn().mockResolvedValue(createContainerProtectionTagRuleMutationErrorPayload)} | ${'Tag name pattern has already been taken'}
|
||||
${'responds with server errors'} | ${jest.fn().mockResolvedValue(createContainerProtectionTagRuleMutationServerErrorPayload)} | ${"tagNamePattern can't be blank"}
|
||||
${'fails with network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL error'))} | ${'Something went wrong while saving the protection rule.'}
|
||||
`(
|
||||
'when apollo mutation request $description',
|
||||
({ mutationResolver, expectedErrorMessage }) => {
|
||||
({ createMutationResolver, expectedErrorMessage }) => {
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
mutationResolver,
|
||||
createMutationResolver,
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('shows error alert with correct message', () => {
|
||||
expect(findAlert().text()).toBe(expectedErrorMessage);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('updating existing rule', () => {
|
||||
const findAlert = () => extendedWrapper(wrapper.findByRole('alert'));
|
||||
const findDeleteCombobox = () => extendedWrapper(findMinimumAccessLevelForDeleteSelect());
|
||||
|
||||
const submitForm = async () => {
|
||||
findTagNamePatternInput().setValue(containerProtectionTagRuleMutationInput.tagNamePattern);
|
||||
await findDeleteCombobox().findAll('option').at(0).setSelected();
|
||||
findForm().trigger('submit');
|
||||
return waitForPromises();
|
||||
};
|
||||
|
||||
it('dispatches correct apollo mutation', async () => {
|
||||
const createMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionTagRuleMutationPayload);
|
||||
const updateMutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(updateContainerProtectionTagRuleMutationPayload);
|
||||
|
||||
mountComponentWithApollo({
|
||||
createMutationResolver,
|
||||
updateMutationResolver,
|
||||
props: { rule },
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(createMutationResolver).not.toHaveBeenCalled();
|
||||
expect(updateMutationResolver).toHaveBeenCalledWith({
|
||||
input: { id: rule.id, ...containerProtectionTagRuleMutationInput },
|
||||
});
|
||||
});
|
||||
|
||||
it('emits event "submit" when apollo mutation successful', async () => {
|
||||
mountComponentWithApollo({
|
||||
props: { rule },
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(wrapper.emitted('submit')).toBeDefined();
|
||||
const expectedEventSubmitPayload =
|
||||
updateContainerProtectionTagRuleMutationPayload.data.updateContainerProtectionTagRule
|
||||
.containerProtectionTagRule;
|
||||
expect(wrapper.emitted('submit')[0]).toEqual([expectedEventSubmitPayload]);
|
||||
|
||||
expect(wrapper.emitted()).not.toHaveProperty('cancel');
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | updateMutationResolver | expectedErrorMessage
|
||||
${'responds with field errors'} | ${jest.fn().mockResolvedValue(updateContainerProtectionTagRuleMutationErrorPayload)} | ${'Tag name pattern has already been taken'}
|
||||
${'responds with server errors'} | ${jest.fn().mockResolvedValue(updateContainerProtectionTagRuleMutationServerErrorPayload)} | ${"tagNamePattern can't be blank"}
|
||||
${'fails with network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL error'))} | ${'Something went wrong while saving the protection rule.'}
|
||||
`(
|
||||
'when apollo mutation request $description',
|
||||
({ updateMutationResolver, expectedErrorMessage }) => {
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
props: { rule },
|
||||
updateMutationResolver,
|
||||
});
|
||||
|
||||
await submitForm();
|
||||
|
|
|
|||
|
|
@ -137,64 +137,6 @@ describe('ContainerProtectionTagRules', () => {
|
|||
expect.objectContaining({ projectPath: defaultProvidedValues.projectPath, first: 5 }),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when `Add protection rule` button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
await findCrudComponent().vm.$emit('showForm');
|
||||
});
|
||||
|
||||
it('opens drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(true);
|
||||
expect(findDrawerTitle().text()).toBe('Add protection rule');
|
||||
});
|
||||
|
||||
it('renders form', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when drawer emits `close` event', () => {
|
||||
beforeEach(async () => {
|
||||
await findDrawer().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('closes drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form emits `cancel` event', () => {
|
||||
beforeEach(async () => {
|
||||
await findForm().vm.$emit('cancel');
|
||||
});
|
||||
|
||||
it('closes drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form emits `submit` event', () => {
|
||||
it('refetches protection rules after successful graphql mutation', async () => {
|
||||
const containerProtectionTagRuleQueryResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(containerProtectionTagRuleQueryPayload);
|
||||
|
||||
createComponent({
|
||||
containerProtectionTagRuleQueryResolver,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(containerProtectionTagRuleQueryResolver).toHaveBeenCalledTimes(1);
|
||||
|
||||
await findCrudComponent().vm.$emit('showForm');
|
||||
await findForm().vm.$emit('submit');
|
||||
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
expect(containerProtectionTagRuleQueryResolver).toHaveBeenCalledTimes(2);
|
||||
expect($toast.show).toHaveBeenCalledWith('Container protection rule created.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded & contains tag protection rules', () => {
|
||||
|
|
@ -203,6 +145,7 @@ describe('ContainerProtectionTagRules', () => {
|
|||
const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1));
|
||||
const findTableRow = (i) => extendedWrapper(findTableBody().findAllByRole('row').at(i));
|
||||
const findTableRowCell = (i, j) => extendedWrapper(findTableRow(i).findAllByRole('cell').at(j));
|
||||
const findTableRowButtonEdit = (i) => findTableRow(i).findByRole('button', { name: /edit/i });
|
||||
const findTableRowButtonDelete = (i) =>
|
||||
findTableRow(i).findByRole('button', { name: /delete/i });
|
||||
|
||||
|
|
@ -281,15 +224,21 @@ describe('ContainerProtectionTagRules', () => {
|
|||
});
|
||||
|
||||
describe('column "rowActions"', () => {
|
||||
describe('button "Delete"', () => {
|
||||
describe.each`
|
||||
buttonName | buttonFinder
|
||||
${'Edit'} | ${findTableRowButtonEdit}
|
||||
${'Delete'} | ${findTableRowButtonDelete}
|
||||
`('button "$buttonName"', ({ buttonFinder }) => {
|
||||
it('exists in table', () => {
|
||||
expect(findTableRowButtonDelete(0).exists()).toBe(true);
|
||||
expect(buttonFinder(0).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when button is clicked', () => {
|
||||
it('renders the "delete container protection rule" confirmation modal', async () => {
|
||||
await findTableRowButtonDelete(0).trigger('click');
|
||||
beforeEach(async () => {
|
||||
await buttonFinder(0).trigger('click');
|
||||
});
|
||||
|
||||
it('renders the "delete container protection rule" confirmation modal', () => {
|
||||
const modalId = getBinding(findTableRowButtonDelete(0).element, 'gl-modal');
|
||||
|
||||
expect(findModal().props('modal-id')).toBe(modalId);
|
||||
|
|
@ -426,6 +375,77 @@ describe('ContainerProtectionTagRules', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | beforeFn | title | toastMessage
|
||||
${'when `Add protection rule` button is clicked'} | ${() => findCrudComponent().vm.$emit('showForm')} | ${'Add protection rule'} | ${'Container protection rule created.'}
|
||||
${'when `Edit` button for a rule is clicked'} | ${() => findTableRowButtonEdit(0).trigger('click')} | ${'Edit protection rule'} | ${'Container protection rule updated.'}
|
||||
`('$description', ({ beforeFn, title, toastMessage }) => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
await beforeFn();
|
||||
});
|
||||
|
||||
it('opens drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(true);
|
||||
});
|
||||
|
||||
it(`sets the appropriate drawer title: ${title}`, () => {
|
||||
expect(findDrawerTitle().text()).toBe(title);
|
||||
});
|
||||
|
||||
it('renders form', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when drawer emits `close` event', () => {
|
||||
beforeEach(async () => {
|
||||
await findDrawer().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('closes drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form emits `cancel` event', () => {
|
||||
beforeEach(async () => {
|
||||
await findForm().vm.$emit('cancel');
|
||||
});
|
||||
|
||||
it('closes drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form emits `submit` event', () => {
|
||||
it('refetches protection rules after successful graphql mutation', async () => {
|
||||
const containerProtectionTagRuleQueryResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(containerProtectionTagRuleQueryPayload);
|
||||
|
||||
createComponent({
|
||||
containerProtectionTagRuleQueryResolver,
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(containerProtectionTagRuleQueryResolver).toHaveBeenCalledTimes(1);
|
||||
|
||||
await beforeFn();
|
||||
await findForm().vm.$emit('submit');
|
||||
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
expect(containerProtectionTagRuleQueryResolver).toHaveBeenCalledTimes(2);
|
||||
expect($toast.show).toHaveBeenCalledWith(toastMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded & contains maximum number of tag protection rules', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlLoadingIcon, GlKeysetPagination, GlModal } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlKeysetPagination, GlModal, GlTable } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -114,7 +114,7 @@ describe('Packages protection rules project settings', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTable().exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlTable).exists()).toBe(false);
|
||||
expect(findEmptyText().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -328,11 +328,11 @@ describe('Packages protection rules project settings', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(findComboboxInTableRow(0).props('value')).toBe(accessLevelValueMaintainer);
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
expect(findComboboxInTableRow(0).props('value')).toBe(accessLevelValueOwner);
|
||||
|
||||
expect(findComboboxInTableRow(1).props('value')).toBe(accessLevelValueMaintainer);
|
||||
await findComboboxInTableRow(1).setValue(accessLevelValueAdmin);
|
||||
await findComboboxInTableRow(1).findAll('option').at(2).setSelected();
|
||||
expect(findComboboxInTableRow(1).props('value')).toBe(accessLevelValueAdmin);
|
||||
|
||||
expect(findComboboxInTableRow(0).props('value')).toBe(accessLevelValueOwner);
|
||||
|
|
@ -347,7 +347,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(updatePackagesProtectionRuleMutationResolver).toHaveBeenCalledTimes(1);
|
||||
expect(updatePackagesProtectionRuleMutationResolver).toHaveBeenCalledWith({
|
||||
|
|
@ -363,7 +363,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(findComboboxInTableRow(0).props('disabled')).toBe(true);
|
||||
expect(findComboboxInTableRow(1).props('disabled')).toBe(false);
|
||||
|
|
@ -379,7 +379,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
expect(findComboboxInTableRow(0).props('disabled')).toBe(true);
|
||||
expect(findTableRowButtonDelete(0).props('disabled')).toBe(true);
|
||||
|
|
@ -399,7 +399,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -420,7 +420,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ describe('Packages protection rules project settings', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findComboboxInTableRow(0).setValue(accessLevelValueOwner);
|
||||
await findComboboxInTableRow(0).findAll('option').at(1).setSelected();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
|
|||
|
|
@ -266,8 +266,8 @@ export const updateContainerProtectionRepositoryRuleMutationPayload = ({
|
|||
},
|
||||
});
|
||||
|
||||
export const createContainerProtectionTagRuleMutationInput = {
|
||||
tagNamePattern: `v.*`,
|
||||
export const containerProtectionTagRuleMutationInput = {
|
||||
tagNamePattern: 'v.+',
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'MAINTAINER',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import CompactCodeDropdown from '~/repository/components/code_dropdown/compact_code_dropdown.vue';
|
||||
import CodeDropdownItem from '~/vue_shared/components/code_dropdown/code_dropdown_item.vue';
|
||||
|
||||
describe('Compact Code Dropdown coomponent', () => {
|
||||
let wrapper;
|
||||
const sshUrl = 'ssh://foo.bar';
|
||||
const httpUrl = 'http://foo.bar';
|
||||
const httpsUrl = 'https://foo.bar';
|
||||
const defaultPropsData = {
|
||||
sshUrl,
|
||||
httpUrl,
|
||||
};
|
||||
|
||||
const findCodeDropdownItems = () => wrapper.findAllComponents(CodeDropdownItem);
|
||||
const findCodeDropdownItemAtIndex = (index) => findCodeDropdownItems().at(index);
|
||||
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
|
||||
const createComponent = (propsData = defaultPropsData) => {
|
||||
wrapper = shallowMount(CompactCodeDropdown, {
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
describe('copyGroup', () => {
|
||||
describe('rendering', () => {
|
||||
it.each`
|
||||
name | index | link
|
||||
${'SSH'} | ${0} | ${sshUrl}
|
||||
${'HTTP'} | ${1} | ${httpUrl}
|
||||
`('renders correct link and a copy-button for $name', ({ index, link }) => {
|
||||
createComponent();
|
||||
|
||||
const item = findCodeDropdownItemAtIndex(index);
|
||||
expect(item.props('link')).toBe(link);
|
||||
});
|
||||
|
||||
it.each`
|
||||
name | value
|
||||
${'sshUrl'} | ${sshUrl}
|
||||
${'httpUrl'} | ${httpUrl}
|
||||
`('does not fail if only $name is set', ({ name, value }) => {
|
||||
createComponent({ [name]: value });
|
||||
|
||||
expect(findCodeDropdownItemAtIndex(0).props('link')).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionality', () => {
|
||||
it.each`
|
||||
name | value
|
||||
${'sshUrl'} | ${null}
|
||||
${'httpUrl'} | ${null}
|
||||
`('allows null values for the props', ({ name, value }) => {
|
||||
createComponent({ ...defaultPropsData, [name]: value });
|
||||
|
||||
expect(findCodeDropdownItems().length).toBe(1);
|
||||
});
|
||||
|
||||
it('correctly calculates httpLabel for HTTPS protocol', () => {
|
||||
createComponent({ httpUrl: httpsUrl });
|
||||
|
||||
expect(findCodeDropdownItemAtIndex(0).attributes('label')).toContain('HTTPS');
|
||||
});
|
||||
|
||||
it.each`
|
||||
name | index | link
|
||||
${'SSH'} | ${0} | ${sshUrl}
|
||||
${'HTTP'} | ${1} | ${httpUrl}
|
||||
`('does not close dropdown on $name item click', () => {
|
||||
createComponent();
|
||||
expect(findDropdown().props('autoClose')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,12 +4,12 @@ import {
|
|||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
|
||||
import CodeDropdownItem from '~/vue_shared/components/code_dropdown/code_dropdown_item.vue';
|
||||
|
||||
describe('Clone Dropdown Button', () => {
|
||||
describe('Code Dropdown component', () => {
|
||||
let wrapper;
|
||||
const sshUrl = 'ssh://foo.bar';
|
||||
const httpUrl = 'http://foo.bar';
|
||||
|
|
@ -40,7 +40,7 @@ describe('Clone Dropdown Button', () => {
|
|||
const closeDropdown = jest.fn();
|
||||
|
||||
const createComponent = (propsData = defaultPropsData) => {
|
||||
wrapper = shallowMount(CodeDropdown, {
|
||||
wrapper = shallowMountExtended(CodeDropdown, {
|
||||
propsData,
|
||||
stubs: {
|
||||
GlFormInputGroup,
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ describe('WorkItemDetail component', () => {
|
|||
mutationHandler,
|
||||
error = undefined,
|
||||
workItemsAlphaEnabled = false,
|
||||
namespaceLevelWorkItems = true,
|
||||
hasSubepicsFeature = true,
|
||||
router = true,
|
||||
modalIsGroup = null,
|
||||
|
|
@ -184,7 +183,6 @@ describe('WorkItemDetail component', () => {
|
|||
provide: {
|
||||
glFeatures: {
|
||||
workItemsAlpha: workItemsAlphaEnabled,
|
||||
namespaceLevelWorkItems,
|
||||
},
|
||||
hasSubepicsFeature,
|
||||
fullPath: 'group/project',
|
||||
|
|
@ -429,17 +427,6 @@ describe('WorkItemDetail component', () => {
|
|||
expect(findWorkItemType().classes()).toEqual(['sm:!gl-block', 'gl-w-full']);
|
||||
});
|
||||
|
||||
describe('`namespace_level_work_items` is disabled', () => {
|
||||
it('does not show ancestors widget and shows title in the header', async () => {
|
||||
createComponent({ namespaceLevelWorkItems: false });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAncestors().exists()).toBe(false);
|
||||
expect(findWorkItemType().classes()).toEqual(['sm:!gl-block', 'gl-w-full']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`subepics` is unavailable', () => {
|
||||
it('does not show ancestors widget and shows title in the header', async () => {
|
||||
const epicWorkItem = workItemByIidResponseFactory({
|
||||
|
|
|
|||
|
|
@ -686,9 +686,9 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
|
|||
end
|
||||
|
||||
describe 'admin user' do
|
||||
it 'returns Admin Panel for admin nav', :aggregate_failures do
|
||||
allow(user).to receive(:can_admin_all_resources?).and_return(true)
|
||||
let(:user) { build(:admin) }
|
||||
|
||||
it 'returns Admin Panel for admin nav', :enable_admin_mode do
|
||||
expect(helper.super_sidebar_nav_panel(nav: 'admin', user: user)).to be_a(Sidebars::Admin::Panel)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::FixUsernamespaceAuditEvents, feature_category: :audit_events do
|
||||
let(:audit_events_table) { table(:audit_events) }
|
||||
let(:instance_audit_events_table) { table(:instance_audit_events) }
|
||||
let(:audit_events_table) { partitioned_table(:audit_events) }
|
||||
let(:instance_audit_events_table) { partitioned_table(:instance_audit_events) }
|
||||
|
||||
let!(:usernamespace_audit_event) do
|
||||
audit_events_table.create!(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
|
|||
*tables_with_alternative_not_null_constraint,
|
||||
'analytics_devops_adoption_segments.namespace_id',
|
||||
*['badges.project_id', 'badges.group_id'],
|
||||
*['boards.project_id', 'boards.group_id'],
|
||||
'ci_pipeline_schedules.project_id',
|
||||
'ci_sources_pipelines.project_id',
|
||||
'ci_triggers.project_id',
|
||||
|
|
|
|||
|
|
@ -912,6 +912,7 @@ project:
|
|||
- security_statistics
|
||||
- vulnerability_management_policies
|
||||
- project_control_compliance_statuses
|
||||
- approval_policies
|
||||
award_emoji:
|
||||
- awardable
|
||||
- user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe DeleteBoardsWithoutProjectAndGroup, migration: :gitlab_main, feature_category: :team_planning do
|
||||
let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
|
||||
let(:namespace) { table(:namespaces).create!(name: "namespace", path: "namespace", organization_id: organization.id) }
|
||||
let(:project) do
|
||||
table(:projects).create!(
|
||||
namespace_id: namespace.id,
|
||||
project_namespace_id: namespace.id,
|
||||
organization_id: organization.id
|
||||
)
|
||||
end
|
||||
|
||||
let(:boards) { table(:boards) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
|
||||
boards.create!
|
||||
boards.create!
|
||||
boards.create!
|
||||
boards.create!
|
||||
boards.create!(group_id: namespace.id)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'updates records in batches' do
|
||||
expect do
|
||||
migrate!
|
||||
end.to make_queries_matching(
|
||||
/DELETE FROM "boards".+WHERE \(group_id IS NULL AND project_id IS NULL\)/,
|
||||
2
|
||||
)
|
||||
end
|
||||
|
||||
it 'deletes offending records records' do
|
||||
expect { migrate! }.to change { boards.count }.from(5).to(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixBoardsWithProjectAndGroup, migration: :gitlab_main, feature_category: :team_planning do
|
||||
let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
|
||||
let(:namespace) { table(:namespaces).create!(name: "namespace", path: "namespace", organization_id: organization.id) }
|
||||
let(:project) do
|
||||
table(:projects).create!(
|
||||
namespace_id: namespace.id,
|
||||
project_namespace_id: namespace.id,
|
||||
organization_id: organization.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:board1) { table(:boards).create!(group_id: namespace.id, project_id: project.id) }
|
||||
let!(:board2) { table(:boards).create!(group_id: namespace.id, project_id: project.id) }
|
||||
let!(:board3) { table(:boards).create!(group_id: namespace.id, project_id: project.id) }
|
||||
let!(:board4) { table(:boards).create!(group_id: namespace.id, project_id: project.id) }
|
||||
let!(:board5) { table(:boards).create!(group_id: namespace.id) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'updates records in batches' do
|
||||
expect do
|
||||
migrate!
|
||||
end.to make_queries_matching(/UPDATE\s+"boards"/, 2)
|
||||
end
|
||||
|
||||
it 'removes group_id from offending records' do
|
||||
expect { migrate! }.to change {
|
||||
[board1, board2, board3, board4].each(&:reload).pluck(:project_id, :group_id)
|
||||
}.from(
|
||||
Array.new(4) { [project.id, namespace.id] }
|
||||
).to(
|
||||
[
|
||||
[project.id, nil],
|
||||
[project.id, nil],
|
||||
[project.id, nil],
|
||||
[project.id, nil]
|
||||
]
|
||||
).and(
|
||||
not_change { board5.reload.group_id }.from(namespace.id)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -20,6 +20,21 @@ RSpec.describe Board do
|
|||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
|
||||
describe 'group and project mutually exclusive' do
|
||||
context 'when project is present' do
|
||||
subject { described_class.new(project: project) }
|
||||
|
||||
it do
|
||||
is_expected.to validate_absence_of(:group)
|
||||
.with_message(_("can't be specified if a project was already provided"))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not present' do
|
||||
it { is_expected.not_to validate_absence_of(:group) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'constants' do
|
||||
|
|
|
|||
|
|
@ -3179,6 +3179,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
|
|||
|
||||
context 'when runner is assigned to build' do
|
||||
let(:runner) { create(:ci_runner, description: 'description', tag_list: %w[docker linux]) }
|
||||
let(:expected_tags_value) { %w[docker linux].to_s }
|
||||
|
||||
before do
|
||||
build.update!(runner: runner)
|
||||
|
|
@ -3186,7 +3187,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
|
|||
|
||||
it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true, masked: false }) }
|
||||
it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true, masked: false }) }
|
||||
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true, masked: false }) }
|
||||
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: expected_tags_value, public: true, masked: false }) }
|
||||
|
||||
context 'when the tags are preloaded' do
|
||||
subject { described_class.preload(:tags).find(build.id).variables }
|
||||
|
||||
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: expected_tags_value, public: true, masked: false }) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is for a deployment' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RSpec.shared_examples 'Admin menu' do |link:, title:, icon:, separated: false|
|
|||
let_it_be(:user) { build(:user, :admin) }
|
||||
|
||||
before do
|
||||
allow(user).to receive(:can_admin_all_resources?).and_return(true)
|
||||
stub_application_setting(admin_mode: false)
|
||||
end
|
||||
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
|
|
|||
Loading…
Reference in New Issue