Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
03a752ebe4
commit
7151b07ad4
|
|
@ -0,0 +1,105 @@
|
|||
# The methods listed here have been identified as "unused" by the linter
|
||||
# scripts/lint/unused_helper_methods.rb, and are potential targets for future
|
||||
# removal.
|
||||
#
|
||||
# If it turns out that a method you are attempting to remove is in fact in use,
|
||||
# remove it from this file and add it to `excluded_methods.yml`.
|
||||
#
|
||||
tag_pair_for_link:
|
||||
file: ee/app/helpers/admin/application_settings_helper.rb
|
||||
start_free_trial_data:
|
||||
file: ee/app/helpers/billing_plans_helper.rb
|
||||
compliance_frameworks_list_data:
|
||||
file: ee/app/helpers/compliance_management/compliance_framework/group_settings_helper.rb
|
||||
project_vulnerability_path:
|
||||
file: ee/app/helpers/ee/gitlab_routing_helper.rb
|
||||
user_group_saml_omniauth_metadata_path:
|
||||
file: ee/app/helpers/ee/gitlab_routing_helper.rb
|
||||
saas_user_caps_i18n_string:
|
||||
file: ee/app/helpers/ee/groups/settings_helper.rb
|
||||
project_compliance_framework_app_data:
|
||||
file: ee/app/helpers/ee/projects_helper.rb
|
||||
discover_page_hand_raise_lead_data:
|
||||
file: ee/app/helpers/gitlab_subscriptions/hand_raise_leads_helper.rb
|
||||
discover_duo_pro_hand_raise_lead_data:
|
||||
file: ee/app/helpers/gitlab_subscriptions/hand_raise_leads_helper.rb
|
||||
buy_addon_data:
|
||||
file: ee/app/helpers/subscriptions_helper.rb
|
||||
group_icon:
|
||||
file: app/helpers/avatars_helper.rb
|
||||
topic_icon:
|
||||
file: app/helpers/avatars_helper.rb
|
||||
project_branches:
|
||||
file: app/helpers/branches_helper.rb
|
||||
can_view_namespace_catalog?:
|
||||
file: app/helpers/ci/catalog/resources_helper.rb
|
||||
js_ci_catalog_data:
|
||||
file: app/helpers/ci/catalog/resources_helper.rb
|
||||
environments_list_data:
|
||||
file: app/helpers/environments_helper.rb
|
||||
event_feed_title:
|
||||
file: app/helpers/events_helper.rb
|
||||
event_feed_summary:
|
||||
file: app/helpers/events_helper.rb
|
||||
group_title_link:
|
||||
file: app/helpers/groups_helper.rb
|
||||
has_dismissed_ide_environments_callout?:
|
||||
file: app/helpers/ide_helper.rb
|
||||
integration_issue_type:
|
||||
file: app/helpers/integrations_helper.rb
|
||||
integration_todo_target_type:
|
||||
file: app/helpers/integrations_helper.rb
|
||||
user_dropdown_label:
|
||||
file: app/helpers/issuables_helper.rb
|
||||
project_dropdown_label:
|
||||
file: app/helpers/issuables_helper.rb
|
||||
group_dropdown_label:
|
||||
file: app/helpers/issuables_helper.rb
|
||||
text_color_for_bg:
|
||||
file: app/helpers/labels_helper.rb
|
||||
manage_labels_title:
|
||||
file: app/helpers/labels_helper.rb
|
||||
show_projects?:
|
||||
file: app/helpers/projects_helper.rb
|
||||
request_access_group_members_path:
|
||||
file: app/helpers/routing/groups/members_helper.rb
|
||||
approve_access_request_group_member_path:
|
||||
file: app/helpers/routing/groups/members_helper.rb
|
||||
resend_invite_group_member_path:
|
||||
file: app/helpers/routing/groups/members_helper.rb
|
||||
edit_pipeline_schedule_path:
|
||||
file: app/helpers/routing/pipeline_schedules_helper.rb
|
||||
play_pipeline_schedule_path:
|
||||
file: app/helpers/routing/pipeline_schedules_helper.rb
|
||||
take_ownership_pipeline_schedule_path:
|
||||
file: app/helpers/routing/pipeline_schedules_helper.rb
|
||||
request_access_project_members_path:
|
||||
file: app/helpers/routing/projects/members_helper.rb
|
||||
leave_project_members_path:
|
||||
file: app/helpers/routing/projects/members_helper.rb
|
||||
approve_access_request_project_member_path:
|
||||
file: app/helpers/routing/projects/members_helper.rb
|
||||
resend_invite_project_member_path:
|
||||
file: app/helpers/routing/projects/members_helper.rb
|
||||
search_filter_link:
|
||||
file: app/helpers/search_helper.rb
|
||||
sidebar_tracking_attributes_by_object:
|
||||
file: app/helpers/sidebars_helper.rb
|
||||
scope_avatar_classes:
|
||||
file: app/helpers/sidebars_helper.rb
|
||||
sort_value_stars_asc:
|
||||
file: app/helpers/sorting_titles_values_helper.rb
|
||||
display_subscription_banner!;:
|
||||
file: app/helpers/subscribable_banner_helper.rb
|
||||
group_or_project_milestone_path:
|
||||
file: app/helpers/timeboxes_helper.rb
|
||||
can_admin_project_milestones?:
|
||||
file: app/helpers/timeboxes_helper.rb
|
||||
render_two_factor_auth_recovery_settings_check;:
|
||||
file: app/helpers/users/callouts_helper.rb
|
||||
dismiss_two_factor_auth_recovery_settings_check;:
|
||||
file: app/helpers/users/callouts_helper.rb
|
||||
all_visibility_levels_restricted?:
|
||||
file: app/helpers/visibility_level_helper.rb
|
||||
link_to_wiki_page:
|
||||
file: app/helpers/wiki_helper.rb
|
||||
|
|
@ -1 +1 @@
|
|||
da07ea6b72e37e8d07020e7be6cd4a378ddc442a
|
||||
bad4daa394053a0f74090abdda47f9035f4e3f9a
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export default {
|
|||
class="gl-mx-3"
|
||||
icon="close"
|
||||
category="tertiary"
|
||||
:to="$options.DESIGNS_ROUTE_NAME"
|
||||
:to="{
|
||||
name: $options.DESIGNS_ROUTE_NAME,
|
||||
query: $route.query,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { GlIcon, GlButton, GlBadge } from '@gitlab/ui';
|
||||
import { GlIcon, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue';
|
||||
import UserDate from '~/vue_shared/components/user_date.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { SHORT_DATE_FORMAT_WITH_TIME } from '~/vue_shared/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
|
@ -16,6 +17,10 @@ export default {
|
|||
GlIcon,
|
||||
GlButton,
|
||||
GlBadge,
|
||||
TimeAgo,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
deleteError: s__(
|
||||
|
|
@ -27,8 +32,6 @@ export default {
|
|||
error: s__('Pages|Has error'),
|
||||
activeState: s__('Pages|Active'),
|
||||
stoppedState: s__('Pages|Stopped'),
|
||||
primaryDeploymentTitle: s__('Pages|Primary deployment'),
|
||||
pathPrefixLabel: s__('Pages|Path prefix'),
|
||||
createdLabel: s__('Pages|Created'),
|
||||
deployJobLabel: s__('Pages|Deploy job'),
|
||||
rootDirLabel: s__('Pages|Root directory'),
|
||||
|
|
@ -39,6 +42,7 @@ export default {
|
|||
deleteBtnLabel: s__('Pages|Delete'),
|
||||
restoreBtnLabel: s__('Pages|Restore'),
|
||||
expiresAtLabel: s__('Pages|Expires at'),
|
||||
neverExpires: s__('Pages|Never expires'),
|
||||
},
|
||||
static: {
|
||||
SHORT_DATE_FORMAT_WITH_TIME,
|
||||
|
|
@ -53,25 +57,12 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
hasError: false,
|
||||
showDetail: false,
|
||||
deleteInProgress: false,
|
||||
restoreInProgress: false,
|
||||
detailContainerHeight: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isPrimary() {
|
||||
return !this.deployment.pathPrefix;
|
||||
},
|
||||
detailHeight() {
|
||||
return this.showDetail ? this.detailContainerHeight : 0;
|
||||
},
|
||||
detailStyle() {
|
||||
return {
|
||||
height: `${this.detailHeight}px`,
|
||||
visibility: this.showDetail ? 'visible' : 'hidden',
|
||||
};
|
||||
},
|
||||
ciBuildUrl() {
|
||||
return joinPaths(
|
||||
gon.relative_url_root || '/',
|
||||
|
|
@ -80,14 +71,14 @@ export default {
|
|||
`${this.deployment.ciBuildId}`,
|
||||
);
|
||||
},
|
||||
formattedRootDirectory() {
|
||||
return `/${this.deployment.rootDirectory || 'public'}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.calculateDetailHeight();
|
||||
},
|
||||
methods: {
|
||||
toggleDetail() {
|
||||
this.showDetail = !this.showDetail;
|
||||
},
|
||||
calculateDetailHeight() {
|
||||
this.detailContainerHeight = this.$refs.details?.scrollHeight;
|
||||
},
|
||||
|
|
@ -137,203 +128,150 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="gl-flex gl-flex-col gl-gap-2" @click="toggleDetail">
|
||||
<div class="gl-flex gl-items-center gl-justify-start gl-gap-3">
|
||||
<div class="gl-flex gl-justify-center gl-gap-3">
|
||||
<div data-testid="deployment-state">
|
||||
<gl-badge
|
||||
v-if="hasError"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
icon="error"
|
||||
icon-size="sm"
|
||||
data-testid="error-badge"
|
||||
>
|
||||
{{ $options.i18n.error }}
|
||||
</gl-badge>
|
||||
<gl-badge
|
||||
v-if="deployment.active"
|
||||
variant="success"
|
||||
size="sm"
|
||||
icon="check-circle-filled"
|
||||
icon-size="sm"
|
||||
>
|
||||
{{ $options.i18n.activeState }}
|
||||
</gl-badge>
|
||||
<gl-badge v-else variant="neutral" size="sm" icon="status-stopped" icon-size="sm">
|
||||
{{ $options.i18n.stoppedState }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
class="!gl-grid gl-grid-cols-[1fr,1fr] gl-gap-2 gl-py-4 md:gl-grid-cols-[1fr,3fr,2fr] md:gl-gap-0"
|
||||
>
|
||||
<div
|
||||
class="gl-flex gl-flex-col gl-gap-4 gl-overflow-hidden md:gl-flex-row md:gl-items-center md:gl-justify-between md:gl-gap-7"
|
||||
class="gl-flex gl-flex-col gl-items-start gl-justify-center gl-gap-2 md:gl-justify-start"
|
||||
data-testid="deployment-state"
|
||||
>
|
||||
<div class="gl-flex gl-items-center gl-gap-4">
|
||||
<div>
|
||||
<gl-icon
|
||||
name="chevron-lg-right"
|
||||
:class="{ 'gl-rotate-90': showDetail }"
|
||||
class="reduce-motion:gl-transition-none gl-transition-transform"
|
||||
variant="subtle"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="deployment-type" class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap">
|
||||
<template v-if="isPrimary">
|
||||
<gl-icon name="home" class="mr-1" variant="subtle" />
|
||||
<span class="sr-only">
|
||||
{{ $options.i18n.primaryDeploymentTitle }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="gl-sr-only">{{ $options.i18n.pathPrefixLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="environment" class="mr-1" variant="subtle" />
|
||||
{{ deployment.pathPrefix }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="gl-flex gl-flex-col gl-gap-2 gl-truncate gl-text-nowrap">
|
||||
<div class="gl-flex gl-items-center gl-gap-2" data-testid="deployment-url">
|
||||
<a
|
||||
v-if="deployment.active"
|
||||
:href="deployment.url"
|
||||
target="_blank"
|
||||
class="gl-w-full gl-truncate"
|
||||
@click.stop
|
||||
>
|
||||
{{ deployment.url }}
|
||||
</a>
|
||||
<span v-else class="gl-w-full gl-truncate gl-text-subtle">
|
||||
{{ deployment.url }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-flex gl-flex-col gl-items-stretch gl-gap-5 md:gl-items-end">
|
||||
<div class="gl-flex gl-items-end gl-justify-between gl-gap-6 md:gl-justify-end">
|
||||
<div
|
||||
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
|
||||
data-testid="deployment-created-at"
|
||||
>
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.createdLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="play" class="mr-1" variant="subtle" />
|
||||
<user-date
|
||||
:date="deployment.createdAt"
|
||||
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="details"
|
||||
:style="detailStyle"
|
||||
data-testid="deployment-details"
|
||||
class="gl-flex gl-flex-col gl-gap-4 gl-overflow-hidden gl-transition-all motion-reduce:gl-transition-none md:gl-flex-row md:gl-gap-7"
|
||||
>
|
||||
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-ci-build-id">
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.deployJobLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="deployments" class="mr-1" variant="subtle" />
|
||||
<a :href="ciBuildUrl" @click.stop>
|
||||
{{ deployment.ciBuildId }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
|
||||
data-testid="deployment-root-directory"
|
||||
<gl-badge
|
||||
v-if="hasError"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
icon="error"
|
||||
icon-size="sm"
|
||||
data-testid="error-badge"
|
||||
>
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.rootDirLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="folder" class="mr-1" variant="subtle" />
|
||||
/{{ deployment.rootDirectory || 'public' }}
|
||||
</div>
|
||||
{{ $options.i18n.error }}
|
||||
</gl-badge>
|
||||
<gl-badge
|
||||
v-if="deployment.active"
|
||||
variant="success"
|
||||
size="sm"
|
||||
icon="check-circle-filled"
|
||||
icon-size="sm"
|
||||
>
|
||||
{{ $options.i18n.activeState }}
|
||||
</gl-badge>
|
||||
<gl-badge v-else variant="neutral" size="sm" icon="status-stopped" icon-size="sm">
|
||||
{{ $options.i18n.stoppedState }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-col-start-1 gl-row-start-2 gl-flex gl-flex-col gl-gap-2 md:gl-col-start-2 md:gl-row-start-1"
|
||||
>
|
||||
<div data-testid="deployment-url">
|
||||
<a
|
||||
v-if="deployment.active"
|
||||
:href="deployment.url"
|
||||
target="_blank"
|
||||
class="gl-w-full gl-truncate !gl-text-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ deployment.url }}
|
||||
</a>
|
||||
<span v-else class="gl-w-full gl-truncate gl-text-subtle">
|
||||
{{ deployment.url }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-file-count">
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.filesLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="documents" class="mr-1" variant="subtle" />
|
||||
{{ deployment.fileCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-size">
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.sizeLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="disk" class="mr-1" variant="subtle" />
|
||||
|
||||
<p class="gl-mb-0" data-testid="deployment-ci-build-id">
|
||||
<gl-icon name="deployments" />
|
||||
<span class="gl-text-subtle">{{ $options.i18n.deployJobLabel }}:</span>
|
||||
<a :href="ciBuildUrl" class="!gl-text-link" @click.stop>
|
||||
{{ deployment.ciBuildId }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="gl-mb-0 gl-flex gl-items-center gl-gap-2 gl-text-subtle">
|
||||
<gl-icon name="folder" />
|
||||
<span
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.rootDirLabel"
|
||||
data-testid="deployment-root-directory"
|
||||
>{{ formattedRootDirectory }}</span
|
||||
>
|
||||
<span aria-hidden="true">·</span>
|
||||
|
||||
<span data-testid="deployment-file-count"
|
||||
>{{ deployment.fileCount }} {{ $options.i18n.filesLabel }}</span
|
||||
>
|
||||
<span aria-hidden="true">·</span>
|
||||
|
||||
<span data-testid="deployment-size">
|
||||
{{ deployment.sizeLabel }}
|
||||
<number-to-human-size :value="deployment.size" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap" data-testid="deployment-updated-at">
|
||||
<div class="gl-text-sm gl-text-subtle">{{ $options.i18n.lastUpdatedLabel }}</div>
|
||||
<div>
|
||||
<gl-icon name="clear-all" class="mr-1" variant="subtle" />
|
||||
<user-date
|
||||
:date="deployment.updatedAt"
|
||||
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="deployment.active && deployment.expiresAt"
|
||||
class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap"
|
||||
data-testid="deployment-expires-at"
|
||||
>
|
||||
<div class="gl-text-sm gl-text-subtle">
|
||||
{{ $options.i18n.expiresAtLabel }}
|
||||
</div>
|
||||
<div>
|
||||
<gl-icon name="remove" class="gl-mr-2" variant="subtle" />
|
||||
<user-date
|
||||
:date="deployment.expiresAt"
|
||||
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!deployment.active" class="gl-flex gl-flex-col gl-gap-2 gl-text-nowrap">
|
||||
<div class="gl-text-sm gl-text-subtle">
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-col-start-1 gl-row-start-3 gl-mt-3 gl-flex gl-flex-col gl-gap-2 md:gl-col-start-2 md:gl-flex-row md:gl-items-center"
|
||||
>
|
||||
<p class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-created-at">
|
||||
{{ $options.i18n.createdLabel }}
|
||||
<time-ago :time="deployment.createdAt" />
|
||||
</p>
|
||||
|
||||
<template v-if="deployment.updatedAt">
|
||||
<span class="gl-hidden md:gl-inline" aria-hidden="true">·</span>
|
||||
<p class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-updated-at">
|
||||
{{ $options.i18n.lastUpdatedLabel }}
|
||||
<time-ago :time="deployment.updatedAt" />
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-col-start-2 gl-row-start-1 gl-flex gl-flex-col gl-items-end gl-justify-between gl-gap-2 md:gl-col-start-3"
|
||||
data-testid="deployment-details"
|
||||
>
|
||||
<gl-button
|
||||
v-if="deployment.active"
|
||||
v-gl-tooltip
|
||||
icon="remove"
|
||||
category="tertiary"
|
||||
:title="$options.i18n.deleteBtnLabel"
|
||||
:loading="deleteInProgress"
|
||||
data-testid="deployment-delete"
|
||||
@click.stop="deleteDeployment"
|
||||
/>
|
||||
<gl-button
|
||||
v-else
|
||||
v-gl-tooltip
|
||||
icon="redo"
|
||||
category="tertiary"
|
||||
:title="$options.i18n.restoreBtnLabel"
|
||||
:loading="restoreInProgress"
|
||||
data-testid="deployment-restore"
|
||||
@click.stop="restoreDeployment"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-col-start-1 gl-row-start-4 gl-flex gl-flex-col gl-justify-between gl-gap-2 md:gl-col-start-3 md:gl-row-start-3 md:gl-mt-3 md:gl-items-end"
|
||||
>
|
||||
<template v-if="!deployment.active">
|
||||
<p class="gl-mb-0 gl-text-sm gl-text-danger">
|
||||
{{ $options.i18n.deleteScheduledAtLabel }}
|
||||
</div>
|
||||
<div>
|
||||
<gl-icon name="remove" class="mr-1" variant="subtle" />
|
||||
<user-date
|
||||
:date="deployment.deletedAt"
|
||||
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-hidden gl-flex-grow md:gl-block"></div>
|
||||
<div class="gl-flex gl-items-end md:gl-h-full">
|
||||
<gl-button
|
||||
v-if="deployment.active"
|
||||
icon="remove"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
size="small"
|
||||
:loading="deleteInProgress"
|
||||
data-testid="deployment-delete"
|
||||
@click.stop="deleteDeployment"
|
||||
>
|
||||
{{ $options.i18n.deleteBtnLabel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-else
|
||||
icon="redo"
|
||||
category="secondary"
|
||||
variant="confirm"
|
||||
size="small"
|
||||
:loading="restoreInProgress"
|
||||
data-testid="deployment-restore"
|
||||
@click.stop="restoreDeployment"
|
||||
>
|
||||
{{ $options.i18n.restoreBtnLabel }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</p>
|
||||
</template>
|
||||
<p v-else class="gl-mb-0 gl-text-sm gl-text-subtle" data-testid="deployment-expires-at">
|
||||
<template v-if="deployment.expiresAt">
|
||||
{{ $options.i18n.expiresAtLabel }}
|
||||
<user-date
|
||||
:date="deployment.expiresAt"
|
||||
:date-format="$options.static.SHORT_DATE_FORMAT_WITH_TIME"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>{{ $options.i18n.neverExpires }}</template>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { debounce, isEmpty } from 'lodash';
|
|||
import { __ } from '~/locale';
|
||||
import { getUsers } from '~/rest_api';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { memberName } from '../utils/member_utils';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { memberName, searchUsers } from '../utils/member_utils';
|
||||
import {
|
||||
SEARCH_DELAY,
|
||||
USERS_FILTER_ALL,
|
||||
|
|
@ -22,6 +23,8 @@ export default {
|
|||
GlIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['searchUrl'],
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
|
|
@ -133,6 +136,9 @@ export default {
|
|||
}));
|
||||
},
|
||||
retrieveUsersRequest() {
|
||||
if (this.glFeatures.newImplementationOfInviteMembersSearch) {
|
||||
return searchUsers(this.searchUrl, this.query);
|
||||
}
|
||||
return getUsers(this.query, this.queryOptions);
|
||||
},
|
||||
retrieveUsers: debounce(async function debouncedRetrieveUsers() {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default (function initInviteMembersModal() {
|
|||
hasGitlabSubscription: parseBoolean(el.dataset.hasGitlabSubscription),
|
||||
addSeatsHref: el.dataset.addSeatsHref,
|
||||
hasBsoEnabled: parseBoolean(el.dataset.hasBsoFeatureEnabled),
|
||||
searchUrl: el.dataset.searchUrl,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement(InviteMembersModal, {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
import { DEFAULT_PER_PAGE } from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export function memberName(member) {
|
||||
// user defined tokens(invites by email) will have email in `name` and will not contain `username`
|
||||
return member.username || member.name;
|
||||
}
|
||||
|
||||
export function searchUsers(url, search) {
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
search,
|
||||
per_page: DEFAULT_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function triggerExternalAlert() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ export const TRACKING_HANDLE_LABEL_MAP = {
|
|||
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, PATH_HANDLE];
|
||||
|
||||
export const SEARCH_SCOPE_PLACEHOLDER = {
|
||||
[COMMAND_HANDLE]: s__('CommandPalette|command'),
|
||||
[USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'),
|
||||
[PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'),
|
||||
[COMMAND_HANDLE]: s__('CommandPalette|Search for a page or action'),
|
||||
[USER_HANDLE]: s__('CommandPalette|Search by username (minimum 3 characters)'),
|
||||
[PROJECT_HANDLE]: s__('CommandPalette|Search by project (minimum 3 characters)'),
|
||||
[ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'),
|
||||
[PATH_HANDLE]: s__('CommandPalette|go to project file'),
|
||||
[PATH_HANDLE]: s__('CommandPalette|Search by filename'),
|
||||
};
|
||||
|
||||
export const SEARCH_SCOPE = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export default {
|
|||
class="gl-mx-3"
|
||||
icon="close"
|
||||
category="tertiary"
|
||||
:to="$options.ROUTES.workItem"
|
||||
:to="{
|
||||
name: $options.ROUTES.workItem,
|
||||
query: $route.query,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -301,6 +301,10 @@ export default {
|
|||
}
|
||||
if (this.isEditing && this.createFlow) {
|
||||
this.startEditing();
|
||||
// Reset edit state as the description
|
||||
// can also be populated from localStorage
|
||||
// when creating a new work item.
|
||||
this.wasEdited = false;
|
||||
}
|
||||
},
|
||||
error() {
|
||||
|
|
@ -328,6 +332,18 @@ export default {
|
|||
if (this.descriptionTemplate === this.descriptionText) {
|
||||
return;
|
||||
}
|
||||
if (this.createFlow && !this.wasEdited && hasContent && this.appliedTemplate === '') {
|
||||
// If the template was fetched on component mount
|
||||
// while in create flow, we may also have populated
|
||||
// the description from localStorage. In this case,
|
||||
// we need avoid showing the warning on first load.
|
||||
// while also setting appliedTemplate to the current
|
||||
// template such that reset is possible.
|
||||
this.appliedTemplate = this.descriptionTemplate;
|
||||
this.wasEdited = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUnchangedTemplate && (isDirty || hasContent)) {
|
||||
this.showTemplateApplyWarning = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
position: sticky;
|
||||
top: calc(#{$calc-application-header-height} + #{$settings-sticky-header-height - $gl-spacing-scale-3});
|
||||
left: 0;
|
||||
margin-top: -1px;
|
||||
margin-top: -2px; // Fix retina issue with half pixel rendering
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
box-shadow: 0 #{$gl-spacing-scale-3} 0 var(--gl-border-color-default);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Members
|
||||
module InviteModalActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def invite_search
|
||||
users = Members::InviteUsersFinder.new(current_user, source, search: invite_search_params[:search]).execute
|
||||
.page(1)
|
||||
.per(invite_search_per_page)
|
||||
|
||||
render json: UserSerializer.new.represent(users)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invite_search_per_page
|
||||
(pagination_params[:per_page] || 20).to_i
|
||||
end
|
||||
|
||||
def invite_search_params
|
||||
params.permit(:search)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Groups::GroupMembersController < Groups::ApplicationController
|
||||
include MembershipActions
|
||||
include Members::InviteModalActions
|
||||
include MembersPresentation
|
||||
include SortingHelper
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
include MembershipActions
|
||||
include Members::InviteModalActions
|
||||
include MembersPresentation
|
||||
include SortingHelper
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Used for searching users that can be added to group/project members
|
||||
#
|
||||
# Arguments:
|
||||
# current_user - which user use
|
||||
# resource - group or project
|
||||
# search: string
|
||||
module Members
|
||||
class InviteUsersFinder < UsersFinder
|
||||
attr_reader :resource
|
||||
|
||||
def initialize(current_user, resource, search: nil)
|
||||
@current_user = current_user
|
||||
@resource = resource
|
||||
@params = { search: search }
|
||||
end
|
||||
|
||||
def base_scope
|
||||
users = User.active.without_project_bot
|
||||
|
||||
users = scope_for_resource(users)
|
||||
|
||||
users.order_id_desc
|
||||
end
|
||||
|
||||
def scope_for_resource(users)
|
||||
users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Members::InviteUsersFinder.prepend_mod
|
||||
|
|
@ -309,16 +309,24 @@ class ContainerRepository < ApplicationRecord
|
|||
self.find_by(project: path.repository_project, name: path.repository_name)
|
||||
end
|
||||
|
||||
def has_protected_tag_rules_for_delete?(user)
|
||||
return true if user.nil?
|
||||
def protected_from_delete_by_tag_rules?(user)
|
||||
return true unless user
|
||||
|
||||
# Check for immutable tag protection rules
|
||||
if Feature.enabled?(:container_registry_immutable_tags, project) && project.has_container_registry_immutable_tag_rules? && has_tags?
|
||||
return true
|
||||
end
|
||||
|
||||
# Admins are not restricted by mutable tag protection rules
|
||||
return false if user.can_admin_all_resources?
|
||||
|
||||
# Check for mutable tag protection rules
|
||||
return false unless project.has_container_registry_protected_tag_rules?(
|
||||
action: 'delete',
|
||||
access_level: project.team.max_member_access(user.id)
|
||||
access_level: project.team.max_member_access(user.id),
|
||||
include_immutable: false
|
||||
)
|
||||
|
||||
# This is an API call so we put it last
|
||||
return false unless has_tags?
|
||||
|
||||
true
|
||||
|
|
|
|||
|
|
@ -3543,9 +3543,15 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def has_container_registry_protected_tag_rules?(action:, access_level:)
|
||||
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level) do
|
||||
container_registry_protection_tag_rules.for_actions_and_access([action], access_level).exists?
|
||||
def has_container_registry_protected_tag_rules?(action:, access_level:, include_immutable: true)
|
||||
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level, include_immutable) do
|
||||
container_registry_protection_tag_rules.for_actions_and_access([action], access_level, include_immutable:).exists?
|
||||
end
|
||||
end
|
||||
|
||||
def has_container_registry_immutable_tag_rules?
|
||||
strong_memoize_with(:has_container_registry_immutable_tag_rules) do
|
||||
container_registry_protection_tag_rules.immutable.exists?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class ContainerRepositoryPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
|
||||
condition(:protected_for_delete) { @subject.has_protected_tag_rules_for_delete?(@user) }
|
||||
condition(:protected_for_delete) { @subject.protected_from_delete_by_tag_rules?(@user) }
|
||||
|
||||
rule { protected_for_delete }.policy do
|
||||
prevent :destroy_container_image
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
.js-invite-members-modal{ data: { is_project: 'false',
|
||||
access_levels: access_level_roles_user_can_assign(group, group.access_level_roles).to_json,
|
||||
reload_page_on_submit: content_for(:reload_on_member_invite_success).present?.to_s,
|
||||
search_url: invite_search_group_group_members_url(group, format: :json),
|
||||
help_link: help_page_url('user/permissions.md') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
.js-invite-members-modal{ data: { is_project: 'true',
|
||||
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
|
||||
reload_page_on_submit: content_for(:reload_on_member_invite_success).present?.to_s,
|
||||
search_url: invite_search_namespace_project_project_members_url(namespace_id: project.namespace, project_id: project, format: :json),
|
||||
help_link: help_page_url('user/permissions.md') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: new_implementation_of_invite_members_search
|
||||
description: New implementation of "Invite Members" search
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460261
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/190070
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/540306
|
||||
milestone: '18.1'
|
||||
group: group::authentication
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -128,6 +128,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
delete :leave
|
||||
|
||||
get :invite_search, format: :json
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
resources :project_members, except: [:show, :new, :create, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+:]+} }, concerns: :access_requestable do
|
||||
collection do
|
||||
delete :leave
|
||||
|
||||
get :invite_search, format: :json
|
||||
end
|
||||
|
||||
member do
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
migration_job_name: BackfillProtectedBranchMergeAccessLevelsProtectedBranchNamespaceId
|
||||
description: Backfills sharding key `protected_branch_merge_access_levels.protected_branch_namespace_id` from `protected_branches`.
|
||||
description: Backfills sharding key `protected_branch_merge_access_levels.protected_branch_namespace_id`
|
||||
from `protected_branches`.
|
||||
feature_category: source_code_management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174564
|
||||
milestone: '17.7'
|
||||
queued_migration_version: 20241204130230
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20250511231556'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
table_name: compromised_password_detections
|
||||
classes:
|
||||
- Users::CompromisedPasswordDetection
|
||||
feature_categories:
|
||||
- system_access
|
||||
description: Stores detections of user passwords being compromised
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191112
|
||||
milestone: '18.1'
|
||||
gitlab_schema: gitlab_main_clusterwide
|
||||
table_size: small
|
||||
|
|
@ -8,14 +8,6 @@ description: PyPI package metadata
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27632
|
||||
milestone: '13.0'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: package_id
|
||||
table: packages_packages
|
||||
sharding_key: project_id
|
||||
belongs_to: package
|
||||
desired_sharding_key_migration_job_name: BackfillPackagesPypiMetadataProjectId
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCompromisedPasswordDetections < Gitlab::Database::Migration[2.3]
|
||||
disable_ddl_transaction!
|
||||
milestone '18.1'
|
||||
|
||||
def up
|
||||
create_table :compromised_password_detections do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.datetime_with_timezone :resolved_at, null: true, index: false
|
||||
t.references :user,
|
||||
null: false,
|
||||
foreign_key: { on_delete: :cascade },
|
||||
index: true
|
||||
|
||||
t.index :user_id,
|
||||
name: "index_unresolved_compromised_password_detection_on_user_id",
|
||||
unique: true,
|
||||
where: "resolved_at IS NULL"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :compromised_password_detections
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeHkBackfillProtectedBranchMergeAccessLevelsProtectedBranch61982 < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillProtectedBranchMergeAccessLevelsProtectedBranchNamespaceId',
|
||||
table_name: :protected_branch_merge_access_levels,
|
||||
column_name: :id,
|
||||
job_arguments: [:protected_branch_namespace_id, :protected_branches, :namespace_id, :protected_branch_id],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPackagesPypiMetadataProjectIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :packages_pypi_metadata, :project_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :packages_pypi_metadata, :project_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
ccfc2c9f52ef631b077ff6dedcd50966fe0390476ed52198c21e380e6a3968a0
|
||||
|
|
@ -0,0 +1 @@
|
|||
f1c7700c9cc75028eb9cc9fb1e03be37b28b603ff70b4b7bfaae965112efb041
|
||||
|
|
@ -0,0 +1 @@
|
|||
dd9a26d16d78fe6d6fdc3dd20137eb83b53952f76bc4e2b6de5d4b732dd238dc
|
||||
|
|
@ -12507,6 +12507,23 @@ CREATE SEQUENCE compliance_requirements_id_seq
|
|||
|
||||
ALTER SEQUENCE compliance_requirements_id_seq OWNED BY compliance_requirements.id;
|
||||
|
||||
CREATE TABLE compromised_password_detections (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
resolved_at timestamp with time zone,
|
||||
user_id bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE compromised_password_detections_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE compromised_password_detections_id_seq OWNED BY compromised_password_detections.id;
|
||||
|
||||
CREATE TABLE container_expiration_policies (
|
||||
project_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
|
@ -19453,6 +19470,7 @@ CREATE TABLE packages_pypi_metadata (
|
|||
CONSTRAINT check_379019d5da CHECK ((char_length(required_python) <= 255)),
|
||||
CONSTRAINT check_65d8dbbd9f CHECK ((char_length(author_email) <= 2048)),
|
||||
CONSTRAINT check_76afb6d4f3 CHECK ((char_length(summary) <= 255)),
|
||||
CONSTRAINT check_77e2d63abb CHECK ((project_id IS NOT NULL)),
|
||||
CONSTRAINT check_80308aa9bd CHECK ((char_length(description) <= 4000)),
|
||||
CONSTRAINT check_b1f32be96c CHECK ((char_length(description_content_type) <= 128))
|
||||
);
|
||||
|
|
@ -27004,6 +27022,8 @@ ALTER TABLE ONLY compliance_requirements ALTER COLUMN id SET DEFAULT nextval('co
|
|||
|
||||
ALTER TABLE ONLY compliance_requirements_controls ALTER COLUMN id SET DEFAULT nextval('compliance_requirements_controls_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY compromised_password_detections ALTER COLUMN id SET DEFAULT nextval('compromised_password_detections_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY container_registry_protection_rules ALTER COLUMN id SET DEFAULT nextval('container_registry_protection_rules_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY container_registry_protection_tag_rules ALTER COLUMN id SET DEFAULT nextval('container_registry_protection_tag_rules_id_seq'::regclass);
|
||||
|
|
@ -29315,6 +29335,9 @@ ALTER TABLE ONLY compliance_requirements_controls
|
|||
ALTER TABLE ONLY compliance_requirements
|
||||
ADD CONSTRAINT compliance_requirements_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY compromised_password_detections
|
||||
ADD CONSTRAINT compromised_password_detections_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY virtual_registries_packages_maven_registry_upstreams
|
||||
ADD CONSTRAINT constraint_vreg_pkgs_mvn_reg_upst_on_unique_regid_pos UNIQUE (registry_id, "position") DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
|
@ -34534,6 +34557,8 @@ CREATE INDEX index_compliance_management_frameworks_on_name_trigram ON complianc
|
|||
|
||||
CREATE INDEX index_compliance_requirements_on_namespace_id ON compliance_requirements USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_compromised_password_detections_on_user_id ON compromised_password_detections USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_container_expiration_policies_on_next_run_at_and_enabled ON container_expiration_policies USING btree (next_run_at, enabled);
|
||||
|
||||
CREATE INDEX index_container_registry_data_repair_details_on_status ON container_registry_data_repair_details USING btree (status);
|
||||
|
|
@ -37482,6 +37507,8 @@ CREATE UNIQUE INDEX index_unit_test_failures_unique_columns ON ci_unit_test_fail
|
|||
|
||||
CREATE UNIQUE INDEX index_unresolved_alerts_on_project_id_and_fingerprint ON alert_management_alerts USING btree (project_id, fingerprint) WHERE ((fingerprint IS NOT NULL) AND (status <> 2));
|
||||
|
||||
CREATE UNIQUE INDEX index_unresolved_compromised_password_detection_on_user_id ON compromised_password_detections USING btree (user_id) WHERE (resolved_at IS NULL);
|
||||
|
||||
CREATE UNIQUE INDEX index_upcoming_reconciliations_on_namespace_id ON upcoming_reconciliations USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_upcoming_reconciliations_on_organization_id ON upcoming_reconciliations USING btree (organization_id);
|
||||
|
|
@ -45416,6 +45443,9 @@ ALTER TABLE ONLY resource_state_events
|
|||
ALTER TABLE ONLY resource_milestone_events
|
||||
ADD CONSTRAINT fk_rails_c940fb9fc5 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY compromised_password_detections
|
||||
ADD CONSTRAINT fk_rails_c95dee3ea4 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY gpg_signatures
|
||||
ADD CONSTRAINT fk_rails_c97176f5f7 FOREIGN KEY (gpg_key_id) REFERENCES gpg_keys(id) ON DELETE SET NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -501,6 +501,31 @@ You can change the maximum time a job can run before it times out:
|
|||
- At the [runner level](../ci/runners/configure_runners.md#set-the-maximum-job-timeout).
|
||||
This limit must be 10 minutes or longer.
|
||||
|
||||
### Maximum number of jobs in a pipeline
|
||||
|
||||
You can limit the maximum number of jobs in a pipeline. The number
|
||||
of jobs in a pipeline is checked at pipeline creation and when new commit statuses are created.
|
||||
Pipelines that have too many jobs fail with a `size_limit_exceeded` error.
|
||||
|
||||
- On GitLab.com, a limit is
|
||||
[defined for each subscription tier](../user/gitlab_com/_index.md#cicd),
|
||||
and this limit affects all projects with that tier.
|
||||
- On GitLab Self-Managed, [Premium or Ultimate](https://about.gitlab.com/pricing/) subscriptions,
|
||||
this limit is defined under a `default` plan that affects all
|
||||
projects. This limit is disabled (`0`) by default.
|
||||
|
||||
To change the limit for a GitLab Self-Managed instance, change the `default` plan's limit with the following
|
||||
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command:
|
||||
|
||||
```ruby
|
||||
# If limits don't exist for the default plan, you can create one with:
|
||||
# Plan.default.create_limits!
|
||||
|
||||
Plan.default.actual_limits.update!(ci_pipeline_size: 500)
|
||||
```
|
||||
|
||||
Set the limit to `0` to disable it.
|
||||
|
||||
### Maximum number of deployment jobs in a pipeline
|
||||
|
||||
You can limit the maximum number of deployment jobs in a pipeline. A deployment is
|
||||
|
|
@ -531,7 +556,7 @@ When this limit is exceeded, pipeline creation fails with the error `downstream
|
|||
|
||||
Increasing this limit is not recommended. The default limit protects your GitLab instance from excessive resource consumption, potential pipeline recursion, and database overload.
|
||||
|
||||
Instead of increasing the limit, restructure your CI/CD configuration by splitting large pipeline hierarchies into smaller pipelines or using parallel jobs.
|
||||
Instead of increasing the limit, restructure your CI/CD configuration by splitting large pipeline hierarchies into smaller pipelines. Consider using `needs` between jobs or dependent stages within a single pipeline.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ Event data does not include source code or other customer-created content stored
|
|||
For more information, see also:
|
||||
|
||||
- [Metrics dictionary](https://metrics.gitlab.com/?status=active) for a list of events and metrics
|
||||
- [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) for data privacy policy
|
||||
- [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/)
|
||||
|
||||
### Benefits of event data
|
||||
|
||||
|
|
@ -81,7 +81,3 @@ The log file is located at:
|
|||
- `/home/git/gitlab/log/product_usage_data.log` on self-compiled installations
|
||||
|
||||
While these logs provide thorough visibility into data transmission, they're designed specifically for inspection by security teams rather than feature usage analysis. For more detailed information about logging system, see the [Log system documentation](../logs/_index.md#product-usage-data-log).
|
||||
|
||||
### Frequently asked questions on event data
|
||||
|
||||
You can access frequently asked questions on event data [here](https://handbook.gitlab.com/handbook/legal/privacy/product-usage-events-faq/).
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ GitLab Inc. periodically collects information about your instance in order
|
|||
to perform various actions.
|
||||
|
||||
For free GitLab Self-Managed instances, all usage statistics are [opt-out](#enable-or-disable-service-ping).
|
||||
For information about other tiers, see [Customer Product Usage Information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
|
||||
|
||||
## Service Ping
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ There are several other benefits to enabling Service Ping:
|
|||
|
||||
- Analyze the users' activities over time of your GitLab installation.
|
||||
- A [DevOps Score](../analytics/dev_ops_reports.md) to give you an overview of your entire instance's adoption of concurrent DevOps from planning to monitoring.
|
||||
- More proactive support (assuming that our [Customer Success Managers (CSMs)](https://handbook.gitlab.com/job-families/sales/customer-success-management/) and support organization used the data to deliver more value).
|
||||
- More proactive support through Customer Success Managers (CSMs) who can use the collected data.
|
||||
- Insight and advice into how to get the most value out of your investment in GitLab.
|
||||
- Reports that show how you compare against other similar organizations (anonymized), with specific advice and recommendations on how to improve your DevOps processes.
|
||||
- Participation in our [Registration Features Program](#registration-features-program) to receive free paid features.
|
||||
|
|
@ -44,7 +43,7 @@ There are several other benefits to enabling Service Ping:
|
|||
In GitLab versions 14.1 and later, GitLab Free customers with a GitLab Self-Managed instance running
|
||||
GitLab Enterprise Edition can receive paid features by [enabling registration features](#enable-registration-features) and sending us
|
||||
activity data through Service Ping. Features introduced here do not remove the feature from its paid
|
||||
tier. Instances on a paid tier are subject to our [Product Usage Data policy](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) managed by [Cloud Licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/).
|
||||
tier. Instances on a paid tier are subject to the [Product Usage Data policy](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/) managed by [Cloud Licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/).
|
||||
|
||||
### Available features
|
||||
|
||||
|
|
@ -137,7 +136,6 @@ If your GitLab instance is behind a proxy, set the appropriate
|
|||
{{< alert type="note" >}}
|
||||
|
||||
Whether you can disable Service Ping completely depends on the instance's tier and the specific license.
|
||||
For more information, see [Customer Product Usage Information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
|
||||
Service Ping settings only control whether the data is being shared with GitLab, or limited to only internal use by the instance.
|
||||
Even if you disable Service Ping, the `gitlab_service_ping_worker` background job still periodically generates a Service Ping payload for your instance.
|
||||
The payload is available in the [Metrics and profiling](#manually-upload-service-ping-payload) admin section.
|
||||
|
|
@ -202,7 +200,6 @@ the **Admin** area.
|
|||
## Enable or disable optional data in Service Ping
|
||||
|
||||
GitLab differentiates between operational and optional collected data.
|
||||
For more information, see [Customer product usage information](https://handbook.gitlab.com/handbook/legal/privacy/customer-product-usage-information/#service-ping-formerly-known-as-usage-ping).
|
||||
|
||||
### Through the UI
|
||||
|
||||
|
|
|
|||
|
|
@ -820,6 +820,11 @@ Example response:
|
|||
Add or update the pipeline status of a commit. If the commit is associated with a merge request,
|
||||
the API call must target the commit in the merge request's source branch.
|
||||
|
||||
If a pipeline already exists and it exceeds the [maximum number of jobs in a single pipeline limit](../administration/instance_limits.md#maximum-number-of-jobs-in-a-pipeline):
|
||||
|
||||
- If `pipeline_id` is specified, a `422` error is returned: `The number of jobs has exceeded the limit`.
|
||||
- Otherwise, a new pipeline is created.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/statuses/:sha
|
||||
```
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ Advanced search works with the following versions of Elasticsearch.
|
|||
|
||||
| GitLab version | Elasticsearch version |
|
||||
|-----------------------|-----------------------------|
|
||||
| GitLab 15.0 and later | Elasticsearch 7.x and later |
|
||||
| GitLab 15.0 and later | Elasticsearch 7.x and 8.x |
|
||||
| GitLab 14.0 to 14.10 | Elasticsearch 6.8 to 7.x |
|
||||
|
||||
Advanced search follows the [Elasticsearch end-of-life policy](https://www.elastic.co/support/eol).
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ the related documentation:
|
|||
| Artifacts maximum size (compressed) | 1 GB | See [Maximum artifacts size](../../administration/settings/continuous_integration.md#set-maximum-artifacts-size). |
|
||||
| Artifacts [expiry time](../../ci/yaml/_index.md#artifactsexpire_in) | 30 days unless otherwise specified | See [Default artifacts expiration](../../administration/settings/continuous_integration.md#set-default-artifacts-expiration). Artifacts created before June 22, 2020 have no expiry. |
|
||||
| Scheduled Pipeline Cron | `*/5 * * * *` | See [Pipeline schedules advanced configuration](../../administration/cicd/_index.md#change-maximum-scheduled-pipeline-frequency). |
|
||||
| Maximum jobs in a single pipeline | `500` for Free tier, `1000` for all trial tiers, `1500` for Premium, and `2000` for Ultimate. | See [Maximum number of jobs in a pipeline](../../administration/instance_limits.md#maximum-number-of-jobs-in-a-pipeline). |
|
||||
| Maximum jobs in active pipelines | `500` for Free tier, `1000` for all trial tiers, `20000` for Premium, and `100000` for Ultimate. | See [Number of jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines). |
|
||||
| Maximum CI/CD subscriptions to a project | `2` | See [Number of CI/CD subscriptions to a project](../../administration/instance_limits.md#number-of-cicd-subscriptions-to-a-project). |
|
||||
| Maximum number of pipeline triggers in a project | `25000` | See [Limit the number of pipeline triggers](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers). |
|
||||
|
|
|
|||
|
|
@ -181,3 +181,21 @@ This error occurs when a Sidekiq worker processing the import
|
|||
restarts due to high CPU or memory usage during import.
|
||||
To configure workers for imports, see
|
||||
[Sidekiq configuration](../../project/import/_index.md#sidekiq-configuration).
|
||||
|
||||
## Error: `BulkImports::FileDownloadService::ServiceError Invalid content type`
|
||||
|
||||
When using direct transfer between GitLab instances, you might encounter the following error:
|
||||
|
||||
```plaintext
|
||||
BulkImports::FileDownloadService::ServiceError Invalid content type
|
||||
```
|
||||
|
||||
This error is related to how network traffic is routed between instances.
|
||||
If a content type other than `application/gzip` is returned,
|
||||
your network requests might be bypassing GitLab Workhorse.
|
||||
|
||||
To resolve this issue:
|
||||
|
||||
- Check that your Ingress is configured to route traffic through
|
||||
GitLab Workhorse on port `8181` rather than directly to Puma.
|
||||
- Consider enabling [proxy downloads](../../../administration/object_storage.md#proxy-download) for object storage.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
stage: Package
|
||||
group: Package Registry
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
title: Virtual Registry
|
||||
title: Virtual registry
|
||||
---
|
||||
|
||||
{{< details >}}
|
||||
|
|
@ -36,8 +36,7 @@ With this approach, you can configure your applications to use one virtual regis
|
|||
|
||||
To configure the virtual registry:
|
||||
|
||||
- You need a top-level group with a GitLab license with at least the premium level.
|
||||
- You must be at least Maintainer of the top-level group.
|
||||
- You need a top-level group with at least the Maintainer role.
|
||||
- Make sure you enable the dependency proxy setting. It's enabled by default, but [administrators can turn it off](../../../administration/packages/dependency_proxy.md).
|
||||
- You must configure authentication for your supported [package format](#supported-package-formats).
|
||||
|
||||
|
|
@ -60,7 +59,12 @@ When a virtual registry receives a request for a package:
|
|||
|
||||
### Caching system
|
||||
|
||||
All upstream registries have a caching system. A caching system stores request paths in a cache entry, and serves the responses for identical requests from the GitLab virtual registry. This way, the virtual registry does not have to contact the upstream again when the same package is requested.
|
||||
All upstream registries have a caching system that:
|
||||
|
||||
- Stores requests in a cache entry
|
||||
- Serves the responses for identical requests from the GitLab virtual registry
|
||||
|
||||
This way, the virtual registry does not have to contact the upstream again when the same package is requested.
|
||||
|
||||
If a requested path has not been cached in any of the available upstreams:
|
||||
|
||||
|
|
@ -71,9 +75,9 @@ If the requested path has been cached in any of the available upstreams:
|
|||
|
||||
1. The virtual registry checks the [cache validity period](#cache-validity-period) to see if the cache entry needs to be refreshed before forwarding the response.
|
||||
1. If the cache is valid, the cache entry of the upstream fulfills the request.
|
||||
- At this point, notice the virtual registry does not walk through the ordered list of upstreams again. If a lower priority upstream has cached the request, and a higher priority upstream has the requested file but not in its cache, the request is fulfilled with the lower priority upstream cache entry. This is by design.
|
||||
- If a lower priority upstream has the request in its cache, and a higher priority contains the file but has not cached the request, the lower priority upstream fulfills the request. The virtual registry does not walk the ordered list of upstreams again.
|
||||
|
||||
If an upstream can't be found to fulfill the request, the virtual registry returns a `404 Not Found` error.
|
||||
The virtual registry returns a `404 Not Found` error if it cannot find an upstream to fulfill the request.
|
||||
|
||||
#### Cache validity period
|
||||
|
||||
|
|
@ -83,26 +87,32 @@ that a cache entry is considered valid to fulfill a request.
|
|||
Before the virtual registry pulls from an existing cache entry,
|
||||
it checks the cache validity period to determine if the entry must be refreshed or not.
|
||||
|
||||
If the entry is outside the validity period, the virtual registry checks if the upstream response is identical to the one in the cache. If:
|
||||
If the entry is outside the validity period, the virtual registry checks
|
||||
if the upstream response is identical to the one in the cache. If:
|
||||
|
||||
- The response is identical, the entry is used to fulfill the request.
|
||||
- The response is not identical, the response is downloaded again from the upstream to overwrite the upstream cache entry.
|
||||
|
||||
If network conditions prevent the virtual registry from connecting to the upstream, the caching system uses the available cache entry to serve the request. This way, as long as the virtual registry has the response related to a request in the cache, that request is fulfilled, even when outside the validity period.
|
||||
If the virtual registry cannot connect to an upstream due to network conditions,
|
||||
the upstream serves the request with the available cache entry.
|
||||
|
||||
As long as the virtual registry has the response related to
|
||||
a request in the cache, that request is fulfilled,
|
||||
even when outside the validity period.
|
||||
|
||||
##### Set the cache validity period
|
||||
|
||||
The cache validity period is important in the overall performance of the virtual registry to fulfill requests. Contacting external registries is a costly operation. Smaller validity periods increase the amount of checks, and longer periods decrease them.
|
||||
|
||||
You can turn off cache validity checks by setting it to 0.
|
||||
You can turn off cache validity checks by setting it to `0`.
|
||||
|
||||
The default value of the cache validity period is 24 hours.
|
||||
The default value of the cache validity period is `24` hours.
|
||||
|
||||
You should set the cache validity period to 0 when the external registry targeted by the upstream is known to have immutable responses. This is often the case when using official public registries. For more information, check your [supported package format](#supported-package-formats).
|
||||
You should set the cache validity period to `0` when the external registry targeted by the upstream is known to have immutable responses. This is often the case with official public registries. For more information, check your [supported package format](#supported-package-formats).
|
||||
|
||||
### Object storage usage
|
||||
|
||||
Cache entries save their files in object storage in [the `dependency_proxy` bucket](../../../administration/object_storage.md#configure-the-parameters-of-each-object).
|
||||
Cache entries save their files in object storage in the [`dependency_proxy` bucket](../../../administration/object_storage.md#configure-the-parameters-of-each-object).
|
||||
|
||||
Object storage usage counts towards the top-level group [object storage usage limit](../../storage_usage_quotas.md#view-storage).
|
||||
|
||||
|
|
@ -116,9 +126,12 @@ Virtual registry performance might vary based on factors like:
|
|||
|
||||
### Tradeoffs
|
||||
|
||||
Virtual registries are more advanced than public registries. When you pull dependencies with a virtual registry, it might take longer than other registries, such as public, official registries.
|
||||
Virtual registries are more advanced than public registries.
|
||||
When you pull dependencies with a virtual registry,
|
||||
it might take longer than other registries, such as public, official registries.
|
||||
|
||||
Compared with public registries, virtual registries also support multiple upstream registries and authentication, but these advantages are not free.
|
||||
Compared with public registries, virtual registries
|
||||
also support multiple upstream registries and authentication.
|
||||
|
||||
### Upstream prioritization
|
||||
|
||||
|
|
@ -132,7 +145,7 @@ When you manage a list of private upstream registries:
|
|||
- You should prioritize registries with the most packages at the top of the list. This approach:
|
||||
- Increases the chances that a high-priority registry can fulfill the request
|
||||
- Prevents walking the entire ordered list to find a valid upstream registry
|
||||
- You should put registries that host the least amount of packages at the bottom of the list.
|
||||
- You should put registries with the least amount of packages at the bottom of the list.
|
||||
|
||||
### Performance improvements with usage
|
||||
|
||||
|
|
@ -142,8 +155,6 @@ When an upstream registry caches a request, the time to fulfill an identical req
|
|||
|
||||
### Use the CI/CD cache
|
||||
|
||||
GitLab CI/CD jobs can further increase their performance by [using caching in GitLab CI/CD](../../../ci/caching/_index.md#common-use-cases-for-caches) for dependencies.
|
||||
You can use [caching in GitLab CI/CD](../../../ci/caching/_index.md#common-use-cases-for-caches) so that jobs do not have to download dependencies from the virtual registry.
|
||||
|
||||
By using caching in the CI/CD cache, jobs can avoid downloading dependencies from the virtual registry, which improves the execution time.
|
||||
|
||||
However, this enhancement comes with the cost of duplicating the storage for each dependency. Each dependency is stored in the virtual registry and the CI/CD cache.
|
||||
This method improves execution time, but also duplicates storage for each dependency (dependencies are stored in the CI/CD cache and virtual registry).
|
||||
|
|
|
|||
|
|
@ -56,18 +56,18 @@ When using the Maven virtual registry, remember the following restrictions:
|
|||
|
||||
## Manage the virtual registry
|
||||
|
||||
Manage the virtual registry with the dedicated virtual registry API.
|
||||
Manage the virtual registry with the [Maven virtual registry API](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
|
||||
|
||||
You cannot configure the virtual registry in the UI, but this [epic 15090](https://gitlab.com/groups/gitlab-org/-/epics/15090) proposes the implementation of a virtual registry UI.
|
||||
You cannot configure the virtual registry in the UI, but [epic 15090](https://gitlab.com/groups/gitlab-org/-/epics/15090) proposes the implementation of a virtual registry UI.
|
||||
|
||||
### Authenticate to the virtual registry API
|
||||
|
||||
The virtual registry API uses [REST API authentication](../../../../api/rest/authentication.md) methods. You must authenticate to the API to manage virtual registry objects.
|
||||
|
||||
Read operations are open to users that can [use the virtual registry](#use-the-virtual-registry).
|
||||
Read operations are available to users that can [use the virtual registry](#use-the-virtual-registry).
|
||||
|
||||
Write operations, such as [creating a new registry](#create-and-manage-a-virtual-registry) or [adding upstreams](#manage-upstream-registries) are restricted to
|
||||
direct maintainers of the top-level group that hosts the virtual registry.
|
||||
Write operations, such as [creating a new registry](#create-and-manage-a-virtual-registry) or [adding upstreams](#manage-upstream-registries), are restricted to
|
||||
direct maintainers of the top-level group of the virtual registry.
|
||||
|
||||
### Create and manage a virtual registry
|
||||
|
||||
|
|
@ -81,11 +81,11 @@ curl --fail-with-body \
|
|||
--url "https://gitlab.example.com/api/v4/groups/<group_id>/-/virtual_registries/packages/maven/registries"
|
||||
```
|
||||
|
||||
- `<header>` is the [authentication header](../../../../api/rest/authentication.md).
|
||||
- `<group_id>` is the top-level group ID.
|
||||
- `<registry_name>` is the registry name. Required.
|
||||
- `<header>`: The [authentication header](../../../../api/rest/authentication.md).
|
||||
- `<group_id>`: The top-level group ID.
|
||||
- `<registry_name>`: The registry name.
|
||||
|
||||
To see other endpoints and examples related to managing a virtual registry, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
|
||||
For more information about other endpoints and examples related to Maven virtual registries, see the [API](../../../../api/maven_virtual_registries.md#manage-virtual-registries).
|
||||
|
||||
### Manage upstream registries
|
||||
|
||||
|
|
@ -105,24 +105,20 @@ curl --fail-with-body \
|
|||
--url "https://gitlab.example.com/api/v4/virtual_registries/packages/maven/registries/<registry_id>/upstreams"
|
||||
```
|
||||
|
||||
- `<header>` is the [authentication header](../../../../api/rest/authentication.md).
|
||||
- `<registry_id>` is the Maven virtual registry ID.
|
||||
- `<upstream_name>` is the upstream registry name. Required.
|
||||
- `<upstream_url>` is the Maven upstream URL. Required.
|
||||
- `<upstream_username>` is the username to use with the Maven upstream. Required if an `<upstream_password>` is set.
|
||||
- `<upstream_password>` is the password to use with the Maven upstream. Required if an `<upstream_username>` is set.
|
||||
- `<upstream_cache_validity_hours>` is the [cache validity period](../_index.md#cache-validity-period) in hours. Optional. The default value is `24`. `0` disables the cache entry checks.
|
||||
- if the `<upstream_url>` is set to [Maven central](#use-maven-central-as-an-upstream), the validity period is set to `0`.
|
||||
- `<header>`: The [authentication header](../../../../api/rest/authentication.md).
|
||||
- `<registry_id>`: The Maven virtual registry ID.
|
||||
- `<upstream_name>`: The upstream registry name.
|
||||
- `<upstream_url>`: The Maven upstream URL.
|
||||
- `<upstream_username>`: The username to use with the Maven upstream. Required if an `<upstream_password>` is set.
|
||||
- `<upstream_password>`: The password to use with the Maven upstream. Required if an `<upstream_username>` is set.
|
||||
- `<upstream_cache_validity_hours>`: (optional) The [cache validity period](../_index.md#cache-validity-period) in hours. The default value is `24`. To turn off cache entry checks, set to `0`.
|
||||
- if the `<upstream_url>` is set to Maven central:
|
||||
- You must use the following URL: `https://repo1.maven.org/maven2`
|
||||
- The validity period is set to `0` by default. All files on Maven central are immutable.
|
||||
|
||||
`<upstream_username>` and `<upstream_password>` are optional. If not set, a public (anonymous) request is used to access the upstream.
|
||||
|
||||
To see other endpoints and examples related to managing an upstream registry, including updating the upstream registry position in the list, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-upstream-registries).
|
||||
|
||||
#### Use Maven central as an upstream
|
||||
|
||||
To configure an upstream to Maven central, use the following URL: `https://repo1.maven.org/maven2`.
|
||||
|
||||
On Maven central, all files are immutable. You should set the [cache validity period](../_index.md#cache-validity-period) to `0` to disable cache checks with Maven central.
|
||||
For more information about other endpoints and examples, like updating the upstream registry position in the list, see the [API](../../../../api/maven_virtual_registries.md#manage-upstream-registries).
|
||||
|
||||
### Manage cache entries
|
||||
|
||||
|
|
@ -130,7 +126,7 @@ If necessary, cache entries can be inspected or destroyed.
|
|||
|
||||
The next time the virtual registry receives a request for the file that was referenced by the destroyed cache entry, the list of upstreams is [walked again](../_index.md#caching-system) to find an upstream that can fulfill this request.
|
||||
|
||||
To see the endpoints and examples related to managing cache entries, see the [API documentation](../../../../api/maven_virtual_registries.md#manage-cache-entries).
|
||||
To learn more about managing cache entries, see the [API](../../../../api/maven_virtual_registries.md#manage-cache-entries).
|
||||
|
||||
## Use the virtual registry
|
||||
|
||||
|
|
@ -166,7 +162,8 @@ The Maven virtual registry supports the following Maven clients:
|
|||
|
||||
You must declare virtual registries in the Maven client configuration.
|
||||
|
||||
All clients must be authenticated. For the client authentication, you can use a custom HTTP header or Basic Auth. You should use one of the configurations below for each client.
|
||||
All clients must be authenticated. For the client authentication, you can use a custom HTTP header or Basic Auth.
|
||||
You should use one of the configurations below for each client.
|
||||
|
||||
{{< tabs >}}
|
||||
|
||||
|
|
@ -216,10 +213,8 @@ To configure a Maven virtual registry as an additional registry, in the `pom.xml
|
|||
</repositories>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<registry_id>` is the ID of the Maven virtual registry.
|
||||
- `<id>` contains the same ID of the `<server>` used in the `settings.xml`.
|
||||
- `<id>`: The same ID of the `<server>` used in the `settings.xml`.
|
||||
- `<registry_id>`: The ID of the Maven virtual registry.
|
||||
|
||||
To configure a Maven virtual registry as a replacement of the default registry, in the `settings.xml`, add a `mirrors` element:
|
||||
|
||||
|
|
@ -239,9 +234,7 @@ To configure a Maven virtual registry as a replacement of the default registry,
|
|||
</settings>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<registry_id>` is the ID of the Maven virtual registry.
|
||||
- `<registry_id>`: The ID of the Maven virtual registry.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
|
|
@ -300,9 +293,7 @@ Add a `repositories` section to your
|
|||
}
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<registry_id>` is the ID of the Maven virtual registry.
|
||||
- `<registry_id>`: The ID of the Maven virtual registry.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
|
|
@ -317,7 +308,7 @@ Where:
|
|||
|
||||
Authentication for [SBT](https://www.scala-sbt.org/index.html) is based on
|
||||
[basic HTTP Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
|
||||
You must to provide a name and a password.
|
||||
You must provide a name and a password.
|
||||
|
||||
In your [`build.sbt`](https://www.scala-sbt.org/1.x/docs/Directories.html#sbt+build+definition+files), add the following lines:
|
||||
|
||||
|
|
@ -327,14 +318,13 @@ resolvers += ("gitlab" at "<endpoint_url>")
|
|||
credentials += Credentials("GitLab Virtual Registry", "<host>", "<username>", "<token>")
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<endpoint_url>`: The Maven virtual registry URL.
|
||||
For example, `https://gitlab.example.com/api/v4/virtual_registries/packages/maven/<registry_id>`, where `<registry_id>` is the ID of the Maven virtual registry.
|
||||
- `<host>`: The host present in the `<endpoint_url>` without the protocol scheme or the port. For example, `gitlab.example.com`.
|
||||
- `<username>`: The username.
|
||||
- `<token>`: The configured token.
|
||||
|
||||
- `<endpoint_url>` is the Maven virtual registry URL.
|
||||
For example, `https://gitlab.example.com/api/v4/virtual_registries/packages/maven/<registry_id>`. `<registry_id>` is the ID of the Maven virtual registry.
|
||||
- `<host>` is the host present in the `<endpoint_url>` without the protocol scheme or the port. For example, `gitlab.example.com`.
|
||||
- `<username>` is the username.
|
||||
- `<token>` is the configured token.
|
||||
- Make sure that the first argument of `Credentials` is `"GitLab Virtual Registry"`. This realm name must _exactly match_ the [basic auth realm](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#www-authenticate_and_proxy-authenticate_headers) sent by the Maven virtual registry.
|
||||
Make sure that the first argument of `Credentials` is `"GitLab Virtual Registry"`. This realm name must _exactly match_ the [Basic Auth realm](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#www-authenticate_and_proxy-authenticate_headers) sent by the Maven virtual registry.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -311,6 +311,12 @@ For more information, see the history.
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Deleting a branch rule is not available for rules targeting `all branches`.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Maintainer role for the project.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ Create a file in `ActiveContext::Config.migrations_path`.
|
|||
ActiveContext supports several field types for defining collection schemas:
|
||||
|
||||
- `bigint`: For large numeric values (accepts `index: true/false`, defaults to `false`)
|
||||
- `integer`: For standard numeric values (accepts `index: true/false`, defaults to `false`)
|
||||
- `smallint`: For small numeric values (accepts `index: true/false`, defaults to `false`)
|
||||
- `boolean`: For boolean values (accepts `index: true/false`, defaults to `true`)
|
||||
- `keyword`: For exact-match searchable string fields (always indexed, no `index` option)
|
||||
- `text`: For full-text searchable content (accepts `index: true/false`, defaults to `false`)
|
||||
|
|
@ -24,6 +26,8 @@ class CreateMergeRequests < ActiveContext::Migration[1.0]
|
|||
create_collection :merge_requests, number_of_partitions: 3 do |c|
|
||||
c.bigint :issue_id, index: true
|
||||
c.bigint :namespace_id, index: true
|
||||
c.integer :iid, index: true
|
||||
c.smallint :priority, index: true
|
||||
c.boolean :is_draft
|
||||
c.keyword :traversal_ids
|
||||
c.text :description
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ module ActiveContext
|
|||
fields << Field::Bigint.new(name, index: index)
|
||||
end
|
||||
|
||||
def integer(name, index: false)
|
||||
fields << Field::Integer.new(name, index: index)
|
||||
end
|
||||
|
||||
def smallint(name, index: false)
|
||||
fields << Field::Smallint.new(name, index: index)
|
||||
end
|
||||
|
||||
def boolean(name, index: true)
|
||||
fields << Field::Boolean.new(name, index: index)
|
||||
end
|
||||
|
|
@ -39,6 +47,8 @@ module ActiveContext
|
|||
end
|
||||
|
||||
class Bigint < Field; end
|
||||
class Integer < Field; end
|
||||
class Smallint < Field; end
|
||||
class Boolean < Field; end
|
||||
class Keyword < Field; end
|
||||
class Text < Field; end
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ module ActiveContext
|
|||
mappings[field.name] = case field
|
||||
when Field::Bigint
|
||||
{ type: 'long' }
|
||||
when Field::Integer
|
||||
{ type: 'integer' }
|
||||
when Field::Smallint
|
||||
{ type: 'short' }
|
||||
when Field::Boolean
|
||||
{ type: 'boolean' }
|
||||
when Field::Keyword
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ module ActiveContext
|
|||
when Field::Bigint
|
||||
# Bigint is 8 bytes
|
||||
fixed_columns << [field, 8]
|
||||
when Field::Integer
|
||||
# Integer is 4 bytes
|
||||
fixed_columns << [field, 4]
|
||||
when Field::Smallint
|
||||
# Smallint is 2 bytes
|
||||
fixed_columns << [field, 2]
|
||||
when Field::Boolean
|
||||
# Boolean is 1 byte
|
||||
fixed_columns << [field, 1]
|
||||
|
|
@ -100,6 +106,10 @@ module ActiveContext
|
|||
case field
|
||||
when Field::Bigint
|
||||
table.bigint(field.name, **field.options.except(:index))
|
||||
when Field::Integer
|
||||
table.integer(field.name, **field.options.except(:index))
|
||||
when Field::Smallint
|
||||
table.integer(field.name, limit: 2, **field.options.except(:index))
|
||||
when Field::Boolean
|
||||
table.boolean(field.name, **field.options.except(:index))
|
||||
when Field::Keyword, Field::Text
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ module Gitlab
|
|||
push_frontend_feature_flag(:new_project_creation_form, current_user, type: :wip)
|
||||
push_frontend_feature_flag(:work_items_client_side_boards, current_user)
|
||||
push_frontend_feature_flag(:glql_work_items, current_user, type: :wip)
|
||||
push_frontend_feature_flag(:new_implementation_of_invite_members_search)
|
||||
end
|
||||
|
||||
# Exposes the state of a feature flag to the frontend code.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ module Gitlab
|
|||
DEFAULT_PREFIX = 'rf-'
|
||||
DEFAULT_RUNNER_COUNT = 40
|
||||
DEFAULT_JOB_COUNT = DEFAULT_RUNNER_COUNT * 10
|
||||
NO_ORGANIZATION_ERROR = "No organization found. Ensure user has an organization or pass an organization_id"
|
||||
|
||||
TAG_LIST = %w[gitlab-org docker ruby 2gb mysql linux shared shell deploy hhvm windows build postgres ios stage
|
||||
android stz front back review-apps pc java scraper test kubernetes staging no-priority osx php nodejs
|
||||
|
|
@ -56,9 +57,11 @@ module Gitlab
|
|||
@user = User.find_by_username(username)
|
||||
@registration_prefix = options[:registration_prefix] || DEFAULT_PREFIX
|
||||
@runner_count = options[:runner_count] || DEFAULT_RUNNER_COUNT
|
||||
@organization_id = nil
|
||||
@organization_id = options[:organization_id] || @user.organizations.first&.id
|
||||
@groups = {}
|
||||
@projects = {}
|
||||
|
||||
raise NO_ORGANIZATION_ERROR unless @organization_id
|
||||
end
|
||||
|
||||
# seed returns an array of hashes of projects to its assigned runners
|
||||
|
|
@ -72,7 +75,6 @@ module Gitlab
|
|||
runner_count: @runner_count
|
||||
)
|
||||
|
||||
@organization_id = ensure_organization_id
|
||||
groups_and_projects = create_groups_and_projects
|
||||
runner_ids = create_runners(groups_and_projects)
|
||||
|
||||
|
|
@ -113,25 +115,6 @@ module Gitlab
|
|||
true
|
||||
end
|
||||
|
||||
def ensure_organization_id
|
||||
args = {
|
||||
name: 'GitLab',
|
||||
path: 'gitlab'
|
||||
}
|
||||
|
||||
organization_id = ::Organizations::Organization.find_by_path(args[:path])&.id
|
||||
|
||||
return organization_id if organization_id
|
||||
|
||||
if Feature.enabled?(:allow_organization_creation, @user)
|
||||
logger.info(message: 'Creating organization', **args)
|
||||
service = ::Organizations::CreateService.new(current_user: @user, params: args)
|
||||
return execute_service!(service, :organization)&.id
|
||||
end
|
||||
|
||||
::Organizations::Organization::DEFAULT_ORGANIZATION_ID
|
||||
end
|
||||
|
||||
def create_groups_and_projects
|
||||
root_group_1 = ensure_group(name: 'top-level group 1', organization_id: @organization_id)
|
||||
root_group_2 = ensure_group(name: 'top-level group 2', organization_id: @organization_id)
|
||||
|
|
|
|||
|
|
@ -3354,6 +3354,9 @@ msgstr ""
|
|||
msgid "Account: %{account}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccountTokens|Service Account/Tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Achievements"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14942,24 +14945,24 @@ msgstr ""
|
|||
msgid "CommandPalette|Project files"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|Search by filename"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|Search by project (minimum 3 characters)"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|Search by username (minimum 3 characters)"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|Search for a page or action"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|command"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|go to project file"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|issue (enter at least 3 chars)"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|project (enter at least 3 chars)"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommandPalette|user (enter at least 3 chars)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commands applied"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35540,6 +35543,12 @@ msgstr ""
|
|||
msgid "LDAP|No LDAP synchronizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "LDAP|Select server"
|
||||
msgstr ""
|
||||
|
||||
msgid "LDAP|Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "LDAP|Sync method"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43968,6 +43977,9 @@ msgstr ""
|
|||
msgid "Pages|Last updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pages|Never expires"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pages|No deployments yet"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43980,9 +43992,6 @@ msgstr ""
|
|||
msgid "Pages|Path prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pages|Primary deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pages|Restore"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51743,6 +51752,9 @@ msgstr ""
|
|||
msgid "Roles and permissions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Roles|Groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Roll up totals may reflect child items you don’t have access to."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53386,12 +53398,21 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|%{listType} according to the %{buttonType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add exception"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add exception(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add new criteria"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add new license"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Add policy exception"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Age criteria can only be added for pre-existing vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53500,6 +53521,9 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|For scanners that require builds, when a project does not have a build pipeline."
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|If selected, the following choices will overwrite %{linkStart}project settings%{linkEnd} but only affect the branches selected in the policy."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53566,6 +53590,9 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Override project approval settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Policy Exception settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Pre-existing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53608,6 +53635,9 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Required scanners defined in the condition did not run or produce any artifacts."
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Roles"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Save allowlist"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53629,9 +53659,15 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Select list type"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Service accounts/tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Severity is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Source branch pattern"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Specify the packages where this license requires approval before use"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -58822,6 +58858,9 @@ msgstr ""
|
|||
msgid "Source-Branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "SourceBranchPattern|Source branch patterns"
|
||||
msgstr ""
|
||||
|
||||
msgid "SourceEditor|\"el\" parameter is required for createInstance()"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ require 'parallel'
|
|||
require 'rainbow'
|
||||
require 'yaml'
|
||||
|
||||
UNUSED_METHODS = 49
|
||||
EXCLUDED_METHODS_PATH = '.gitlab/lint/unused_helper_methods/excluded_methods.yml'
|
||||
POTENTIAL_METHODS_PATH = '.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml'
|
||||
|
||||
print_output = %w[true 1].include? ENV["REPORT_ALL_UNUSED_METHODS"]
|
||||
|
||||
|
|
@ -72,18 +72,21 @@ if print_output
|
|||
exit 0
|
||||
end
|
||||
|
||||
if unused.size > UNUSED_METHODS
|
||||
added = unused.size - UNUSED_METHODS
|
||||
potential_methods_count = YAML.load_file(POTENTIAL_METHODS_PATH, symbolize_names: true).size
|
||||
|
||||
if unused.size > potential_methods_count
|
||||
added = unused.size - potential_methods_count
|
||||
puts Rainbow("ERROR: #{added} unused methods were added. Please remove them.").red.bright
|
||||
|
||||
exit 1
|
||||
elsif unused.size < UNUSED_METHODS
|
||||
elsif unused.size < potential_methods_count
|
||||
warning = <<~UPDATE_UNUSED
|
||||
WARNING: It appears you have removed unused methods. Thank you!
|
||||
🏆 It appears you have removed unused methods. Thank you!
|
||||
|
||||
Please update scripts/lint/unused_helper_methods.rb to reflect the new number:
|
||||
UNUSED_METHODS = #{unused.size}
|
||||
Please update potential_methods_to_remove.yml with the current list of unused methods.
|
||||
UPDATE_UNUSED
|
||||
|
||||
puts Rainbow(warning).yellow.bright
|
||||
print Rainbow(warning).yellow.bright
|
||||
|
||||
exit 1
|
||||
end
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', :unlimited_max_formatted_output
|
|||
'.gitlab/changelog_config.yml',
|
||||
'.gitlab/CODEOWNERS',
|
||||
'.gitlab/lint/unused_helper_methods/excluded_methods.yml',
|
||||
'.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml',
|
||||
'.gitleaksignore',
|
||||
'.gitpod.yml',
|
||||
'.graphqlrc',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Members::InviteUsersFinder, feature_category: :groups_and_projects do
|
||||
let_it_be(:current_user) { create(:user, :with_namespace) }
|
||||
let_it_be(:root_group) { create(:group) }
|
||||
|
||||
let_it_be(:regular_user) { create(:user) }
|
||||
let_it_be(:admin_user) { create(:user, :admin) }
|
||||
let_it_be(:banned_user) { create(:user, :banned) }
|
||||
let_it_be(:blocked_user) { create(:user, :blocked) }
|
||||
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
|
||||
let_it_be(:external_user) { create(:user, :external) }
|
||||
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
|
||||
let_it_be(:omniauth_user) { create(:omniauth_user) }
|
||||
let_it_be(:internal_user) { Users::Internal.alert_bot }
|
||||
let_it_be(:project_bot_user) { create(:user, :project_bot) }
|
||||
let_it_be(:service_account_user) { create(:user, :service_account) }
|
||||
|
||||
before_all do
|
||||
root_group.add_owner(current_user)
|
||||
end
|
||||
|
||||
subject(:finder) do
|
||||
described_class.new(current_user, resource)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'searchable' do
|
||||
let(:searchable_users_ordered_by_id_desc) do
|
||||
[
|
||||
current_user,
|
||||
regular_user,
|
||||
admin_user,
|
||||
external_user,
|
||||
unconfirmed_user,
|
||||
omniauth_user,
|
||||
service_account_user
|
||||
].sort_by(&:id).reverse
|
||||
end
|
||||
|
||||
it 'returns searchable users ordered by id descending' do
|
||||
expect(finder.execute).to eq(searchable_users_ordered_by_id_desc)
|
||||
end
|
||||
|
||||
context 'for search param' do
|
||||
subject(:finder) do
|
||||
described_class.new(current_user, resource, search: search)
|
||||
end
|
||||
|
||||
context 'with empty string' do
|
||||
let(:search) { '' }
|
||||
|
||||
it 'returns searchable users ordered by id descending' do
|
||||
expect(finder.execute).to eq(searchable_users_ordered_by_id_desc)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a user's name" do
|
||||
let(:search) { regular_user.name }
|
||||
|
||||
it 'returns users that match the name' do
|
||||
expect(finder.execute).to eq([regular_user])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for root_group' do
|
||||
let_it_be(:resource) { root_group }
|
||||
|
||||
include_examples 'searchable'
|
||||
end
|
||||
|
||||
context 'for subgroup' do
|
||||
let_it_be(:subgroup) { create(:group, parent: root_group) }
|
||||
let_it_be(:resource) { subgroup }
|
||||
|
||||
include_examples 'searchable'
|
||||
end
|
||||
|
||||
context 'for project within group namespace' do
|
||||
let_it_be(:project) { create(:project, namespace: root_group, creator: current_user) }
|
||||
let_it_be(:resource) { project }
|
||||
|
||||
include_examples 'searchable'
|
||||
end
|
||||
|
||||
context 'for project within user namespace' do
|
||||
let_it_be(:project) { create(:project, namespace: current_user.namespace) }
|
||||
let_it_be(:resource) { project }
|
||||
|
||||
include_examples 'searchable'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,25 +1,32 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import CloseButton from '~/design_management/components/toolbar/close_button.vue';
|
||||
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
|
||||
|
||||
describe('Design management toolbar close button', () => {
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/', name: 'workItemList', component: { template: '<div>Designs</div>' } },
|
||||
{ path: '/designs', name: 'designs', component: { template: '<div>Design detail</div>' } },
|
||||
],
|
||||
mode: 'history',
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
|
||||
function createComponent() {
|
||||
wrapper = shallowMount(CloseButton, {
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
},
|
||||
wrapper = mount(CloseButton, {
|
||||
router,
|
||||
});
|
||||
}
|
||||
|
||||
it('links back to designs list', async () => {
|
||||
it('links back to designs list', () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlButton).attributes('to')).toBe(DESIGNS_ROUTE_NAME);
|
||||
expect(wrapper.findComponent(GlButton).attributes().href).toEqual('/designs');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import PagesDeployment from '~/gitlab_pages/components/deployment.vue';
|
||||
import deletePagesDeploymentMutation from '~/gitlab_pages/queries/delete_pages_deployment.mutation.graphql';
|
||||
|
|
@ -8,6 +7,7 @@ import restorePagesDeploymentMutation from '~/gitlab_pages/queries/restore_pages
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import UserDate from '~/vue_shared/components/user_date.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import {
|
||||
primaryDeployment,
|
||||
environmentDeployment,
|
||||
|
|
@ -51,10 +51,10 @@ describe('PagesDeployment', () => {
|
|||
const findErrorBadge = () => wrapper.findByTestId('error-badge');
|
||||
|
||||
describe.each`
|
||||
description | deployment | isPrimary
|
||||
${'Primary deployment'} | ${primaryDeployment} | ${true}
|
||||
${'Environment deployment'} | ${environmentDeployment} | ${false}
|
||||
`('$description', ({ deployment, isPrimary }) => {
|
||||
description | deployment
|
||||
${'Primary deployment'} | ${primaryDeployment}
|
||||
${'Environment deployment'} | ${environmentDeployment}
|
||||
`('$description', ({ deployment }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ deployment });
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ describe('PagesDeployment', () => {
|
|||
it('renders deployment details', () => {
|
||||
expect(wrapper.findByTestId('deployment-url').text()).toBe(deployment.url);
|
||||
expect(
|
||||
wrapper.findByTestId('deployment-created-at').findComponent(UserDate).props('date'),
|
||||
wrapper.findByTestId('deployment-created-at').findComponent(TimeAgo).props('time'),
|
||||
).toBe(deployment.createdAt);
|
||||
expect(wrapper.findByTestId('deployment-ci-build-id').text()).toContain(
|
||||
deployment.ciBuildId.toString(),
|
||||
|
|
@ -76,7 +76,7 @@ describe('PagesDeployment', () => {
|
|||
);
|
||||
expect(wrapper.findByTestId('deployment-size').text()).toContain('1.0 KiB');
|
||||
expect(
|
||||
wrapper.findByTestId('deployment-updated-at').findComponent(UserDate).props('date'),
|
||||
wrapper.findByTestId('deployment-updated-at').findComponent(TimeAgo).props('time'),
|
||||
).toBe(deployment.updatedAt);
|
||||
|
||||
if (deployment.expiresAt) {
|
||||
|
|
@ -84,35 +84,9 @@ describe('PagesDeployment', () => {
|
|||
wrapper.findByTestId('deployment-expires-at').findComponent(UserDate).props('date'),
|
||||
).toBe(deployment.expiresAt);
|
||||
} else {
|
||||
expect(wrapper.findByTestId('deployment-expires-at').exists()).toBe(false);
|
||||
expect(wrapper.findByTestId('deployment-expires-at').text()).toBe('Never expires');
|
||||
}
|
||||
});
|
||||
|
||||
it('toggles deployment details on click', async () => {
|
||||
expect(wrapper.findByTestId('deployment-details').isVisible()).toBe(false);
|
||||
|
||||
await wrapper.trigger('click');
|
||||
|
||||
expect(wrapper.findByTestId('deployment-details').isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
if (isPrimary) {
|
||||
it('shows "Primary deployment" as deployment type label for screen readers', () => {
|
||||
expect(wrapper.findByTestId('deployment-type').text()).toContain('Primary deployment');
|
||||
});
|
||||
|
||||
it('shows the "home" icon', () => {
|
||||
expect(wrapper.findByTestId('deployment-type').findComponent(GlIcon).props('name')).toBe(
|
||||
'home',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
it('shows the pathPrefix', () => {
|
||||
expect(wrapper.findByTestId('deployment-type').text()).toContain(
|
||||
environmentDeployment.pathPrefix,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('deployment is active', () => {
|
||||
|
|
@ -156,7 +130,7 @@ describe('PagesDeployment', () => {
|
|||
expect(wrapper.findByTestId('deployment-delete').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('restores deployment when delete button is clicked', async () => {
|
||||
it('restores deployment when restore button is clicked', async () => {
|
||||
await restoreDeployment();
|
||||
|
||||
expect(restorePagesDeploymentMutationHandler).toHaveBeenCalledWith({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import * as UserApi from '~/api/user_api';
|
||||
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
|
||||
import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants';
|
||||
import * as MembersUtils from '~/invite_members/utils/member_utils';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
|
||||
const label = 'testgroup';
|
||||
|
|
@ -16,8 +17,9 @@ const handleEnterSpy = jest.fn();
|
|||
|
||||
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
|
||||
let wrapper;
|
||||
const searchUrl = 'https://example.com/gitlab/groups/mygroup/-/group_members/invite_search.json';
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
const createComponent = ({ props = {}, glFeatures = {} } = {}) => {
|
||||
wrapper = mountExtended(MembersTokenSelect, {
|
||||
propsData: {
|
||||
ariaLabelledby: label,
|
||||
|
|
@ -25,6 +27,7 @@ const createComponent = ({ props = {} } = {}) => {
|
|||
placeholder,
|
||||
...props,
|
||||
},
|
||||
provide: { glFeatures, searchUrl },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -89,6 +92,25 @@ describe('MembersTokenSelect', () => {
|
|||
});
|
||||
|
||||
describe('users', () => {
|
||||
describe('when `newImplementationOfInviteMembersSearch` is enabled', () => {
|
||||
let tokenSelector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(MembersUtils, 'searchUsers').mockResolvedValue({ data: allUsers });
|
||||
createComponent({ glFeatures: { newImplementationOfInviteMembersSearch: true } });
|
||||
tokenSelector = findTokenSelector();
|
||||
});
|
||||
|
||||
it('calls the API with search parameter with whitespaces and is trimmed', async () => {
|
||||
tokenSelector.vm.$emit('text-input', ' foo@bar.com ');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(MembersUtils.searchUsers).toHaveBeenCalledWith(searchUrl, 'foo@bar.com');
|
||||
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers });
|
||||
createComponent();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { memberName, searchUsers, triggerExternalAlert } from '~/invite_members/utils/member_utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
|
|
@ -13,6 +16,25 @@ describe('Member Name', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('searchUsers', () => {
|
||||
let mockAxios;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
it('should call axios.get with correct URL and params', async () => {
|
||||
const url = 'https://example.com/gitlab/groups/mygroup/-/group_members/invite_search.json';
|
||||
const search = 'my user';
|
||||
mockAxios.onGet().replyOnce(HTTP_STATUS_OK);
|
||||
|
||||
await searchUsers(url, search);
|
||||
expect(mockAxios.history.get[0]).toEqual(
|
||||
expect.objectContaining({ url, params: { search, per_page: 20 } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trigger External Alert', () => {
|
||||
it('returns false', () => {
|
||||
expect(triggerExternalAlert()).toBe(false);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import CloseButton from '~/work_items/components/design_management/design_preview/close_button.vue';
|
||||
import { ROUTES } from '~/work_items/constants';
|
||||
|
||||
describe('Design management toolbar close button', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent() {
|
||||
wrapper = shallowMount(CloseButton, {
|
||||
stubs: {
|
||||
RouterLink: RouterLinkStub,
|
||||
Vue.use(VueRouter);
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/', name: 'workItemList', component: { template: '<div>Work items list</div>' } },
|
||||
{
|
||||
path: '/workItem',
|
||||
name: 'workItem',
|
||||
component: { template: '<div>Work items detail</div>' },
|
||||
},
|
||||
});
|
||||
}
|
||||
],
|
||||
mode: 'history',
|
||||
});
|
||||
|
||||
it('links back to designs list', async () => {
|
||||
const createComponent = () => {
|
||||
wrapper = mount(CloseButton, {
|
||||
router,
|
||||
});
|
||||
};
|
||||
|
||||
it('links back to designs list', () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlButton).attributes('to')).toEqual(ROUTES.workItem);
|
||||
expect(wrapper.findComponent(GlButton).attributes().href).toEqual('/workItem');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -402,6 +402,25 @@ describe('WorkItemDescription', () => {
|
|||
expect(findApplyTemplate().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not display a warning when a description is pre-populated in create mode', async () => {
|
||||
// Mimic component mount with a pre-populated description
|
||||
await createComponent({
|
||||
editMode: true,
|
||||
workItemId: newWorkItemId(workItemQueryResponse.data.workItem.workItemType.name),
|
||||
});
|
||||
findDescriptionTemplateListbox().vm.$emit('selectTemplate', {
|
||||
name: 'default',
|
||||
projectId: 1,
|
||||
catagory: 'catagory',
|
||||
});
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(false);
|
||||
expect(findCancelApplyTemplate().exists()).toBe(false);
|
||||
expect(findApplyTemplate().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('hides the warning when the cancel button is clicked', async () => {
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(true);
|
||||
findCancelApplyTemplate().vm.$emit('click');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ require 'spec_helper'
|
|||
NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
|
||||
|
||||
RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_category: :fleet_visibility do
|
||||
let_it_be(:user) { create(:user, :admin, username: 'test-admin') }
|
||||
let_it_be(:user_organization) { create(:organization) }
|
||||
let_it_be(:user) { create(:user, :admin, username: 'test-admin', organizations: [user_organization]) }
|
||||
|
||||
subject(:seeder) do
|
||||
described_class.new(NULL_LOGGER,
|
||||
|
|
@ -92,29 +93,46 @@ RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_categor
|
|||
end
|
||||
end
|
||||
|
||||
context 'when organization cannot be created' do
|
||||
before do
|
||||
allow_next_instance_of(::Organizations::CreateService, current_user: user, params: anything) do |service|
|
||||
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'test error'))
|
||||
end
|
||||
context 'when organization is passed to the initializer' do
|
||||
let(:other_organization) { create(:organization) }
|
||||
|
||||
subject(:seed_with_organization) do
|
||||
described_class.new(NULL_LOGGER,
|
||||
username: user.username,
|
||||
registration_prefix: registration_prefix,
|
||||
runner_count: runner_count,
|
||||
organization_id: other_organization.id
|
||||
).seed
|
||||
end
|
||||
|
||||
it 'raises RuntimeError' do
|
||||
expect { seed }.to raise_error(RuntimeError)
|
||||
it 'assigns organization_id to created entities' do
|
||||
expect { seed_with_organization }.not_to raise_error
|
||||
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(other_organization.id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag allow_organization_creation is disabled' do
|
||||
let_it_be(:default_organization) { create(:organization, :default) }
|
||||
context 'when organization is not passed to the initializer' do
|
||||
it 'assigns organization_id of the user to created entities' do
|
||||
expect { seed }.not_to raise_error
|
||||
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(user.organizations.first.id))
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(allow_organization_creation: false)
|
||||
context 'when no organization can be used' do
|
||||
let(:user_without_org) { create(:user, organizations: []) }
|
||||
|
||||
subject(:seed_without_organization) do
|
||||
described_class.new(NULL_LOGGER,
|
||||
username: user_without_org.username,
|
||||
registration_prefix: registration_prefix,
|
||||
runner_count: runner_count
|
||||
).seed
|
||||
end
|
||||
|
||||
it 'uses the default organization ID' do
|
||||
expect(::Organizations::Organization).not_to receive(:default_organization)
|
||||
expect { seed }.not_to raise_error
|
||||
expect(Group.search(registration_prefix).pluck(:organization_id)).to all(eq(default_organization.id))
|
||||
it 'fails with error' do
|
||||
expect { seed_without_organization }.to raise_error(
|
||||
"No organization found. Ensure user has an organization or pass an organization_id"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1257,37 +1257,18 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
|
|||
end
|
||||
end
|
||||
|
||||
describe '#has_protected_tag_rules_for_delete?' do
|
||||
describe '#protected_from_delete_by_tag_rules?' do
|
||||
let_it_be_with_refind(:project) { create(:project, path: 'test') }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:has_tags) { true }
|
||||
|
||||
subject { repository.has_protected_tag_rules_for_delete?(user) }
|
||||
subject { repository.protected_from_delete_by_tag_rules?(user) }
|
||||
|
||||
before do
|
||||
allow(repository).to receive(:has_tags?).and_return(has_tags)
|
||||
end
|
||||
|
||||
context 'when the project does not have tag protection rules' do
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when the user is nil' do
|
||||
let(:user) { nil }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when the project has tag protection rules' do
|
||||
let_it_be(:project) { create(:project, path: 'test') }
|
||||
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
minimum_access_level_for_delete: Gitlab::Access::OWNER
|
||||
)
|
||||
end
|
||||
|
||||
shared_examples 'checking for mutable tag protection rules' do
|
||||
context 'for admin' do
|
||||
before do
|
||||
allow(user).to receive(:can_admin_all_resources?).and_return(true)
|
||||
|
|
@ -1296,7 +1277,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
|
|||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when user has lower access level' do
|
||||
context 'when the user has a lower access level' do
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
|
@ -1310,7 +1291,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user has the same or higher access level' do
|
||||
context 'when the user meets the minimum access level' do
|
||||
before_all do
|
||||
project.add_owner(user)
|
||||
end
|
||||
|
|
@ -1318,5 +1299,79 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
|
|||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project does not have tag protection rules' do
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when the user is nil' do
|
||||
let(:user) { nil }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when the project only has mutable tag protection rules' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
minimum_access_level_for_delete: Gitlab::Access::OWNER
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking for mutable tag protection rules'
|
||||
end
|
||||
|
||||
context 'when the project has immutable tag protection rule only' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
:immutable,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when the container repository does not have tags' do
|
||||
let(:has_tags) { false }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when the feature container_registry_immutable_tags is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has both immutable and mutable tags' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
:immutable,
|
||||
project: project
|
||||
)
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
tag_name_pattern: 'mutable',
|
||||
minimum_access_level_for_delete: Gitlab::Access::OWNER
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when the feature container_registry_immutable_tags is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking for mutable tag protection rules'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9944,8 +9944,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
|
||||
describe '#has_container_registry_protected_tag_rules?' do
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
let(:include_immutable) { true }
|
||||
|
||||
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER) }
|
||||
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER, include_immutable: include_immutable) }
|
||||
|
||||
it 'returns false when there is no matching tag protection rule' do
|
||||
create(:container_registry_protection_tag_rule,
|
||||
|
|
@ -9957,25 +9958,100 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
expect(subject).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns true when there exists a matching tag protection rule' do
|
||||
it 'returns true when there is a matching tag protection rule' do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
minimum_access_level_for_push: :maintainer,
|
||||
minimum_access_level_for_delete: :admin
|
||||
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
|
||||
minimum_access_level_for_delete: Gitlab::Access::ADMIN
|
||||
)
|
||||
|
||||
expect(subject).to eq(true)
|
||||
end
|
||||
|
||||
it 'memoizes the call' do
|
||||
context 'with immutable tag rules only' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, project: project)
|
||||
end
|
||||
|
||||
context 'when include_immutable is true' do
|
||||
let(:include_immutable) { true }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when include_immutable is false' do
|
||||
let(:include_immutable) { false }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with both mutable and immutable tag rules' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, project: project)
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
tag_name_pattern: 'mutable',
|
||||
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
|
||||
minimum_access_level_for_delete: Gitlab::Access::ADMIN
|
||||
)
|
||||
end
|
||||
|
||||
context 'when include_immutable is true' do
|
||||
let(:include_immutable) { true }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when include_immutable is false' do
|
||||
let(:include_immutable) { false }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
it 'memoizes calls with the same parameters' do
|
||||
allow(project.container_registry_protection_tag_rules).to receive(:for_actions_and_access).and_call_original
|
||||
|
||||
2.times do
|
||||
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer)
|
||||
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer, include_immutable: true)
|
||||
end
|
||||
|
||||
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer).once
|
||||
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer, include_immutable: true).once
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_container_registry_immutable_tag_rules?' do
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
|
||||
subject { project.has_container_registry_immutable_tag_rules? }
|
||||
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, project: project)
|
||||
end
|
||||
|
||||
context 'when there is no immutable tag rule' do
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when there is an immutable tag rule' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, tag_name_pattern: 'immutable', project: project)
|
||||
end
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
it 'memoizes calls with the same parameters' do
|
||||
allow(project.container_registry_protection_tag_rules).to receive(:immutable).and_call_original
|
||||
|
||||
2.times do
|
||||
project.has_container_registry_immutable_tag_rules?
|
||||
end
|
||||
|
||||
expect(project.container_registry_protection_tag_rules).to have_received(:immutable).once
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,57 @@ RSpec.describe ContainerRepositoryPolicy, feature_category: :container_registry
|
|||
allow(container_repository).to receive(:has_tags?).and_return(has_tags)
|
||||
end
|
||||
|
||||
context 'when the project has tag protection rules' do
|
||||
context 'when the project has an immutable tag protection rule' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
:immutable,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the container repository has tags' do
|
||||
let(:has_tags) { true }
|
||||
|
||||
[:owner, :maintainer, :developer].each do |user_role|
|
||||
context "when the user is #{user_role}" do
|
||||
before do
|
||||
project.send(:"add_#{user_role}", user)
|
||||
end
|
||||
|
||||
it { expect_disallowed(:destroy_container_image) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current user is an admin', :enable_admin_mode do
|
||||
let(:user) { build_stubbed(:admin) }
|
||||
|
||||
it { expect_disallowed(:destroy_container_image) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the container repository does not have tags' do
|
||||
let(:has_tags) { false }
|
||||
|
||||
[:owner, :maintainer, :developer].each do |user_role|
|
||||
context "when the user is #{user_role}" do
|
||||
before do
|
||||
project.send(:"add_#{user_role}", user)
|
||||
end
|
||||
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current user is an admin', :enable_admin_mode do
|
||||
let(:user) { build_stubbed(:admin) }
|
||||
|
||||
it { expect_allowed(:destroy_container_image) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has a mutable tag protection rule' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
|
|
|
|||
|
|
@ -29,4 +29,91 @@ RSpec.describe Groups::GroupMembersController, feature_category: :groups_and_pro
|
|||
|
||||
it_behaves_like 'request_accessable'
|
||||
end
|
||||
|
||||
describe 'GET /groups/*group_id/-/group_members/invite_search.json' do
|
||||
subject(:request) do
|
||||
get invite_search_group_group_members_path(membershipable, params: params, format: :json)
|
||||
end
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
let_it_be(:regular_user) { create(:user) }
|
||||
let_it_be(:admin_user) { create(:user, :admin) }
|
||||
let_it_be(:banned_user) { create(:user, :banned) }
|
||||
let_it_be(:blocked_user) { create(:user, :blocked) }
|
||||
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
|
||||
let_it_be(:external_user) { create(:user, :external) }
|
||||
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
|
||||
let_it_be(:omniauth_user) { create(:omniauth_user) }
|
||||
let_it_be(:internal_user) { Users::Internal.alert_bot }
|
||||
let_it_be(:project_bot_user) { create(:user, :project_bot) }
|
||||
let_it_be(:service_account_user) { create(:user, :service_account) }
|
||||
|
||||
let(:searchable_users) do
|
||||
[
|
||||
user,
|
||||
regular_user,
|
||||
admin_user,
|
||||
external_user,
|
||||
unconfirmed_user,
|
||||
omniauth_user,
|
||||
service_account_user
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when user has permission to manage group members' do
|
||||
before_all do
|
||||
membershipable.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns searchable users' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
|
||||
end
|
||||
|
||||
context 'for search param' do
|
||||
let(:params) { { search: search } }
|
||||
|
||||
context 'with empty string' do
|
||||
let(:search) { '' }
|
||||
|
||||
it 'returns searchable users' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a user's name" do
|
||||
let(:search) { regular_user.name }
|
||||
|
||||
it 'returns users that match the name' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to contain_exactly(regular_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission to manage group members' do
|
||||
before_all do
|
||||
membershipable.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns 403 forbidden' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ require_relative '../concerns/membership_actions_shared_examples'
|
|||
|
||||
RSpec.describe Projects::ProjectMembersController, feature_category: :groups_and_projects do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:membershipable) { create(:project, :public, namespace: create(:group, :public)) }
|
||||
let_it_be(:membershipable) { create(:project, :public, namespace: create(:group, :public), creator: user) }
|
||||
|
||||
let(:membershipable_path) { project_path(membershipable) }
|
||||
|
||||
|
|
@ -20,4 +20,96 @@ RSpec.describe Projects::ProjectMembersController, feature_category: :groups_and
|
|||
|
||||
it_behaves_like 'request_accessable'
|
||||
end
|
||||
|
||||
describe 'GET /*namespace_id/:project_id/-/project_members/invite_search.json' do
|
||||
subject(:request) do
|
||||
get invite_search_namespace_project_project_members_path(
|
||||
namespace_id: membershipable.namespace,
|
||||
project_id: membershipable,
|
||||
params: params,
|
||||
format: :json
|
||||
)
|
||||
end
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
let_it_be(:regular_user) { create(:user) }
|
||||
let_it_be(:admin_user) { create(:user, :admin) }
|
||||
let_it_be(:banned_user) { create(:user, :banned) }
|
||||
let_it_be(:blocked_user) { create(:user, :blocked) }
|
||||
let_it_be(:ldap_blocked_user) { create(:user, :ldap_blocked) }
|
||||
let_it_be(:external_user) { create(:user, :external) }
|
||||
let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) }
|
||||
let_it_be(:omniauth_user) { create(:omniauth_user) }
|
||||
let_it_be(:internal_user) { Users::Internal.alert_bot }
|
||||
let_it_be(:project_bot_user) { create(:user, :project_bot) }
|
||||
let_it_be(:service_account_user) { create(:user, :service_account) }
|
||||
|
||||
let(:searchable_users) do
|
||||
[
|
||||
user,
|
||||
regular_user,
|
||||
admin_user,
|
||||
external_user,
|
||||
unconfirmed_user,
|
||||
omniauth_user,
|
||||
service_account_user
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when user has permission to manage project members' do
|
||||
before_all do
|
||||
membershipable.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns searchable users' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
|
||||
end
|
||||
|
||||
context 'for search param' do
|
||||
let(:params) { { search: search } }
|
||||
|
||||
context 'with empty string' do
|
||||
let(:search) { '' }
|
||||
|
||||
it 'returns searchable users' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to match_array(searchable_users.map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a user's name" do
|
||||
let(:search) { regular_user.name }
|
||||
|
||||
it 'returns users that match the name' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.pluck('id')).to contain_exactly(regular_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission to manage project members' do
|
||||
before_all do
|
||||
membershipable.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns 404 not_found' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -185,4 +185,14 @@ EOS
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
def simulate_post_receive(project, branch_name, identifier)
|
||||
oldrev = project.repository.commit(branch_name).sha
|
||||
|
||||
yield
|
||||
|
||||
newrev = project.repository.commit(branch_name).sha
|
||||
changes = Base64.encode64("#{oldrev} #{newrev} refs/heads/#{branch_name}")
|
||||
Repositories::PostReceiveWorker.new.perform("project-#{project.id}", identifier, changes)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'gitlab:seed:runner_fleet rake task', :silence_stdout, feature_ca
|
|||
|
||||
context 'with admin username', :enable_admin_mode do
|
||||
let(:username) { 'runner_fleet_seed' }
|
||||
let!(:admin) { create(:user, :admin, username: username) }
|
||||
let!(:admin) { create(:user, :admin, :with_organization, username: username) }
|
||||
|
||||
it 'performs runner fleet seed successfully' do
|
||||
expect { rake_task }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module Tooling
|
|||
class DownloadJobTrace
|
||||
DEFAULT_TRACE_MARKER = 'failure-analyzer'
|
||||
DEFAULT_MAX_ATTEMPTS = 5
|
||||
DEFAULT_RETRY_DELAY_SECONDS = 10
|
||||
DEFAULT_RETRY_DELAY_SECONDS = 20
|
||||
|
||||
def initialize(
|
||||
api_url: ENV['CI_API_V4_URL'],
|
||||
|
|
|
|||
Loading…
Reference in New Issue