Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2620cc543d
commit
23835e8cac
|
|
@ -47,3 +47,6 @@ include:
|
|||
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
|
||||
inputs:
|
||||
gem_name: "openbao_client"
|
||||
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
|
||||
inputs:
|
||||
gem_name: "gitlab-active-context"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
b621ac4ec3d3b032489d4ac65df1ffa5752a5565
|
||||
2035810e4e65a367b3483c75e40f1ce4eb726c48
|
||||
|
|
|
|||
3
Gemfile
3
Gemfile
|
|
@ -242,6 +242,9 @@ gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search
|
|||
# Used with Elasticsearch to support http keep-alive connections
|
||||
gem 'typhoeus', '~> 1.4.0', feature_category: :global_search
|
||||
|
||||
gem 'gitlab-active-context', path: 'gems/gitlab-active-context', require: 'active_context',
|
||||
feature_category: :global_search
|
||||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 2.14.3', feature_category: :markdown
|
||||
gem 'deckar01-task_list', '2.3.4', feature_category: :markdown
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ PATH
|
|||
error_tracking_open_api (1.0.0)
|
||||
typhoeus (~> 1.0, >= 1.0.1)
|
||||
|
||||
PATH
|
||||
remote: gems/gitlab-active-context
|
||||
specs:
|
||||
gitlab-active-context (0.0.1)
|
||||
zeitwerk
|
||||
|
||||
PATH
|
||||
remote: gems/gitlab-backup-cli
|
||||
specs:
|
||||
|
|
@ -2066,6 +2072,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.4, >= 3.4.9)
|
||||
gettext_i18n_rails (~> 1.13.0)
|
||||
gitaly (~> 17.5.0.pre.rc1)
|
||||
gitlab-active-context!
|
||||
gitlab-backup-cli!
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-cloud-connector (~> 0.2.5)
|
||||
|
|
|
|||
|
|
@ -567,7 +567,7 @@
|
|||
{"name":"rbs","version":"3.6.1","platform":"ruby","checksum":"ed7273d018556844583d1785ac54194e67eec594d68e317d57fa90ad035532c0"},
|
||||
{"name":"rbtrace","version":"0.5.1","platform":"ruby","checksum":"e8cba64d462bfb8ba102d7be2ecaacc789247d52ac587d8003549d909cb9c5dc"},
|
||||
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
|
||||
{"name":"rdoc","version":"6.8.1","platform":"ruby","checksum":"0128002d1bfc4892bdd780940841e4ca41275f63781fd832d11bc8ba4461462c"},
|
||||
{"name":"rdoc","version":"6.9.1","platform":"ruby","checksum":"3344bf498a46b701aba70ccdd5cdfa8be37e68493984c1bf8c579f06c3442c9f"},
|
||||
{"name":"re2","version":"2.7.0","platform":"aarch64-linux","checksum":"778921298b6e8aba26a6230dd298c9b361b92e45024f81fa6aee788060fa307c"},
|
||||
{"name":"re2","version":"2.7.0","platform":"arm-linux","checksum":"d328b5286d83ae265e13b855da8e348a976f80f91b748045b52073a570577954"},
|
||||
{"name":"re2","version":"2.7.0","platform":"arm64-darwin","checksum":"7d993f27a1afac4001c539a829e2af211ced62604930c90df32a307cf74cb4a4"},
|
||||
|
|
@ -592,7 +592,7 @@
|
|||
{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"},
|
||||
{"name":"reline","version":"0.5.12","platform":"ruby","checksum":"41ab36d3fd2aaa169e99f8b82a93b9585f51130529360e24388fcccc20a055a2"},
|
||||
{"name":"reline","version":"0.6.0","platform":"ruby","checksum":"57620375dcbe56ec09bac7192bfb7460c716bbf0054dc94345ecaa5438e539d2"},
|
||||
{"name":"representable","version":"3.2.0","platform":"ruby","checksum":"cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace"},
|
||||
{"name":"request_store","version":"1.5.1","platform":"ruby","checksum":"07a204d161590789f2b1d27f9f0eadcdecd6d868cb2f03240250e1bc747df78e"},
|
||||
{"name":"responders","version":"3.0.1","platform":"ruby","checksum":"613fe28e498987f4feaa3230aa6313ca4bd5f0563a3da83511b0dd6cd8f47292"},
|
||||
|
|
@ -737,7 +737,7 @@
|
|||
{"name":"thread_safe","version":"0.3.6","platform":"ruby","checksum":"9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a"},
|
||||
{"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"},
|
||||
{"name":"tilt","version":"2.0.11","platform":"ruby","checksum":"7b180fc472cbdeb186c85d31c0f2d1e61a2c0d77e1d9fd0ca28482a9d972d6a0"},
|
||||
{"name":"timeout","version":"0.4.2","platform":"ruby","checksum":"8aca2d5ff98eb2f7a501c03f8c3622065932cc58bc58f725cd50a09e63b4cc19"},
|
||||
{"name":"timeout","version":"0.4.3","platform":"ruby","checksum":"9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e"},
|
||||
{"name":"timfel-krb5-auth","version":"0.8.3","platform":"ruby","checksum":"ab388c9d747fa3cd95baf2cc1c03253e372d8c680adcc543670f4f099854bb80"},
|
||||
{"name":"tins","version":"1.31.1","platform":"ruby","checksum":"51c4a347c25c630d310cbc2c040ffb84e266c8227f2ade881f1130ee4f9fbecf"},
|
||||
{"name":"toml-rb","version":"2.2.0","platform":"ruby","checksum":"a1e2c54ac3cc9d49861004f75f0648b3622ac03a76abe105358c31553227d9a6"},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ PATH
|
|||
error_tracking_open_api (1.0.0)
|
||||
typhoeus (~> 1.0, >= 1.0.1)
|
||||
|
||||
PATH
|
||||
remote: gems/gitlab-active-context
|
||||
specs:
|
||||
gitlab-active-context (0.0.1)
|
||||
zeitwerk
|
||||
|
||||
PATH
|
||||
remote: gems/gitlab-backup-cli
|
||||
specs:
|
||||
|
|
@ -1557,7 +1563,7 @@ GEM
|
|||
msgpack (>= 0.4.3)
|
||||
optimist (>= 3.0.0)
|
||||
rchardet (1.8.0)
|
||||
rdoc (6.8.1)
|
||||
rdoc (6.9.1)
|
||||
psych (>= 4.0.0)
|
||||
re2 (2.7.0)
|
||||
mini_portile2 (~> 2.8.5)
|
||||
|
|
@ -1587,7 +1593,7 @@ GEM
|
|||
redis (>= 4, < 6)
|
||||
regexp_parser (2.6.0)
|
||||
regexp_property_values (1.0.0)
|
||||
reline (0.5.12)
|
||||
reline (0.6.0)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
|
|
@ -1861,7 +1867,7 @@ GEM
|
|||
thread_safe (0.3.6)
|
||||
thrift (0.16.0)
|
||||
tilt (2.0.11)
|
||||
timeout (0.4.2)
|
||||
timeout (0.4.3)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
tins (1.31.1)
|
||||
sync
|
||||
|
|
@ -2094,6 +2100,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.4, >= 3.4.9)
|
||||
gettext_i18n_rails (~> 1.13.0)
|
||||
gitaly (~> 17.5.0.pre.rc1)
|
||||
gitlab-active-context!
|
||||
gitlab-backup-cli!
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-cloud-connector (~> 0.2.5)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,29 @@ export const config = {
|
|||
toReference({ __typename: 'LocalWorkItemChildIsExpanded', id: variables.id }),
|
||||
},
|
||||
},
|
||||
WorkItemDescriptionTemplateConnection: {
|
||||
fields: {
|
||||
nodes: {
|
||||
read(_, { variables }) {
|
||||
const templates = [
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
{ name: 'template 1', content: 'A template' },
|
||||
{ name: 'template 2', content: 'Another template' },
|
||||
{ name: 'template 3', content: 'Secret template omg wow' },
|
||||
{ name: 'template 4', content: 'Another another template' },
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
];
|
||||
if (variables.search) {
|
||||
return templates.filter(({ name }) => name.includes(variables.search));
|
||||
}
|
||||
if (variables.name) {
|
||||
return templates.filter(({ name }) => name === variables.name);
|
||||
}
|
||||
return templates;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Project: {
|
||||
fields: {
|
||||
projectMembers: {
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ export default class CreateMergeRequestDropdown {
|
|||
removeMessage(target) {
|
||||
const { input, message } = this.getTargetData(target);
|
||||
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
|
||||
const messageClasses = ['gl-text-subtle', 'gl-text-red-500', 'gl-text-green-500'];
|
||||
const messageClasses = ['gl-text-subtle', 'gl-text-red-500', 'gl-text-success'];
|
||||
|
||||
inputClasses.forEach((cssClass) => input.classList.remove(cssClass));
|
||||
messageClasses.forEach((cssClass) => message.classList.remove(cssClass));
|
||||
|
|
@ -520,7 +520,7 @@ export default class CreateMergeRequestDropdown {
|
|||
|
||||
this.removeMessage(target);
|
||||
input.classList.add('gl-field-success-outline');
|
||||
message.classList.add('gl-text-green-500');
|
||||
message.classList.add('gl-text-success');
|
||||
message.textContent = sprintf(__('%{text} is available'), { text });
|
||||
message.style.display = 'inline-block';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const formatGraphQLProjects = (projects) =>
|
|||
...project,
|
||||
id: getIdFromGraphQLId(id),
|
||||
name: nameWithNamespace,
|
||||
avatarLabel: nameWithNamespace,
|
||||
mergeRequestsAccessLevel: mergeRequestsAccessLevel.stringValue,
|
||||
issuesAccessLevel: issuesAccessLevel.stringValue,
|
||||
forkingAccessLevel: forkingAccessLevel.stringValue,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
<script>
|
||||
import { GlTruncateText } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue';
|
||||
|
||||
export default {
|
||||
name: 'ProjectListItemDescription',
|
||||
i18n: {
|
||||
showMore: __('Show more'),
|
||||
showLess: __('Show less'),
|
||||
},
|
||||
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
|
||||
components: {
|
||||
GlTruncateText,
|
||||
ListItemDescription,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
|
|
@ -31,19 +25,5 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-truncate-text
|
||||
v-if="showDescription"
|
||||
:lines="2"
|
||||
:mobile-lines="2"
|
||||
:show-more-text="$options.i18n.showMore"
|
||||
:show-less-text="$options.i18n.showLess"
|
||||
:toggle-button-props="$options.truncateTextToggleButtonProps"
|
||||
class="gl-mt-2 gl-max-w-88"
|
||||
>
|
||||
<div
|
||||
v-safe-html="project.descriptionHtml"
|
||||
class="md md-child-content-text-subtle gl-text-sm"
|
||||
data-testid="project-description"
|
||||
></div>
|
||||
</gl-truncate-text>
|
||||
<list-item-description v-if="showDescription" :description-html="project.descriptionHtml" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
GlAvatarLabeled,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlBadge,
|
||||
GlTooltipDirective,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { GlIcon, GlBadge, GlTooltipDirective, GlPopover, GlSprintf } from '@gitlab/ui';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
|
||||
import {
|
||||
|
|
@ -23,7 +15,6 @@ import { FEATURABLE_ENABLED } from '~/featurable/constants';
|
|||
import { __, s__ } from '~/locale';
|
||||
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
|
||||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
|
||||
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
|
||||
import {
|
||||
|
|
@ -32,6 +23,8 @@ import {
|
|||
} from '~/vue_shared/components/resource_lists/constants';
|
||||
import { deleteProject } from '~/rest_api';
|
||||
import { createAlert } from '~/alert';
|
||||
import ListItem from '~/vue_shared/components/resource_lists/list_item.vue';
|
||||
import ListItemStat from '~/vue_shared/components/resource_lists/list_item_stat.vue';
|
||||
|
||||
const MAX_TOPICS_TO_SHOW = 3;
|
||||
const MAX_TOPIC_TITLE_LENGTH = 15;
|
||||
|
|
@ -45,8 +38,6 @@ export default {
|
|||
topics: __('Topics'),
|
||||
topicsPopoverTargetText: __('+ %{count} more'),
|
||||
moreTopics: __('More topics'),
|
||||
[TIMESTAMP_TYPE_CREATED_AT]: __('Created'),
|
||||
[TIMESTAMP_TYPE_UPDATED_AT]: __('Updated'),
|
||||
project: __('Project'),
|
||||
deleteErrorMessage: s__(
|
||||
'Projects|An error occurred deleting the project. Please refresh the page to try again.',
|
||||
|
|
@ -54,13 +45,12 @@ export default {
|
|||
ciCatalogBadge: s__('CiCatalog|CI/CD Catalog project'),
|
||||
},
|
||||
components: {
|
||||
GlAvatarLabeled,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlBadge,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
TimeAgoTooltip,
|
||||
ListItem,
|
||||
ListItemStat,
|
||||
DeleteModal,
|
||||
ProjectListItemDescription,
|
||||
ProjectListItemActions,
|
||||
|
|
@ -212,12 +202,6 @@ export default {
|
|||
hasActionDelete() {
|
||||
return this.project.availableActions?.includes(ACTION_DELETE);
|
||||
},
|
||||
timestampText() {
|
||||
return this.$options.i18n[this.timestampType];
|
||||
},
|
||||
timestamp() {
|
||||
return this.project[this.timestampType];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
topicPath(topic) {
|
||||
|
|
@ -255,170 +239,129 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="projects-list-item gl-border-b gl-flex gl-py-5">
|
||||
<div class="gl-grow md:gl-flex">
|
||||
<div class="gl-flex gl-grow gl-items-start">
|
||||
<div v-if="showProjectIcon" class="gl-mr-3 gl-flex gl-h-9 gl-shrink-0 gl-items-center">
|
||||
<gl-icon name="project" variant="subtle" />
|
||||
</div>
|
||||
<gl-avatar-labeled
|
||||
:entity-id="project.id"
|
||||
:entity-name="project.name"
|
||||
:label="project.name"
|
||||
:label-link="project.webUrl"
|
||||
:src="project.avatarUrl"
|
||||
shape="rect"
|
||||
:size="48"
|
||||
>
|
||||
<template #meta>
|
||||
<div class="gl-px-2">
|
||||
<div class="-gl-mx-2 gl-flex gl-flex-wrap gl-items-center">
|
||||
<div class="gl-px-2">
|
||||
<gl-icon
|
||||
v-if="visibility"
|
||||
v-gl-tooltip="visibilityTooltip"
|
||||
:name="visibilityIcon"
|
||||
variant="subtle"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="project.isCatalogResource" class="gl-px-2">
|
||||
<gl-badge
|
||||
icon="catalog-checkmark"
|
||||
variant="info"
|
||||
data-testid="ci-catalog-badge"
|
||||
:href="project.exploreCatalogPath"
|
||||
>{{ $options.i18n.ciCatalogBadge }}</gl-badge
|
||||
>
|
||||
</div>
|
||||
<div class="gl-px-2">
|
||||
<gl-badge
|
||||
v-if="shouldShowAccessLevel"
|
||||
class="gl-block"
|
||||
data-testid="access-level-badge"
|
||||
>{{ accessLevelLabel }}</gl-badge
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<project-list-item-description :project="project" />
|
||||
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
|
||||
<div
|
||||
class="-gl-mx-2 -gl-my-2 gl-inline-flex gl-w-full gl-flex-wrap gl-items-center gl-text-base gl-font-normal"
|
||||
>
|
||||
<span class="gl-p-2 gl-text-sm gl-text-subtle">{{ $options.i18n.topics }}:</span>
|
||||
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
|
||||
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
|
||||
{{ topicTitle(topic) }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
<template v-if="popoverTopics.length">
|
||||
<div
|
||||
:id="topicsPopoverTarget"
|
||||
class="gl-p-2 gl-text-sm gl-text-subtle"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
|
||||
<template #count>{{ popoverTopics.length }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
|
||||
<div class="-gl-mx-2 -gl-my-2 gl-text-base gl-font-normal">
|
||||
<div v-for="topic in popoverTopics" :key="topic" class="gl-inline-block gl-p-2">
|
||||
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
|
||||
{{ topicTitle(topic) }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
</div>
|
||||
</gl-popover>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</gl-avatar-labeled>
|
||||
</div>
|
||||
<div
|
||||
class="gl-mt-3 gl-shrink-0 gl-flex-col gl-items-end md:gl-mt-0 md:gl-flex md:gl-pl-0"
|
||||
:class="showProjectIcon ? 'gl-pl-12' : 'gl-pl-10'"
|
||||
<list-item
|
||||
:resource="project"
|
||||
:show-icon="showProjectIcon"
|
||||
icon-name="project"
|
||||
:timestamp-type="timestampType"
|
||||
>
|
||||
<template #avatar-meta>
|
||||
<gl-icon
|
||||
v-if="visibility"
|
||||
v-gl-tooltip="visibilityTooltip"
|
||||
:name="visibilityIcon"
|
||||
variant="subtle"
|
||||
/>
|
||||
<gl-badge
|
||||
v-if="project.isCatalogResource"
|
||||
icon="catalog-checkmark"
|
||||
variant="info"
|
||||
data-testid="ci-catalog-badge"
|
||||
:href="project.exploreCatalogPath"
|
||||
>{{ $options.i18n.ciCatalogBadge }}</gl-badge
|
||||
>
|
||||
<div class="gl-flex gl-items-center gl-gap-x-3 md:gl-h-9">
|
||||
<project-list-item-inactive-badge :project="project" />
|
||||
<gl-link
|
||||
v-gl-tooltip="$options.i18n.stars"
|
||||
:href="starsHref"
|
||||
:aria-label="$options.i18n.stars"
|
||||
class="gl-text-subtle"
|
||||
data-testid="stars-btn"
|
||||
>
|
||||
<gl-icon name="star-o" />
|
||||
<span>{{ starCount }}</span>
|
||||
</gl-link>
|
||||
<gl-link
|
||||
v-if="isForkingEnabled"
|
||||
v-gl-tooltip="$options.i18n.forks"
|
||||
:href="forksHref"
|
||||
:aria-label="$options.i18n.forks"
|
||||
class="gl-text-subtle"
|
||||
data-testid="forks-btn"
|
||||
>
|
||||
<gl-icon name="fork" />
|
||||
<span>{{ forksCount }}</span>
|
||||
</gl-link>
|
||||
<gl-link
|
||||
v-if="isMergeRequestsEnabled"
|
||||
v-gl-tooltip="$options.i18n.mergeRequests"
|
||||
:href="mergeRequestsHref"
|
||||
:aria-label="$options.i18n.mergeRequests"
|
||||
class="gl-text-subtle"
|
||||
data-testid="mrs-btn"
|
||||
>
|
||||
<gl-icon name="merge-request" />
|
||||
<span>{{ openMergeRequestsCount }}</span>
|
||||
</gl-link>
|
||||
<gl-link
|
||||
v-if="isIssuesEnabled"
|
||||
v-gl-tooltip="$options.i18n.issues"
|
||||
:href="issuesHref"
|
||||
:aria-label="$options.i18n.issues"
|
||||
class="gl-text-subtle"
|
||||
data-testid="issues-btn"
|
||||
>
|
||||
<gl-icon name="issues" />
|
||||
<span>{{ openIssuesCount }}</span>
|
||||
</gl-link>
|
||||
</div>
|
||||
<gl-badge v-if="shouldShowAccessLevel" class="gl-block" data-testid="access-level-badge">{{
|
||||
accessLevelLabel
|
||||
}}</gl-badge>
|
||||
</template>
|
||||
|
||||
<template #avatar-default>
|
||||
<project-list-item-description :project="project" />
|
||||
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
|
||||
<div
|
||||
v-if="timestamp"
|
||||
class="gl-mt-3 gl-whitespace-nowrap gl-text-sm gl-text-subtle md:-gl-mt-2"
|
||||
class="-gl-mx-2 -gl-my-2 gl-inline-flex gl-w-full gl-flex-wrap gl-items-center gl-text-base gl-font-normal"
|
||||
>
|
||||
<span>{{ timestampText }}</span>
|
||||
<time-ago-tooltip :time="timestamp" />
|
||||
<span class="gl-p-2 gl-text-sm gl-text-subtle">{{ $options.i18n.topics }}:</span>
|
||||
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
|
||||
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
|
||||
{{ topicTitle(topic) }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
<template v-if="popoverTopics.length">
|
||||
<div
|
||||
:id="topicsPopoverTarget"
|
||||
class="gl-p-2 gl-text-sm gl-text-subtle"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
|
||||
<template #count>{{ popoverTopics.length }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
|
||||
<div class="-gl-mx-2 -gl-my-2 gl-text-base gl-font-normal">
|
||||
<div v-for="topic in popoverTopics" :key="topic" class="gl-inline-block gl-p-2">
|
||||
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
|
||||
{{ topicTitle(topic) }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
</div>
|
||||
</gl-popover>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasActions" class="gl-ml-3 gl-flex gl-h-9 gl-items-center">
|
||||
</template>
|
||||
|
||||
<template #stats>
|
||||
<project-list-item-inactive-badge :project="project" />
|
||||
<list-item-stat
|
||||
:href="starsHref"
|
||||
:tooltip-text="$options.i18n.stars"
|
||||
icon-name="star-o"
|
||||
:stat="starCount"
|
||||
data-testid="stars-btn"
|
||||
/>
|
||||
<list-item-stat
|
||||
v-if="isForkingEnabled"
|
||||
:href="forksHref"
|
||||
:tooltip-text="$options.i18n.forks"
|
||||
icon-name="fork"
|
||||
:stat="forksCount"
|
||||
data-testid="forks-btn"
|
||||
/>
|
||||
<list-item-stat
|
||||
v-if="isMergeRequestsEnabled"
|
||||
:href="mergeRequestsHref"
|
||||
:tooltip-text="$options.i18n.mergeRequests"
|
||||
icon-name="merge-request"
|
||||
:stat="openMergeRequestsCount"
|
||||
data-testid="mrs-btn"
|
||||
/>
|
||||
<list-item-stat
|
||||
v-if="isIssuesEnabled"
|
||||
:href="issuesHref"
|
||||
:tooltip-text="$options.i18n.issues"
|
||||
icon-name="issues"
|
||||
:stat="openIssuesCount"
|
||||
data-testid="issues-btn"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="hasActions" #actions>
|
||||
<project-list-item-actions
|
||||
:project="project"
|
||||
@refetch="$emit('refetch')"
|
||||
@delete="onActionDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<delete-modal
|
||||
v-if="hasActionDelete"
|
||||
v-model="isDeleteModalVisible"
|
||||
:confirm-phrase="project.name"
|
||||
:is-fork="project.isForked"
|
||||
:confirm-loading="isDeleteLoading"
|
||||
:merge-requests-count="openMergeRequestsCount"
|
||||
:issues-count="openIssuesCount"
|
||||
:forks-count="forksCount"
|
||||
:stars-count="starCount"
|
||||
@primary="onDeleteModalPrimary"
|
||||
>
|
||||
<template #modal-footer
|
||||
><project-list-item-delayed-deletion-modal-footer :project="project"
|
||||
/></template>
|
||||
</delete-modal>
|
||||
</li>
|
||||
<template #footer>
|
||||
<delete-modal
|
||||
v-if="hasActionDelete"
|
||||
v-model="isDeleteModalVisible"
|
||||
:confirm-phrase="project.name"
|
||||
:is-fork="project.isForked"
|
||||
:confirm-loading="isDeleteLoading"
|
||||
:merge-requests-count="openMergeRequestsCount"
|
||||
:issues-count="openIssuesCount"
|
||||
:forks-count="forksCount"
|
||||
:stars-count="starCount"
|
||||
@primary="onDeleteModalPrimary"
|
||||
>
|
||||
<template #modal-footer
|
||||
><project-list-item-delayed-deletion-modal-footer :project="project"
|
||||
/></template>
|
||||
</delete-modal>
|
||||
</template>
|
||||
</list-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
|
@ -9,21 +9,19 @@ import {
|
|||
TIMESTAMP_TYPE_CREATED_AT,
|
||||
TIMESTAMP_TYPE_UPDATED_AT,
|
||||
} from '~/vue_shared/components/resource_lists/constants';
|
||||
import ListItemDescription from './list_item_description.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
showMore: __('Show more'),
|
||||
showLess: __('Show less'),
|
||||
[TIMESTAMP_TYPE_CREATED_AT]: __('Created'),
|
||||
[TIMESTAMP_TYPE_UPDATED_AT]: __('Updated'),
|
||||
},
|
||||
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
|
||||
components: {
|
||||
GlAvatarLabeled,
|
||||
GlIcon,
|
||||
GlTruncateText,
|
||||
ListActions,
|
||||
TimeAgoTooltip,
|
||||
ListItemDescription,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -34,7 +32,7 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
validator(resource) {
|
||||
const requiredKeys = ['id', 'avatarUrl', 'avatarLabel', 'webUrl', 'availableActions'];
|
||||
const requiredKeys = ['id', 'avatarUrl', 'avatarLabel', 'webUrl'];
|
||||
|
||||
return requiredKeys.every((key) => Object.prototype.hasOwnProperty.call(resource, key));
|
||||
},
|
||||
|
|
@ -76,7 +74,10 @@ export default {
|
|||
return this.$options.i18n[this.timestampType];
|
||||
},
|
||||
hasActions() {
|
||||
return Object.keys(this.actions).length && this.resource.availableActions?.length;
|
||||
return (
|
||||
this.$scopedSlots.actions ||
|
||||
(Object.keys(this.actions).length && this.resource.availableActions?.length)
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -105,21 +106,12 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<gl-truncate-text
|
||||
v-if="resource.descriptionHtml"
|
||||
:lines="2"
|
||||
:mobile-lines="2"
|
||||
:show-more-text="$options.i18n.showMore"
|
||||
:show-less-text="$options.i18n.showLess"
|
||||
:toggle-button-props="$options.truncateTextToggleButtonProps"
|
||||
class="gl-mt-2 gl-max-w-88"
|
||||
>
|
||||
<div
|
||||
v-safe-html="resource.descriptionHtml"
|
||||
class="md md-child-content-text-subtle gl-text-sm"
|
||||
data-testid="description"
|
||||
></div>
|
||||
</gl-truncate-text>
|
||||
<slot name="avatar-default">
|
||||
<list-item-description
|
||||
v-if="resource.descriptionHtml"
|
||||
:description-html="resource.descriptionHtml"
|
||||
/>
|
||||
</slot>
|
||||
</gl-avatar-labeled>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -138,12 +130,10 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-gl-mt-3 gl-ml-3 gl-flex gl-items-center">
|
||||
<list-actions
|
||||
v-if="hasActions"
|
||||
:actions="actions"
|
||||
:available-actions="resource.availableActions"
|
||||
/>
|
||||
<div v-if="hasActions" class="-gl-mt-3 gl-ml-3 gl-flex gl-items-center">
|
||||
<slot name="actions">
|
||||
<list-actions :actions="actions" :available-actions="resource.availableActions" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<slot name="footer"></slot>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import { GlTruncateText } from '@gitlab/ui';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
showMore: __('Show more'),
|
||||
showLess: __('Show less'),
|
||||
},
|
||||
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
|
||||
components: {
|
||||
GlTruncateText,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
descriptionHtml: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-truncate-text
|
||||
:lines="2"
|
||||
:mobile-lines="2"
|
||||
:show-more-text="$options.i18n.showMore"
|
||||
:show-less-text="$options.i18n.showLess"
|
||||
:toggle-button-props="$options.truncateTextToggleButtonProps"
|
||||
class="gl-mt-2 gl-max-w-88"
|
||||
>
|
||||
<div
|
||||
v-safe-html="descriptionHtml"
|
||||
class="md md-child-content-text-subtle gl-text-sm"
|
||||
data-testid="description"
|
||||
></div>
|
||||
</gl-truncate-text>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltipDirective, GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: { GlIcon },
|
||||
|
|
@ -20,17 +20,29 @@ export default {
|
|||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
component() {
|
||||
return this.href ? GlLink : 'div';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<component
|
||||
:is="component"
|
||||
v-gl-tooltip="tooltipText"
|
||||
:aria-label="tooltipText"
|
||||
:href="href"
|
||||
class="gl-flex gl-items-center gl-gap-x-2 gl-text-subtle"
|
||||
>
|
||||
<gl-icon :name="iconName" />
|
||||
<span class="gl-leading-1">{{ stat }}</span>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { __, s__ } from '~/locale';
|
||||
import EditedAt from '~/issues/show/components/edited.vue';
|
||||
import Tracking from '~/tracking';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
newWorkItemId,
|
||||
newWorkItemFullPath,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
markdownPreviewPath,
|
||||
} from '~/work_items/utils';
|
||||
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
||||
import workItemDescriptionTemplateQuery from '../graphql/work_item_description_template.query.graphql';
|
||||
import {
|
||||
i18n,
|
||||
NEW_WORK_ITEM_IID,
|
||||
|
|
@ -21,6 +24,7 @@ import {
|
|||
WIDGET_TYPE_DESCRIPTION,
|
||||
} from '../constants';
|
||||
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
|
||||
import WorkItemDescriptionTemplateListbox from './work_item_description_template_listbox.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -32,8 +36,9 @@ export default {
|
|||
GlFormTextarea,
|
||||
MarkdownEditor,
|
||||
WorkItemDescriptionRendered,
|
||||
WorkItemDescriptionTemplateListbox,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
|
||||
inject: ['isGroup'],
|
||||
props: {
|
||||
description: {
|
||||
|
|
@ -103,6 +108,10 @@ export default {
|
|||
id: 'work-item-description',
|
||||
name: 'work-item-description',
|
||||
},
|
||||
selectedTemplate: '',
|
||||
descriptionTemplate: null,
|
||||
appliedTemplate: '',
|
||||
showTemplateApplyWarning: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -132,6 +141,35 @@ export default {
|
|||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
descriptionTemplate: {
|
||||
query: workItemDescriptionTemplateQuery,
|
||||
skip() {
|
||||
return !this.selectedTemplate;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
name: this.selectedTemplate,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.namespace.workItemDescriptionTemplates.nodes[0] || {};
|
||||
},
|
||||
result() {
|
||||
const isDirty = this.descriptionText !== this.workItemDescription?.description;
|
||||
const isUnchangedTemplate = this.descriptionText === this.appliedTemplate;
|
||||
const hasContent = this.descriptionText !== '';
|
||||
if (!isUnchangedTemplate && (isDirty || hasContent)) {
|
||||
this.showTemplateApplyWarning = true;
|
||||
} else {
|
||||
this.applyTemplate();
|
||||
}
|
||||
},
|
||||
error(e) {
|
||||
Sentry.captureException(e);
|
||||
this.$emit('error', s__('WorkItem|Unable to find selected template.'));
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
createFlow() {
|
||||
|
|
@ -216,6 +254,12 @@ export default {
|
|||
showEditedAt() {
|
||||
return (this.taskCompletionStatus || this.lastEditedAt) && !this.editMode;
|
||||
},
|
||||
canShowDescriptionTemplateSelector() {
|
||||
return this.glFeatures.workItemsAlpha;
|
||||
},
|
||||
descriptionTemplateContent() {
|
||||
return this.descriptionTemplate?.content || '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
updateInProgress(newValue) {
|
||||
|
|
@ -223,6 +267,9 @@ export default {
|
|||
},
|
||||
editMode(newValue) {
|
||||
this.isEditing = newValue;
|
||||
this.selectedTemplate = '';
|
||||
this.appliedTemplate = '';
|
||||
this.showTemplateApplyWarning = false;
|
||||
if (newValue) {
|
||||
this.startEditing();
|
||||
}
|
||||
|
|
@ -299,6 +346,20 @@ export default {
|
|||
this.$emit('updateDraft', this.descriptionText);
|
||||
this.updateWorkItem();
|
||||
},
|
||||
handleSelectTemplate(templateName) {
|
||||
this.selectedTemplate = templateName;
|
||||
},
|
||||
applyTemplate() {
|
||||
this.appliedTemplate = this.descriptionTemplateContent;
|
||||
this.setDescriptionText(this.descriptionTemplateContent);
|
||||
this.onInput();
|
||||
this.showTemplateApplyWarning = false;
|
||||
},
|
||||
cancelApplyTemplate() {
|
||||
this.selectedTemplate = '';
|
||||
this.descriptionTemplate = null;
|
||||
this.showTemplateApplyWarning = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -309,9 +370,42 @@ export default {
|
|||
<gl-form-group
|
||||
:class="formGroupClass"
|
||||
:label="__('Description')"
|
||||
label-sr-only
|
||||
:label-sr-only="!canShowDescriptionTemplateSelector"
|
||||
label-for="work-item-description"
|
||||
>
|
||||
<work-item-description-template-listbox
|
||||
v-if="canShowDescriptionTemplateSelector"
|
||||
:full-path="fullPath"
|
||||
:template="selectedTemplate"
|
||||
@selectTemplate="handleSelectTemplate"
|
||||
/>
|
||||
<gl-alert
|
||||
v-if="showTemplateApplyWarning"
|
||||
:dismissible="false"
|
||||
variant="warning"
|
||||
class="gl-mt-2"
|
||||
data-testid="description-template-warning"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'WorkItem|Applying a template will replace the existing description. Any changes you have made will be lost.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<template #actions>
|
||||
<gl-button variant="confirm" data-testid="template-apply" @click="applyTemplate"
|
||||
>{{ s__('WorkItem|Apply template') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
category="secondary"
|
||||
class="gl-ml-3"
|
||||
data-testid="template-cancel"
|
||||
@click="cancelApplyTemplate"
|
||||
>{{ s__('WorkItem|Cancel') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-alert>
|
||||
<markdown-editor
|
||||
:value="descriptionText"
|
||||
:render-markdown-path="markdownPreviewPath"
|
||||
|
|
@ -322,6 +416,7 @@ export default {
|
|||
enable-autocomplete
|
||||
supports-quick-actions
|
||||
:autofocus="autofocus"
|
||||
:class="{ 'gl-mt-2': canShowDescriptionTemplateSelector }"
|
||||
@input="setDescriptionText"
|
||||
@keydown.meta.enter="updateWorkItem"
|
||||
@keydown.ctrl.enter="updateWorkItem"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<script>
|
||||
import { GlCollapsibleListbox, GlSkeletonLoader, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import workItemDescriptionTemplatesListQuery from '../graphql/work_item_description_templates_list.query.graphql';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemDescriptionTemplateListbox',
|
||||
components: {
|
||||
GlCollapsibleListbox,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showListbox: false,
|
||||
descriptionTemplates: [],
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
descriptionTemplates: {
|
||||
query: workItemDescriptionTemplatesListQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.namespace?.workItemDescriptionTemplates.nodes || [];
|
||||
},
|
||||
error(e) {
|
||||
Sentry.captureException(e);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$apollo.queries.descriptionTemplates.loading;
|
||||
},
|
||||
toggleText() {
|
||||
return this.template || s__('WorkItem|Choose a template');
|
||||
},
|
||||
hasTemplates() {
|
||||
return this.descriptionTemplates.length > 0;
|
||||
},
|
||||
items() {
|
||||
const listboxItems = this.descriptionTemplates.map(({ name }) => ({
|
||||
value: name,
|
||||
text: name,
|
||||
}));
|
||||
if (this.searchTerm) {
|
||||
return listboxItems.filter(({ text }) => text.includes(this.searchTerm));
|
||||
}
|
||||
return listboxItems;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSelect(item) {
|
||||
this.$emit('selectTemplate', item);
|
||||
},
|
||||
handleSearch(searchTerm) {
|
||||
this.searchTerm = searchTerm;
|
||||
},
|
||||
},
|
||||
templateDocsPath: helpPagePath('user/project/description_templates'),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-skeleton-loader v-if="loading" />
|
||||
|
||||
<gl-collapsible-listbox
|
||||
v-else-if="hasTemplates"
|
||||
:items="items"
|
||||
:toggle-text="toggleText"
|
||||
:header-text="s__('WorkItem|Select template')"
|
||||
size="small"
|
||||
:selected="template"
|
||||
:loading="loading"
|
||||
searchable
|
||||
@shown="showListbox = true"
|
||||
@hidden="showListbox = false"
|
||||
@select="handleSelect"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<p v-else data-testid="template-message">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'WorkItem|Add %{linkStart}description templates%{linkEnd} to help your contributors communicate effectively!',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.templateDocsPath">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
query workItemDescriptionTemplate($fullPath: ID!, $name: String!) {
|
||||
namespace(fullPath: $fullPath) {
|
||||
id
|
||||
workItemDescriptionTemplates(name: $name) {
|
||||
__typename
|
||||
nodes @client {
|
||||
name
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
query workItemDescriptionTemplatesList($fullPath: ID!) {
|
||||
namespace(fullPath: $fullPath) {
|
||||
id
|
||||
workItemDescriptionTemplates {
|
||||
__typename
|
||||
nodes @client {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -553,20 +553,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.projects-list-item {
|
||||
.description {
|
||||
max-height: $gl-spacing-scale-8;
|
||||
|
||||
p {
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.projects-list .description p {
|
||||
@apply gl-line-clamp-2 gl-whitespace-normal;
|
||||
margin-bottom: 0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ module AutocompleteSources
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
AUTOCOMPLETE_EXPIRES_IN = 3.minutes
|
||||
AUTOCOMPLETE_CACHED_ACTIONS = [:members, :commands, :labels, :issues].freeze
|
||||
AUTOCOMPLETE_CACHED_ACTIONS = [:members, :labels].freeze
|
||||
|
||||
included do
|
||||
before_action :set_expires_in, only: AUTOCOMPLETE_CACHED_ACTIONS
|
||||
|
|
@ -14,18 +14,7 @@ module AutocompleteSources
|
|||
private
|
||||
|
||||
def set_expires_in
|
||||
case action_name.to_sym
|
||||
when :members
|
||||
expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_members, current_user)
|
||||
when :commands
|
||||
expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_commands, current_user)
|
||||
when :labels
|
||||
expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_labels, current_user)
|
||||
when :issues
|
||||
if Feature.enabled?(:cache_autocomplete_sources_issues, current_user, type: :wip)
|
||||
expires_in AUTOCOMPLETE_EXPIRES_IN
|
||||
end
|
||||
end
|
||||
expires_in AUTOCOMPLETE_EXPIRES_IN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:issues_list_drawer, project)
|
||||
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
|
||||
push_force_frontend_feature_flag(:glql_integration, project&.glql_integration_feature_flag_enabled?)
|
||||
push_force_frontend_feature_flag(:work_items_alpha, project&.work_items_alpha_feature_flag_enabled?)
|
||||
end
|
||||
|
||||
before_action only: [:index, :show] do
|
||||
|
|
@ -63,7 +64,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
before_action only: :show do
|
||||
push_frontend_feature_flag(:work_items_beta, project&.group)
|
||||
push_force_frontend_feature_flag(:work_items_beta, project&.work_items_beta_feature_flag_enabled?)
|
||||
push_force_frontend_feature_flag(:work_items_alpha, project&.work_items_alpha_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
|
||||
push_frontend_feature_flag(:namespace_level_work_items, project&.group)
|
||||
push_frontend_feature_flag(:work_items_view_preference, current_user)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
# project_id; integer
|
||||
# target_id; integer
|
||||
# state: 'pending' (default) or 'done'
|
||||
# is_snoozed: boolean
|
||||
# type: 'Issue' or 'MergeRequest' or ['Issue', 'MergeRequest']
|
||||
#
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ class TodosFinder
|
|||
items = by_action(items)
|
||||
items = by_author(items)
|
||||
items = by_state(items)
|
||||
items = by_snoozed_status(items) if Feature.enabled?(:todos_snoozing, current_user)
|
||||
items = by_target_id(items)
|
||||
items = by_types(items)
|
||||
items = by_group(items)
|
||||
|
|
@ -103,6 +105,10 @@ class TodosFinder
|
|||
params[:action]
|
||||
end
|
||||
|
||||
def snoozed?
|
||||
params[:is_snoozed]
|
||||
end
|
||||
|
||||
def author?
|
||||
params[:author_id].present?
|
||||
end
|
||||
|
|
@ -214,6 +220,13 @@ class TodosFinder
|
|||
items
|
||||
end
|
||||
|
||||
def by_snoozed_status(items)
|
||||
return items.snoozed if snoozed?
|
||||
return items.not_snoozed if filter_pending_only?
|
||||
|
||||
items
|
||||
end
|
||||
|
||||
def by_target_id(items)
|
||||
return items if params[:target_id].blank?
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ module Resolvers
|
|||
required: false,
|
||||
description: 'State of the todo.'
|
||||
|
||||
argument :is_snoozed, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: 'Whether the to-do item is snoozed.'
|
||||
|
||||
argument :type, [Types::TodoTargetEnum],
|
||||
required: false,
|
||||
description: 'Type of the todo.'
|
||||
|
|
@ -53,6 +57,7 @@ module Resolvers
|
|||
def todo_finder_params(args)
|
||||
{
|
||||
state: args[:state],
|
||||
is_snoozed: args[:is_snoozed],
|
||||
type: args[:type],
|
||||
group_id: args[:group_id],
|
||||
author_id: args[:author_id],
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module Ci
|
|||
when :online
|
||||
title = s_("Runners|Runner is online; last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(contacted_at) }
|
||||
icon = 'status-active'
|
||||
span_class = 'gl-text-green-500'
|
||||
span_class = 'gl-text-success'
|
||||
when :never_contacted
|
||||
title = s_("Runners|Runner has never contacted this instance")
|
||||
icon = 'warning-solid'
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ module Auth
|
|||
patterns = actions_to_check.index_with { [] }
|
||||
# Admins get unrestricted access, but the registry expects to always see an array for each granted actions, so we
|
||||
# can return early here, but not any earlier.
|
||||
return patterns if user.admin?
|
||||
return patterns if user.can_admin_all_resources?
|
||||
|
||||
user_access_level = user.max_member_access_for_project(project.id)
|
||||
applicable_rules = project.container_registry_protection_tag_rules.for_actions_and_access(actions_to_check, user_access_level)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ module AwardEmojis
|
|||
from_awardable.award_emoji.find_each do |award|
|
||||
new_award = award.dup
|
||||
new_award.awardable = to_awardable
|
||||
new_award.save!
|
||||
# In some instances when an awardable has a custom emoji and is being moved to a namespace where this
|
||||
# emoji does not exist the save! will raise a validation exception.
|
||||
# see `AwardEmoji`: validates :name, presence: true, 'gitlab/emoji_name': true
|
||||
#
|
||||
# We can skip copying custom emoji for now: https://gitlab.com/gitlab-org/gitlab/-/issues/501193#note_2186334353
|
||||
new_award.save! if new_award.valid?
|
||||
end
|
||||
|
||||
ServiceResponse.success
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
module Todos
|
||||
class SnoozingService
|
||||
def snooze_todo(todo, snooze_until)
|
||||
if !todo.snoozed_until.nil? || todo.update(snoozed_until: snooze_until)
|
||||
if todo.update(snoozed_until: snooze_until)
|
||||
ServiceResponse.success(payload: { todo: todo })
|
||||
else
|
||||
ServiceResponse.error(message: todo.errors.full_messages)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,19 @@
|
|||
"description": "Setting to understand if a user is joining a project or not during onboarding",
|
||||
"type": "boolean"
|
||||
},
|
||||
"registration_objective": {
|
||||
"description": "Goal of registration collected during onboarding",
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"description": "User persona collected during onboarding",
|
||||
"type": "integer",
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: cache_autocomplete_sources_commands
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138226
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433168
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::global search
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: cache_autocomplete_sources_labels
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138226
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433170
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::global search
|
||||
default_enabled: true
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: cache_autocomplete_sources_members
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133454
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427452
|
||||
milestone: '16.5'
|
||||
type: development
|
||||
group: group::global search
|
||||
default_enabled: true
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: cache_autocomplete_sources_issues
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11777
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156032
|
||||
rollout_issue_url:
|
||||
milestone: '17.2'
|
||||
group: group::product planning
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: todos_snoozing
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/17712
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175163
|
||||
rollout_issue_url:
|
||||
milestone: '17.7'
|
||||
group: group::personal productivity
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -543,7 +543,10 @@ Logs stored in the S3 bucket are retained indefinitely, until the one year reten
|
|||
To gain read only access to the S3 bucket with your application logs:
|
||||
|
||||
1. Open a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650) with the title `Customer Log Access`.
|
||||
1. In the body of the ticket, include a list of IAM Principal Amazon Resource Names (users or roles) that require access to the logs from the S3 bucket.
|
||||
1. In the body of the ticket, include a list of IAM Principal Amazon Resource Names (ARNs) that require access to the logs from the S3 bucket. The ARNs can be for users or roles.
|
||||
|
||||
NOTE:
|
||||
Specify the full ARN path without wildcards (`*`). Wildcard characters are not supported. GitLab team members can read more about the proposed feature to add wildcard support in this confidential issue: [7010](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/team/-/issues/7010).
|
||||
|
||||
GitLab provides the name of the S3 bucket. Your authorized users or roles can then access all objects in the bucket. To verify access, you can use the [AWS CLI](https://aws.amazon.com/cli/).
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ sign in.
|
|||
|
||||
### View user sign ups pending approval
|
||||
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
To view user sign ups pending approval:
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ To view user sign ups pending approval:
|
|||
|
||||
### Approve or reject a user sign up
|
||||
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
A user sign up pending approval can be approved or rejected from the **Admin** area.
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ To report abuse from other users, see [report abuse](../user/report_abuse.md). F
|
|||
|
||||
### Unblock a user
|
||||
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
A blocked user can be unblocked from the **Admin** area. To do this:
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ The user's state is set to active and they consume a
|
|||
[seat](../subscriptions/self_managed/index.md#billable-users).
|
||||
|
||||
NOTE:
|
||||
Users can also be unblocked using the [GitLab API](../api/user_moderation.md#unblock-a-user).
|
||||
Users can also be unblocked using the [GitLab API](../api/user_moderation.md#unblock-access-to-a-user).
|
||||
|
||||
The unblock option may be unavailable for LDAP users. To enable the unblock option,
|
||||
the LDAP identity first needs to be deleted:
|
||||
|
|
@ -145,15 +145,15 @@ the LDAP identity first needs to be deleted:
|
|||
1. Select the **Identities** tab.
|
||||
1. Find the LDAP provider and select **Delete**.
|
||||
|
||||
## Activate and deactivate users
|
||||
## Deactivate and reactivate users
|
||||
|
||||
GitLab administrators can deactivate and activate users.
|
||||
You should deactivate a user if they have no recent activity, and you don't want them to occupy a seat on the instance.
|
||||
GitLab administrators can deactivate and reactivate users.
|
||||
You should deactivate a user if they have no recent activity, and you do not want them to occupy a seat on the instance.
|
||||
|
||||
A deactivated user:
|
||||
|
||||
- Can sign in to GitLab.
|
||||
- If a deactivated user signs in, they are automatically activated.
|
||||
- If a deactivated user signs in, they are automatically reactivated.
|
||||
- Cannot access repositories or the API.
|
||||
- Cannot use slash commands. For more information, see [slash commands](../user/project/integrations/gitlab_slack_application.md#slash-commands).
|
||||
- Does not occupy a seat. For more information, see [billable users](../subscriptions/self_managed/index.md#billable-users).
|
||||
|
|
@ -200,7 +200,7 @@ To do this:
|
|||
1. Under **Days of inactivity before deactivation**, enter the number of days before deactivation. Minimum value is 90 days.
|
||||
1. Select **Save changes**.
|
||||
|
||||
When this feature is enabled, GitLab runs a job once a day to deactivate the dormant users.
|
||||
When this feature is enabled, GitLab runs a daily job to deactivate the dormant users.
|
||||
|
||||
A maximum of 100,000 users can be deactivated per day.
|
||||
|
||||
|
|
@ -239,25 +239,25 @@ This job only runs when the `email_confirmation_setting` is set to `soft` or `ha
|
|||
|
||||
A maximum of 240,000 users can be deleted per day.
|
||||
|
||||
### Activate a user
|
||||
### Reactivate a user
|
||||
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
A deactivated user can be activated from the **Admin** area.
|
||||
You can reactivate a deactivated user from the **Admin** area.
|
||||
|
||||
To do this:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Overview > Users**.
|
||||
1. In the search box, filter by **State=Deactivated** and press <kbd>Enter</kbd>.
|
||||
1. For the user you want to activate, select the vertical ellipsis (**{ellipsis_v}**), then **Activate**.
|
||||
1. For the user you want to reactivate, select the vertical ellipsis (**{ellipsis_v}**), then **Activate**.
|
||||
|
||||
The user's state is set to active and they consume a
|
||||
[seat](../subscriptions/self_managed/index.md#billable-users).
|
||||
|
||||
NOTE:
|
||||
A deactivated user can also activate their account themselves by logging back in through the UI.
|
||||
Users can also be activated using the [GitLab API](../api/user_moderation.md#activate-a-user).
|
||||
A deactivated user can also reactivate their account themselves by logging back in through the UI.
|
||||
Users can also be reactivated using the [GitLab API](../api/user_moderation.md#reactivate-a-user).
|
||||
|
||||
## Ban and unban users
|
||||
|
||||
|
|
@ -288,7 +288,7 @@ To ban a user:
|
|||
|
||||
### Unban a user
|
||||
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
To unban a user:
|
||||
|
||||
|
|
@ -328,7 +328,7 @@ Before 15.1, additionally groups of which deleted user were the only owner among
|
|||
## Trust and untrust users
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132402) in GitLab 16.5.
|
||||
> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0.
|
||||
|
||||
You can trust and untrust users from the **Admin** area.
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=main"
|
||||
```
|
||||
|
||||
|
|
@ -223,7 +224,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch"
|
||||
```
|
||||
|
||||
|
|
@ -252,7 +254,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -18595,6 +18595,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="addonusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="addonusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="addonusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="addonusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="addonusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="addonusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="addonusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -18931,6 +18932,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="alertmanagementalerttodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="alertmanagementalerttodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="alertmanagementalerttodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="alertmanagementalerttodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="alertmanagementalerttodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="alertmanagementalerttodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="alertmanagementalerttodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -19588,6 +19590,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="autocompletedusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="autocompletedusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="autocompletedusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="autocompletedusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="autocompletedusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="autocompletedusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="autocompletedusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -22205,6 +22208,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="currentusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="currentusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="currentusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="currentusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="currentusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="currentusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="currentusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -27624,6 +27628,7 @@ Defines which user roles, users, or groups can merge into a protected branch.
|
|||
| <a id="mergerequestmilestone"></a>`milestone` | [`Milestone`](#milestone) | Milestone of the merge request. |
|
||||
| <a id="mergerequestname"></a>`name` | [`String`](#string) | Name or title of this object. |
|
||||
| <a id="mergerequestparticipants"></a>`participants` | [`MergeRequestParticipantConnection`](#mergerequestparticipantconnection) | Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes. (see [Connections](#connections)) |
|
||||
| <a id="mergerequestpoliciesoverridingapprovalsettings"></a>`policiesOverridingApprovalSettings` | [`[PolicyApprovalSettingsOverride!]`](#policyapprovalsettingsoverride) | Approval settings that are overridden by the policies for the merge request. |
|
||||
| <a id="mergerequestpolicyviolations"></a>`policyViolations` | [`PolicyViolationDetails`](#policyviolationdetails) | Policy violations reported on the merge request. |
|
||||
| <a id="mergerequestpreparedat"></a>`preparedAt` | [`Time`](#time) | Timestamp of when the merge request was prepared. |
|
||||
| <a id="mergerequestproject"></a>`project` | [`Project!`](#project) | Alias for target_project. |
|
||||
|
|
@ -28142,6 +28147,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="mergerequestassigneetodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="mergerequestassigneetodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="mergerequestassigneetodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="mergerequestassigneetodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="mergerequestassigneetodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="mergerequestassigneetodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="mergerequestassigneetodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -28551,6 +28557,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="mergerequestauthortodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="mergerequestauthortodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="mergerequestauthortodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="mergerequestauthortodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="mergerequestauthortodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="mergerequestauthortodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="mergerequestauthortodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -29006,6 +29013,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="mergerequestparticipanttodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="mergerequestparticipanttodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="mergerequestparticipanttodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="mergerequestparticipanttodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="mergerequestparticipanttodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="mergerequestparticipanttodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="mergerequestparticipanttodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -29434,6 +29442,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="mergerequestreviewertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="mergerequestreviewertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="mergerequestreviewertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="mergerequestreviewertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="mergerequestreviewertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="mergerequestreviewertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="mergerequestreviewertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -31326,6 +31335,18 @@ Represents policy violation for `any_merge_request` report_type.
|
|||
| <a id="policyapprovalgroupid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
|
||||
| <a id="policyapprovalgroupweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the group. |
|
||||
|
||||
### `PolicyApprovalSettingsOverride`
|
||||
|
||||
Represents the approval settings of merge request overridden by a policy.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="policyapprovalsettingsoverrideeditpath"></a>`editPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Path to edit the policy. |
|
||||
| <a id="policyapprovalsettingsoverridename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Policy name. |
|
||||
| <a id="policyapprovalsettingsoverridesettings"></a>`settings` | [`JSON!`](#json) | Overridden project approval settings. |
|
||||
|
||||
### `PolicyApproversType`
|
||||
|
||||
Multiple approvers action.
|
||||
|
|
@ -36240,6 +36261,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="usercoretodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="usercoretodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="usercoretodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="usercoretodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="usercoretodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="usercoretodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="usercoretodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
@ -43866,6 +43888,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="usertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
|
||||
| <a id="usertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
|
||||
| <a id="usertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
|
||||
| <a id="usertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
|
||||
| <a id="usertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
|
||||
| <a id="usertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
|
||||
| <a id="usertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ GET /groups/:id/protected_branches
|
|||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches"
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -122,7 +122,7 @@ GET /groups/:id/protected_branches/:name
|
|||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches/main"
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -163,8 +163,9 @@ POST /groups/:id/protected_branches
|
|||
```
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -227,8 +228,9 @@ access to the project and each group must
|
|||
allow [more granular control over protected branch access](../user/project/repository/branches/protected.md).
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -275,18 +277,18 @@ Example request:
|
|||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "main",
|
||||
"allowed_to_push": [{"access_level": 30}],
|
||||
"allowed_to_merge": [{
|
||||
"access_level": 30
|
||||
},{
|
||||
"access_level": 40
|
||||
}
|
||||
]}'
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches"
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "main",
|
||||
"allowed_to_push": [{"access_level": 30}],
|
||||
"allowed_to_merge": [{
|
||||
"access_level": 30
|
||||
},{
|
||||
"access_level": 40
|
||||
}
|
||||
]}'
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -343,8 +345,9 @@ DELETE /groups/:id/protected_branches/:name
|
|||
```
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable"
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -378,8 +381,9 @@ PATCH /groups/:id/protected_branches/:name
|
|||
```
|
||||
|
||||
```shell
|
||||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true"
|
||||
curl --request PATCH \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -412,9 +416,9 @@ To delete:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{access_level: 40}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{access_level: 40}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -438,8 +442,9 @@ Example response:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -463,8 +468,9 @@ Example response:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ Read more on [pagination](rest/index.md#pagination).
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/groups/90/ssh_certificates"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://primary.example.com/api/v4/groups/90/ssh_certificates"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
|||
|
|
@ -84,19 +84,20 @@ POST /project_aliases
|
|||
| `project_id` | integer or string | Yes | The ID or path of the project. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/project_aliases" \
|
||||
--form "project_id=1" \
|
||||
--form "name=gitlab"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/project_aliases" \
|
||||
--form "project_id=1" \
|
||||
--form "name=gitlab"
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/project_aliases" \
|
||||
--form "project_id=gitlab-org/gitlab" \
|
||||
--form "name=gitlab"
|
||||
--url "https://gitlab.example.com/api/v4/project_aliases" \
|
||||
--form "project_id=gitlab-org/gitlab" \
|
||||
--form "name=gitlab"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -123,6 +124,7 @@ DELETE /project_aliases/:name
|
|||
| `name` | string | Yes | The name of the alias. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/project_aliases/gitlab"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ Supported attributes:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
|
||||
curl --request GET \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
|
||||
```
|
||||
|
||||
If successful, returns [`200 OK`](rest/troubleshooting.md#status-codes) and the
|
||||
|
|
@ -94,7 +96,8 @@ Supported attributes:
|
|||
Example request to add pull mirroring:
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request PUT \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"enabled": true,
|
||||
|
|
@ -108,7 +111,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
Example request to remove pull mirroring:
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request PUT \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" \
|
||||
--data "enabled=false"
|
||||
```
|
||||
|
|
@ -146,7 +150,8 @@ Supported attributes:
|
|||
Example creating a project with pull mirroring:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "new_project",
|
||||
|
|
@ -160,7 +165,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
Example adding pull mirroring:
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request PUT \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id" \
|
||||
--data "mirror=true&import_url=https://username:token@gitlab.example.com/group/project.git"
|
||||
```
|
||||
|
|
@ -168,7 +174,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
Example removing pull mirroring:
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request PUT \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id" \
|
||||
--data "mirror=false"
|
||||
```
|
||||
|
|
@ -190,5 +197,7 @@ Supported attributes:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
stage: Secure
|
||||
stage: Application Security Testing
|
||||
group: Secret Detection
|
||||
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"
|
||||
---
|
||||
|
|
|
|||
|
|
@ -264,7 +264,8 @@ GET /projects/:id/snippets/:snippet_id/user_agent_detail
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ GET /projects/:id/protected_branches
|
|||
In the following example, the project ID is `5`.
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
|
||||
```
|
||||
|
||||
The following example response includes:
|
||||
|
|
@ -167,7 +168,8 @@ GET /projects/:id/protected_branches/:name
|
|||
In the following example, the project ID is `5` and branch name is `main`:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/main"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -254,7 +256,9 @@ POST /projects/:id/protected_branches
|
|||
In the following example, the project ID is `5` and branch name is `*-stable`.
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
|
||||
```
|
||||
|
||||
The example response includes:
|
||||
|
|
@ -356,7 +360,9 @@ The following example request creates a protected branch with user push access a
|
|||
The `user_id` is `2` and the `group_id` is `3`.
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=2&allowed_to_merge%5B%5D%5Bgroup_id%5D=3"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=2&allowed_to_merge%5B%5D%5Bgroup_id%5D=3"
|
||||
```
|
||||
|
||||
The following example response includes:
|
||||
|
|
@ -416,7 +422,9 @@ The deploy key must be enabled for your project and it must have write access to
|
|||
For other requirements, see [Allow deploy keys to push to a protected branch](../user/project/repository/branches/protected.md#allow-deploy-keys-to-push-to-a-protected-branch).
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push[][deploy_key_id]=1"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push[][deploy_key_id]=1"
|
||||
```
|
||||
|
||||
The following example response includes:
|
||||
|
|
@ -475,19 +483,19 @@ Example request:
|
|||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "main",
|
||||
"allowed_to_push": [
|
||||
{"access_level": 30}
|
||||
],
|
||||
"allowed_to_merge": [
|
||||
{"access_level": 30},
|
||||
{"access_level": 40}
|
||||
]
|
||||
}'
|
||||
"https://gitlab.example.com/api/v4/projects/5/protected_branches"
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "main",
|
||||
"allowed_to_push": [
|
||||
{"access_level": 30}
|
||||
],
|
||||
"allowed_to_merge": [
|
||||
{"access_level": 30},
|
||||
{"access_level": 40}
|
||||
]
|
||||
}'
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
|
||||
```
|
||||
|
||||
The following example response includes:
|
||||
|
|
@ -556,7 +564,9 @@ DELETE /projects/:id/protected_branches/:name
|
|||
In the following example, the project ID is `5` and branch name is `*-stable`.
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable"
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable"
|
||||
```
|
||||
|
||||
## Update a protected branch
|
||||
|
|
@ -583,7 +593,9 @@ PATCH /projects/:id/protected_branches/:name
|
|||
In the following example, the project ID is `5` and branch name is `feature-branch`.
|
||||
|
||||
```shell
|
||||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true"
|
||||
curl --request PATCH \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true"
|
||||
```
|
||||
|
||||
Elements in the `allowed_to_push`, `allowed_to_merge` and `allowed_to_unprotect` arrays should be one of `user_id`, `group_id` or
|
||||
|
|
@ -608,9 +620,9 @@ To delete:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{"access_level": 40}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{"access_level": 40}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -634,8 +646,9 @@ Example response:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -659,8 +672,9 @@ Example response:
|
|||
|
||||
```shell
|
||||
curl --header 'Content-Type: application/json' --request PATCH \
|
||||
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ DETAILS:
|
|||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
**Valid access levels**
|
||||
## Valid access levels
|
||||
|
||||
These access levels are recognized:
|
||||
|
||||
|
|
|
|||
|
|
@ -418,7 +418,8 @@ If the last tag is `v0.9.0` and the default branch is `main`, the range of commi
|
|||
included in this example is `v0.9.0..main`:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: token" \
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: token" \
|
||||
--data "version=1.0.0" \
|
||||
--url "https://gitlab.com/api/v4/projects/42/repository/changelog"
|
||||
```
|
||||
|
|
@ -427,7 +428,8 @@ To generate the data on a different branch, specify the `branch` parameter. This
|
|||
command generates data from the `foo` branch:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: token" \
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: token" \
|
||||
--data "version=1.0.0&branch=foo" \
|
||||
--url "https://gitlab.com/api/v4/projects/42/repository/changelog"
|
||||
```
|
||||
|
|
@ -443,7 +445,8 @@ curl --request POST --header "PRIVATE-TOKEN: token" \
|
|||
To store the results in a different file, use the `file` parameter:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: token" \
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: token" \
|
||||
--data "version=1.0.0&file=NEWS" \
|
||||
--url "https://gitlab.com/api/v4/projects/42/repository/changelog"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -396,7 +396,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/snippets/1"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -12,23 +12,23 @@ DETAILS:
|
|||
|
||||
Use this API to moderate user accounts. For more information, see [Moderate users](../administration/moderate_users.md).
|
||||
|
||||
## Approve a user
|
||||
## Approve access to a user
|
||||
|
||||
Approves the specified user.
|
||||
Approves access to a given user account that is pending approval.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/approve
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|----------------------|
|
||||
| `id` | integer | yes | ID of specified user |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/42/approve"
|
||||
|
|
@ -55,22 +55,23 @@ Example Responses:
|
|||
{ "message": "The user you are trying to approve is not pending approval" }
|
||||
```
|
||||
|
||||
## Reject a user
|
||||
## Reject access to a user
|
||||
|
||||
Reject the specified user that is
|
||||
[pending approval](../administration/moderate_users.md#users-pending-approval).
|
||||
Rejects access to a given user account that is pending approval.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/reject
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
- `id` (required) - ID of specified user
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/42/reject"
|
||||
|
|
@ -97,47 +98,23 @@ Example Responses:
|
|||
{ "message": "User does not have a pending request" }
|
||||
```
|
||||
|
||||
## Activate a user
|
||||
|
||||
Activate the specified user.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/activate
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|----------------------|
|
||||
| `id` | integer | yes | ID of specified user |
|
||||
|
||||
Returns:
|
||||
|
||||
- `201 OK` on success.
|
||||
- `404 User Not Found` if the user cannot be found.
|
||||
- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization.
|
||||
|
||||
## Deactivate a user
|
||||
|
||||
Deactivate the specified user.
|
||||
Deactivates a given user account. For more information on banned users, see [Activate and deactivate users](../administration/moderate_users.md#deactivate-and-reactivate-users).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/deactivate
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|----------------------|
|
||||
| `id` | integer | yes | ID of specified user |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns:
|
||||
|
||||
|
|
@ -148,23 +125,47 @@ Returns:
|
|||
- Not [dormant](../administration/moderate_users.md#automatically-deactivate-dormant-users).
|
||||
- Internal.
|
||||
|
||||
## Block a user
|
||||
## Reactivate a user
|
||||
|
||||
Block the specified user.
|
||||
Reactivates a given user account that was previously deactivated.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/activate
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns:
|
||||
|
||||
- `201 OK` on success.
|
||||
- `404 User Not Found` if the user cannot be found.
|
||||
- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization.
|
||||
|
||||
## Block access to a user
|
||||
|
||||
Blocks a given user account. For more information on banned users, see [Block and unblock users](../administration/moderate_users.md#block-and-unblock-users).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/block
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|----------------------|
|
||||
| `id` | integer | yes | ID of specified user |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns:
|
||||
|
||||
|
|
@ -174,42 +175,47 @@ Returns:
|
|||
- A user that is blocked through LDAP.
|
||||
- An internal user.
|
||||
|
||||
## Unblock a user
|
||||
## Unblock access to a user
|
||||
|
||||
Unblock the specified user.
|
||||
Unblocks a given user account that was previously blocked.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/unblock
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|----------------------|
|
||||
| `id` | integer | yes | ID of specified user |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns `201 OK` on success, `404 User Not Found` is user cannot be found or
|
||||
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
|
||||
Returns:
|
||||
|
||||
- `201 OK` on success.
|
||||
- `404 User Not Found` if user cannot be found.
|
||||
- `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
|
||||
|
||||
## Ban a user
|
||||
|
||||
Ban the specified user.
|
||||
Bans a given user account. For more information on banned users, see [Ban and unban users](../administration/moderate_users.md#ban-and-unban-users).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/ban
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
- `id` (required) - ID of specified user
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns:
|
||||
|
||||
|
|
@ -219,15 +225,21 @@ Returns:
|
|||
|
||||
## Unban a user
|
||||
|
||||
Unban the specified user. Available only for administrator.
|
||||
Unbans a given user account that was previously banned.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/unban
|
||||
```
|
||||
|
||||
Parameters:
|
||||
Supported attributes:
|
||||
|
||||
- `id` (required) - ID of specified user
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------|---------|----------|--------------------|
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Returns:
|
||||
|
||||
|
|
|
|||
|
|
@ -72,14 +72,17 @@ Details of each dependency are listed, sorted by decreasing severity of vulnerab
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422356) in GitLab 16.7 [with a flag](../../../administration/feature_flags.md) named `group_level_dependencies_filtering`. Disabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422356) in GitLab 16.10. Feature flag `group_level_dependencies_filtering` removed.
|
||||
|
||||
In the group-level dependency list you can filter by:
|
||||
You can filter the dependency list to focus on only a subset of dependencies. The dependency
|
||||
list is only available for groups.
|
||||
|
||||
You can filter by:
|
||||
|
||||
- Project
|
||||
- License
|
||||
|
||||
To filter the dependency list:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group.
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
|
||||
1. Select **Secure > Dependency list**.
|
||||
1. Select the filter bar.
|
||||
1. Select a filter, then from the dropdown list select one or more criteria.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ DETAILS:
|
|||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5708) in GitLab 17.7 [with a flag](../../../administration/feature_flags.md) named `vulnerability_management_policy_type`. Enabled by default.
|
||||
> - [Enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/467259) in GitLab 17.7.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
|
|
|
|||
|
|
@ -24,244 +24,92 @@ GitLab is [transparent](https://handbook.gitlab.com/handbook/values/#transparenc
|
|||
As GitLab Duo features mature, the documentation will be updated to clearly state
|
||||
how and where you can access these features.
|
||||
|
||||
## Generally available features
|
||||
## Working across the entire software development lifecycle
|
||||
|
||||
### GitLab Duo Chat
|
||||
To improve your workflow across the entire software development lifecycle, try these features:
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
- [GitLab Duo Chat](../gitlab_duo_chat/index.md): Write and understand code, get up to speed on the status of projects,
|
||||
and learn about GitLab by asking your questions in a chat window.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=ZQBAuf-CTAY&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [Self-Hosted Models](../../administration/self_hosted_models/index.md): Host the language models that power AI features in GitLab.
|
||||
Code Suggestions and Duo Chat are supported. Use GitLab model vendors or self-host a supported language model.
|
||||
- [GitLab Duo Workflow](../duo_workflow/index.md): Automate tasks and help increase productivity in your development workflow.
|
||||
- [AI Impact Dashboard](../analytics/ai_impact_analytics.md): Measure the AI effectiveness and impact on SDLC metrics.
|
||||
|
||||
- Help you write and understand code faster, get up to speed on the status of projects,
|
||||
and quickly learn about GitLab by answering your questions in a chat window.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=ZQBAuf-CTAY&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [View documentation](../gitlab_duo_chat/index.md).
|
||||
## Planning work
|
||||
|
||||
### Discussion Summary
|
||||
To improve your workflow while planning work, try these features:
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
- [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation): Generate a more in-depth issue description based on a short summary.
|
||||
- [Discussion Summary](../discussions/index.md#summarize-issue-discussions-with-duo-chat): Summarize lengthy conversations in an issue.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=IcdxLfTIUgc)
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
## Authoring code
|
||||
|
||||
- Helps everyone get up to speed by summarizing the lengthy conversations in an issue.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=IcdxLfTIUgc)
|
||||
- [View documentation](../discussions/index.md#summarize-issue-discussions-with-duo-chat).
|
||||
To improve your workflow while authoring code, try these features:
|
||||
|
||||
### Code Suggestions
|
||||
- [Code Suggestions](../project/repository/code_suggestions/index.md): Generate code and show suggestions as you type.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://youtu.be/ds7SG1wgcVM)
|
||||
- Code Explanation: Have code explained. View docs for explaining code in:
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
- Helps you write code more efficiently by generating code and showing suggestions as you type.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://youtu.be/ds7SG1wgcVM)
|
||||
- [View documentation](../project/repository/code_suggestions/index.md).
|
||||
|
||||
### Code Explanation
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
- Helps you understand the selected code by explaining it more clearly.
|
||||
- View documentation for explaining code in:
|
||||
- [The IDE](../gitlab_duo_chat/examples.md#explain-selected-code).
|
||||
- [A file](../../user/project/repository/code_explain.md).
|
||||
- [A merge request](../../user/project/merge_requests/changes.md#explain-code-in-a-merge-request).
|
||||
- [Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide): Test your code by generating tests.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [Refactor Code](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide): Improve or refactor the selected code.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [Fix Code](../gitlab_duo_chat/examples.md#fix-code-in-the-ide): Fix quality problems, like bugs or typos, in the selected code.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli): Discover or recall `git` commands.
|
||||
|
||||
### Test Generation
|
||||
## Reviewing code
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
To improve your workflow while reviewing code in merge requests, try these features:
|
||||
|
||||
- Helps catch bugs early by generating tests for the selected code.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [View documentation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide).
|
||||
- [Merge Request Summary](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes): Generate a description based on the code changes.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=CKjkVsfyFd8&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [Code Review](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code): Review proposed code changes.
|
||||
- [Code Review Summary](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review): Summarize all the comments in a review.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=Bx6Zajyuy9k&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [Merge Commit Message Generation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message): Generate commit messages.
|
||||
|
||||
### Refactor Code
|
||||
## Testing and deploying code
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
To improve your testing and deployment workflow, try these features:
|
||||
|
||||
- Improve or refactor the selected code.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [View documentation](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide).
|
||||
- [Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis): Research the root cause for a CI/CD job failure by analyzing the logs.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=MLjhVbMjFAY&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
|
||||
### Fix Code
|
||||
## Securing code
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
To improve your security, try these features:
|
||||
|
||||
- Fix quality problems such as bugs or typos in the selected code.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [View documentation](../gitlab_duo_chat/examples.md#fix-code-in-the-ide).
|
||||
- [Vulnerability Explanation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability): Learn more about vulnerabilities, how they can be exploited, and how to fix them.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [Vulnerability Resolution](../application_security/vulnerabilities/index.md#vulnerability-resolution): Generate a merge request that addresses a vulnerability.
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=VJmsw_C125E&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
|
||||
### GitLab Duo for the CLI
|
||||
## Summary of all GitLab Duo features
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- `glab duo ask` helps you discover or recall `git` commands when and where you need them.
|
||||
- [View documentation](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli).
|
||||
|
||||
### Merge Commit Message Generation
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps you merge more quickly by generating meaningful commit messages.
|
||||
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message).
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123692) in GitLab 16.2 as an [experiment](../../policy/development_stages_support.md#experiment) on GitLab.com.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/441681) and moved to GitLab Duo Chat in GitLab 17.3.
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps you determine the root cause for a CI/CD job failure by analyzing the logs.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=MLjhVbMjFAY&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [View documentation](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis).
|
||||
|
||||
### Vulnerability Explanation
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps you understand vulnerabilities, how they can be exploited, and how to fix them.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [View documentation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability).
|
||||
|
||||
### AI Impact Dashboard
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Measure the AI effectiveness and impact on SDLC metrics.
|
||||
- Visualize which metrics improved as a result of investments in AI.
|
||||
- Compare the performance of teams that are using AI against teams that are not using AI.
|
||||
- Track the progress of AI adoption.
|
||||
- [View documentation](../analytics/ai_impact_analytics.md).
|
||||
|
||||
## Beta features
|
||||
|
||||
### Self-Hosted Models
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** Self-managed
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/12972) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `ai_custom_model`. Disabled by default.
|
||||
> - [Enabled on self-managed](https://gitlab.com/groups/gitlab-org/-/epics/15176) in GitLab 17.6.
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
> - Feature flag `ai_custom_model` removed in GitLab 17.8
|
||||
|
||||
Host the language models that power AI features in GitLab. Code Suggestions and Duo Chat are supported.
|
||||
|
||||
You can use language model vendors provided by GitLab or fully manage specific language models in your self-hosted environment.
|
||||
|
||||
- Use GitLab model vendors: Connect with default external model providers, like Google Vertex AI or Anthropic, by
|
||||
using the GitLab-managed AI gateway.
|
||||
- Host your own models: Deploy and manage your own AI gateway and language models in your infrastructure,
|
||||
without depending on GitLab-provided external language providers.
|
||||
- [View documentation](../../administration/self_hosted_models/index.md).
|
||||
|
||||
### Merge Request Summary
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Beta
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps populate a merge request more quickly by generating a description based on the code changes.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=CKjkVsfyFd8&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes).
|
||||
|
||||
### Vulnerability Resolution
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Beta
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Help resolve a vulnerability by generating a merge request that addresses it.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=VJmsw_C125E&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [View documentation](../application_security/vulnerabilities/index.md#vulnerability-resolution).
|
||||
|
||||
## Experimental features
|
||||
|
||||
### Issue Description Generation
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Experiment
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps populate an issue more quickly by generating a more in-depth description, based on a short summary you provide.
|
||||
- [View documentation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation).
|
||||
|
||||
### Code Review
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Experiment
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Automated code review of the proposed changes in your merge request.
|
||||
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code).
|
||||
|
||||
### Code Review Summary
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Experiment
|
||||
|
||||
> - Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
|
||||
- Helps make merge request handover to reviewers easier by summarizing all the comments in a merge request review.
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=Bx6Zajyuy9k&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
|
||||
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review).
|
||||
|
||||
### GitLab Duo Workflow
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Experiment
|
||||
|
||||
- Automate tasks and help increase productivity in your development workflow.
|
||||
- [View documentation](../duo_workflow/index.md).
|
||||
|
||||
## Disable GitLab Duo features for specific groups or projects or an entire instance
|
||||
|
||||
Disable GitLab Duo features by [following these instructions](turn_on_off.md).
|
||||
| Feature | Tier | Add-on | Offering | Status |
|
||||
| ------- | ---- | ------ | -------- | ------ |
|
||||
| [GitLab Duo Chat](../gitlab_duo_chat/index.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Self-Hosted Models](../../administration/self_hosted_models/index.md) | Ultimate | GitLab Duo Enterprise | Self-managed | Beta |
|
||||
| [GitLab Duo Workflow](../duo_workflow/index.md) | Ultimate | - | GitLab.com | Experiment |
|
||||
| [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment |
|
||||
| [Discussion Summary](../discussions/index.md#summarize-issue-discussions-with-duo-chat) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Code Suggestions](../project/repository/code_suggestions/index.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Code Explanation](../../user/project/repository/code_explain.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Refactor Code](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Fix Code](../gitlab_duo_chat/examples.md#fix-code-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Merge Request Summary](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | Ultimate | GitLab Duo Enterprise | GitLab.com | Beta |
|
||||
| [Code Review](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment |
|
||||
| [Code Review Summary](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment |
|
||||
| [Merge Commit Message Generation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability |
|
||||
| [Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Generally available |
|
||||
| [Vulnerability Explanation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Generally available |
|
||||
| [Vulnerability Resolution](../application_security/vulnerabilities/index.md#vulnerability-resolution) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Beta |
|
||||
| [AI Impact Dashboard](../analytics/ai_impact_analytics.md) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed | Generally available |
|
||||
|
|
|
|||
|
|
@ -264,7 +264,8 @@ When global group memberships lock is enabled:
|
|||
- Share a project with other groups.
|
||||
|
||||
NOTE:
|
||||
This limits the use of groups in other product features such as [adding a group as a Code Owner](../../project/codeowners/index.md#add-a-group-as-a-code-owner).
|
||||
You cannot set groups or subgroups as [Code Owners](../../project/codeowners/index.md).
|
||||
The Code Owners feature requires direct group memberships, which are not possible when this lock is enabled.
|
||||
|
||||
- Invite members to a project created in a group.
|
||||
|
||||
|
|
|
|||
|
|
@ -143,10 +143,22 @@ file.md @group-x @group-x/subgroup-y
|
|||
```
|
||||
|
||||
NOTE:
|
||||
You cannot set a member of a group or subgroup as a Code Owner if [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled.
|
||||
When [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled, you cannot set a group or subgroup as a Code Owner. For more information, see [Incompatibility with Global SAML group memberships lock](#incompatibility-with-global-saml-group-memberships-lock).
|
||||
|
||||
If you encounter issues, refer to [User not shown as possible approver](troubleshooting.md#user-not-shown-as-possible-approver).
|
||||
|
||||
#### Incompatibility with Global SAML group memberships lock
|
||||
|
||||
The Code Owners feature requires direct group memberships to projects.
|
||||
When the [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled,
|
||||
it prevents groups from being invited as direct members to projects. This creates an incompatibility between the two features.
|
||||
|
||||
If you enabled Global SAML group memberships lock, you can't use groups or subgroups as Code Owners.
|
||||
In this case, you have the following options:
|
||||
|
||||
- Use individual users as Code Owners instead of groups.
|
||||
- If using group-based Code Owners is a higher priority, disable the Global SAML group memberships lock.
|
||||
|
||||
#### Group inheritance and eligibility
|
||||
|
||||
```mermaid
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
|
||||
# rspec failure tracking
|
||||
.rspec_status
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
include:
|
||||
- local: gems/gem.gitlab-ci.yml
|
||||
inputs:
|
||||
gem_name: "gitlab-active-context"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
--format documentation
|
||||
--color
|
||||
--require spec_helper
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
inherit_from:
|
||||
- ../config/rubocop.yml
|
||||
|
||||
Gemfile/MissingFeatureCategory:
|
||||
Enabled: false
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Specify your gem's dependencies in active_context.gemspec
|
||||
gemspec
|
||||
|
||||
gem "rake", "~> 13.0"
|
||||
gem "activesupport"
|
||||
|
||||
group :development, :test do
|
||||
gem "rspec", "~> 3.0"
|
||||
gem "byebug"
|
||||
gem "rubocop"
|
||||
gem "rubocop-rspec"
|
||||
end
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
gitlab-active-context (0.0.1)
|
||||
zeitwerk
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionpack (8.0.0.1)
|
||||
actionview (= 8.0.0.1)
|
||||
activesupport (= 8.0.0.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actionview (8.0.0.1)
|
||||
activesupport (= 8.0.0.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activesupport (8.0.0.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.2)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.8)
|
||||
builder (3.3.0)
|
||||
byebug (11.1.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.4.1)
|
||||
diff-lcs (1.5.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.0)
|
||||
gitlab-styles (13.0.2)
|
||||
rubocop (~> 1.68.0)
|
||||
rubocop-capybara (~> 2.21.0)
|
||||
rubocop-factory_bot (~> 2.26.1)
|
||||
rubocop-graphql (~> 1.5.4)
|
||||
rubocop-performance (~> 1.21.1)
|
||||
rubocop-rails (~> 2.26.0)
|
||||
rubocop-rspec (~> 3.0.4)
|
||||
rubocop-rspec_rails (~> 2.30.0)
|
||||
hashdiff (1.1.2)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
json (2.9.0)
|
||||
language_server-protocol (3.17.0.3)
|
||||
logger (1.6.2)
|
||||
loofah (2.23.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
nokogiri (1.17.1)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.17.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.6.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
psych (5.2.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
racc (1.8.1)
|
||||
rack (3.1.8)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (8.0.0.1)
|
||||
actionpack (= 8.0.0.1)
|
||||
activesupport (= 8.0.0.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.8.1)
|
||||
psych (>= 4.0.0)
|
||||
regexp_parser (2.9.3)
|
||||
reline (0.5.12)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.3.9)
|
||||
rspec (3.13.0)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.2)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.68.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rubocop-ast (>= 1.32.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.36.2)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.21.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.26.1)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-graphql (1.5.4)
|
||||
rubocop (>= 1.50, < 2)
|
||||
rubocop-performance (1.21.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.26.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.0.5)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec_rails (2.30.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec (~> 3, >= 3.0.1)
|
||||
ruby-progressbar (1.13.0)
|
||||
securerandom (0.4.0)
|
||||
stringio (3.1.2)
|
||||
thor (1.3.2)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.11)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
zeitwerk (2.7.1)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport
|
||||
byebug
|
||||
gitlab-active-context!
|
||||
gitlab-styles
|
||||
rake (~> 13.0)
|
||||
rspec (~> 3.0)
|
||||
rspec-rails
|
||||
rubocop
|
||||
rubocop-rspec
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.23
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# GitLab Active Context
|
||||
|
||||
`ActiveContext` is a gem used for interfacing with vector stores like Elasticsearch, OpenSearch and Postgres with PGVector for storing and querying vectors.
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
||||
|
||||
## Installation
|
||||
|
||||
TODO
|
||||
|
||||
## Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
Add an initializer with the following options:
|
||||
|
||||
1. `enabled`: `true|false`. Defaults to `false`
|
||||
1. `databases`: Hash containing database configuration options
|
||||
1. `logger`: Logger. Defaults to `Logger.new($stdout)`
|
||||
|
||||
For example:
|
||||
|
||||
```ruby
|
||||
ActiveContext.configure do |config|
|
||||
config.enabled = true
|
||||
config.logger = ::Gitlab::Elasticsearch::Logger.build
|
||||
|
||||
config.databases = {
|
||||
es1: {
|
||||
adapter: 'elasticsearch',
|
||||
prefix: 'gitlab',
|
||||
options: ::Gitlab::CurrentSettings.elasticsearch_config
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
TODO
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/gem_tasks"
|
||||
require "rspec/core/rake_task"
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
|
||||
require "rubocop/rake_task"
|
||||
|
||||
RuboCop::RakeTask.new
|
||||
|
||||
task default: %i[spec rubocop]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/setup"
|
||||
require "active_context"
|
||||
|
||||
# You can add fixtures and/or initialization code here to make experimenting
|
||||
# with your gem easier. You can also use a different console, if you like.
|
||||
|
||||
require "irb"
|
||||
IRB.start(__FILE__)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
set -vx
|
||||
|
||||
bundle install
|
||||
|
||||
# Do any other automated setup that you need to do here
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "lib/active_context/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "gitlab-active-context"
|
||||
spec.version = ActiveContext::VERSION
|
||||
spec.authors = ["GitLab"]
|
||||
spec.email = ["gitlab_rubygems@gitlab.com"]
|
||||
|
||||
spec.summary = "Abstraction for indexing and searching vectors"
|
||||
spec.description = "Abstraction for indexing and searching vectors"
|
||||
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-active-context"
|
||||
spec.license = "MIT"
|
||||
spec.required_ruby_version = ">= 3.1.0"
|
||||
|
||||
spec.metadata["homepage_uri"] = spec.homepage
|
||||
|
||||
spec.files = Dir['lib/**/*.rb']
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency 'zeitwerk'
|
||||
|
||||
spec.add_development_dependency 'gitlab-styles'
|
||||
spec.add_development_dependency 'rspec-rails'
|
||||
spec.add_development_dependency 'rubocop-rspec'
|
||||
spec.add_development_dependency 'webmock'
|
||||
|
||||
spec.metadata["rubygems_mfa_required"] = "true"
|
||||
end
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "zeitwerk"
|
||||
loader = Zeitwerk::Loader.for_gem
|
||||
loader.setup
|
||||
|
||||
module ActiveContext
|
||||
def self.configure(...)
|
||||
ActiveContext::Config.configure(...)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveContext
|
||||
class Config
|
||||
CONFIG = Struct.new(:enabled, :databases, :logger)
|
||||
|
||||
class << self
|
||||
def configure(&block)
|
||||
@instance = new(block)
|
||||
end
|
||||
|
||||
def config
|
||||
@instance&.config || {}
|
||||
end
|
||||
|
||||
def enabled?
|
||||
config.enabled || false
|
||||
end
|
||||
|
||||
def databases
|
||||
config.databases || {}
|
||||
end
|
||||
|
||||
def logger
|
||||
config.logger || Logger.new($stdout)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(config_block)
|
||||
@config_block = config_block
|
||||
end
|
||||
|
||||
def config
|
||||
struct = CONFIG.new
|
||||
@config_block.call(struct)
|
||||
struct
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveContext
|
||||
VERSION = "0.0.1"
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ActiveContext do
|
||||
it "has a version number" do
|
||||
expect(ActiveContext::VERSION).not_to be_nil
|
||||
end
|
||||
|
||||
describe '.configure' do
|
||||
let(:elastic) do
|
||||
{
|
||||
es1: {
|
||||
adapter: 'elasticsearch',
|
||||
prefix: 'gitlab',
|
||||
options: { elastisearch_url: 'http://localhost:9200' }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new instance with the provided configuration block' do
|
||||
ActiveContext.configure do |config|
|
||||
config.enabled = true
|
||||
config.databases = elastic
|
||||
config.logger = ::Logger.new(nil)
|
||||
end
|
||||
|
||||
expect(ActiveContext::Config.enabled?).to be true
|
||||
expect(ActiveContext::Config.databases).to eq(elastic)
|
||||
expect(ActiveContext::Config.logger).to be_a(::Logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ActiveContext::Config do
|
||||
let(:logger) { ::Logger.new(nil) }
|
||||
let(:elastic) do
|
||||
{
|
||||
es1: {
|
||||
adapter: 'elasticsearch',
|
||||
prefix: 'gitlab',
|
||||
options: { elastisearch_url: 'http://localhost:9200' }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
described_class.configure do |config|
|
||||
config.enabled = nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.configure' do
|
||||
it 'creates a new instance with the provided configuration block' do
|
||||
described_class.configure do |config|
|
||||
config.enabled = true
|
||||
config.databases = elastic
|
||||
config.logger = logger
|
||||
end
|
||||
|
||||
expect(described_class.enabled?).to be true
|
||||
expect(described_class.databases).to eq(elastic)
|
||||
expect(described_class.logger).to eq(logger)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.enabled?' do
|
||||
context 'when enabled is not set' do
|
||||
it 'returns false' do
|
||||
expect(described_class.enabled?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enabled is set to true' do
|
||||
before do
|
||||
described_class.configure do |config|
|
||||
config.enabled = true
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(described_class.enabled?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.databases' do
|
||||
context 'when databases are not set' do
|
||||
it 'returns an empty hash' do
|
||||
expect(described_class.databases).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when databases are set' do
|
||||
before do
|
||||
described_class.configure do |config|
|
||||
config.databases = elastic
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the configured databases' do
|
||||
expect(described_class.databases).to eq(elastic)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.logger' do
|
||||
context 'when logger is not set' do
|
||||
it 'returns a default stdout logger' do
|
||||
expect(described_class.logger).to be_a(Logger)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logger is set' do
|
||||
before do
|
||||
described_class.configure do |config|
|
||||
config.logger = logger
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the configured logger' do
|
||||
expect(described_class.logger).to eq(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_context"
|
||||
require 'logger'
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Enable flags like --only-failures and --next-failure
|
||||
config.example_status_persistence_file_path = ".rspec_status"
|
||||
|
||||
# Disable RSpec exposing methods globally on `Module` and `main`
|
||||
config.disable_monkey_patching!
|
||||
|
||||
config.expect_with :rspec do |c|
|
||||
c.syntax = :expect
|
||||
end
|
||||
end
|
||||
|
|
@ -6088,6 +6088,12 @@ msgstr ""
|
|||
msgid "An error occured fetching failed jobs count"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occured when subscribing to the comment temperature updates. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occured while parsing comment temperature. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred creating the new branch."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13783,6 +13789,9 @@ msgstr ""
|
|||
msgid "Comment added to the timeline."
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment anyway"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment could not be submitted. Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23377,6 +23386,9 @@ msgstr ""
|
|||
msgid "Failed to mark this issue as a duplicate because referenced issue was not found."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to measure the comment temperature. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to move this issue because label was not found."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -42327,6 +42339,9 @@ msgstr ""
|
|||
msgid "Proceed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proceed with caution."
|
||||
msgstr ""
|
||||
|
||||
msgid "Product Analytics"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -59857,6 +59872,9 @@ msgstr ""
|
|||
msgid "Updated date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updated. Check again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62293,6 +62311,9 @@ msgstr ""
|
|||
msgid "We found your token in a public project and have automatically revoked it to protect your account."
|
||||
msgstr ""
|
||||
|
||||
msgid "We have detected that your message might be composed against %{linkStart}our guidelines%{linkEnd}. Please review our findings below:"
|
||||
msgstr ""
|
||||
|
||||
msgid "We heard back from your device. You have been authenticated."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63274,6 +63295,9 @@ msgstr ""
|
|||
msgid "WorkItem|Add"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add %{linkStart}description templates%{linkEnd} to help your contributors communicate effectively!"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63301,6 +63325,12 @@ msgstr ""
|
|||
msgid "WorkItem|Ancestor not available"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Apply template"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Applying a template will replace the existing description. Any changes you have made will be lost."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Apricot"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63352,6 +63382,9 @@ msgstr ""
|
|||
msgid "WorkItem|Child removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Choose a template"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Clear"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63679,6 +63712,9 @@ msgstr ""
|
|||
msgid "WorkItem|Select parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select template"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63856,6 +63892,9 @@ msgstr ""
|
|||
msgid "WorkItem|Type changed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Unable to find selected template."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Undo"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -48,19 +48,6 @@ RSpec.describe AutocompleteSources::ExpiresIn, feature_category: :global_search
|
|||
expect(response.headers['Cache-Control']).to eq(expected_cache_control)
|
||||
end
|
||||
end
|
||||
|
||||
context "when action is #{action} with feature flag disabled" do
|
||||
before do
|
||||
stub_feature_flags("cache_autocomplete_sources_#{action}" => false)
|
||||
end
|
||||
|
||||
it 'does not set cache-control' do
|
||||
get action
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Cache-Control']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when action is not in AUTOCOMPLETE_CACHED_ACTIONS' do
|
||||
|
|
|
|||
|
|
@ -198,11 +198,13 @@ RSpec.describe TodosFinder, feature_category: :notifications do
|
|||
let!(:todo2) { create(:todo, user: user, group: group, target: issue, state: :done, author: banned_user) }
|
||||
let!(:todo3) { create(:todo, user: user, group: group, target: issue, state: :pending) }
|
||||
let!(:todo4) { create(:todo, user: user, group: group, target: issue, state: :pending, author: banned_user) }
|
||||
let!(:todo5) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.from_now) }
|
||||
let!(:todo6) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.ago) }
|
||||
|
||||
it 'returns the expected items when no state is provided' do
|
||||
todos = finder.new(user, {}).execute
|
||||
|
||||
expect(todos).to match_array([todo3])
|
||||
expect(todos).to match_array([todo3, todo6])
|
||||
end
|
||||
|
||||
it 'returns the expected items when a state is provided' do
|
||||
|
|
@ -214,7 +216,31 @@ RSpec.describe TodosFinder, feature_category: :notifications do
|
|||
it 'returns the expected items when multiple states are provided' do
|
||||
todos = finder.new(user, { state: [:pending, :done] }).execute
|
||||
|
||||
expect(todos).to match_array([todo1, todo2, todo3])
|
||||
expect(todos).to match_array([todo1, todo2, todo3, todo5, todo6])
|
||||
end
|
||||
end
|
||||
|
||||
context 'by snoozed state' do
|
||||
let_it_be(:todo1) { create(:todo, user: user, group: group, target: issue, state: :pending) }
|
||||
let_it_be(:todo2) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.from_now) }
|
||||
let_it_be(:todo3) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.ago) }
|
||||
|
||||
it 'returns the snoozed todos only' do
|
||||
todos = finder.new(user, { is_snoozed: true }).execute
|
||||
|
||||
expect(todos).to match_array([todo2])
|
||||
end
|
||||
|
||||
context 'when todos_snoozing feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(todos_snoozing: false)
|
||||
end
|
||||
|
||||
it 'returns all pending todos' do
|
||||
todos = finder.new(user, { is_snoozed: true }).execute
|
||||
|
||||
expect(todos).to match_array([todo1, todo2, todo3])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ describe('GroupsListItem', () => {
|
|||
it('renders subgroup count', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByTestId('subgroups-count').props()).toEqual({
|
||||
expect(wrapper.findByTestId('subgroups-count').props()).toMatchObject({
|
||||
tooltipText: 'Subgroups',
|
||||
iconName: 'subgroup',
|
||||
stat: group.descendantGroupsCount.toString(),
|
||||
|
|
@ -86,7 +86,7 @@ describe('GroupsListItem', () => {
|
|||
it('renders projects count', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByTestId('projects-count').props()).toEqual({
|
||||
expect(wrapper.findByTestId('projects-count').props()).toMatchObject({
|
||||
tooltipText: 'Projects',
|
||||
iconName: 'project',
|
||||
stat: group.projectsCount.toString(),
|
||||
|
|
@ -96,7 +96,7 @@ describe('GroupsListItem', () => {
|
|||
it('renders members count', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByTestId('members-count').props()).toEqual({
|
||||
expect(wrapper.findByTestId('members-count').props()).toMatchObject({
|
||||
tooltipText: 'Direct members',
|
||||
iconName: 'users',
|
||||
stat: group.groupMembersCount.toString(),
|
||||
|
|
|
|||
|
|
@ -47,16 +47,20 @@ jest.mock('~/api/projects_api');
|
|||
describe('ProjectsListItem', () => {
|
||||
let wrapper;
|
||||
|
||||
const [{ permissions, ...project }] = convertObjectPropsToCamelCase(projects, { deep: true });
|
||||
const [{ permissions, ...mockProject }] = convertObjectPropsToCamelCase(projects, { deep: true });
|
||||
|
||||
const project = {
|
||||
...mockProject,
|
||||
accessLevel: {
|
||||
integerValue: permissions.projectAccess.accessLevel,
|
||||
},
|
||||
avatarUrl: 'avatar.jpg',
|
||||
avatarLabel: mockProject.nameWithNamespace,
|
||||
isForked: false,
|
||||
};
|
||||
|
||||
const defaultPropsData = {
|
||||
project: {
|
||||
...project,
|
||||
accessLevel: {
|
||||
integerValue: permissions.projectAccess.accessLevel,
|
||||
},
|
||||
avatarUrl: 'avatar.jpg',
|
||||
},
|
||||
project,
|
||||
};
|
||||
|
||||
const createComponent = ({ propsData = {} } = {}) => {
|
||||
|
|
@ -69,9 +73,9 @@ describe('ProjectsListItem', () => {
|
|||
};
|
||||
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
const findMergeRequestsLink = () => wrapper.findByTestId('mrs-btn');
|
||||
const findIssuesLink = () => wrapper.findByTestId('issues-btn');
|
||||
const findForksLink = () => wrapper.findByTestId('forks-btn');
|
||||
const findMergeRequestsStat = () => wrapper.findByTestId('mrs-btn');
|
||||
const findIssuesStat = () => wrapper.findByTestId('issues-btn');
|
||||
const findForksStat = () => wrapper.findByTestId('forks-btn');
|
||||
const findProjectTopics = () => wrapper.findByTestId('project-topics');
|
||||
const findPopover = () => findProjectTopics().findComponent(GlPopover);
|
||||
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
|
||||
|
|
@ -97,13 +101,13 @@ describe('ProjectsListItem', () => {
|
|||
const avatarLabeled = findAvatarLabeled();
|
||||
|
||||
expect(avatarLabeled.props()).toMatchObject({
|
||||
label: project.name,
|
||||
label: project.nameWithNamespace,
|
||||
labelLink: project.webUrl,
|
||||
});
|
||||
|
||||
expect(avatarLabeled.attributes()).toMatchObject({
|
||||
'entity-id': project.id.toString(),
|
||||
'entity-name': project.name,
|
||||
'entity-name': project.nameWithNamespace,
|
||||
src: defaultPropsData.project.avatarUrl,
|
||||
shape: 'rect',
|
||||
});
|
||||
|
|
@ -143,7 +147,7 @@ describe('ProjectsListItem', () => {
|
|||
describe('when access level is not available', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: { project },
|
||||
propsData: { project: { ...project, accessLevel: null } },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -175,13 +179,12 @@ describe('ProjectsListItem', () => {
|
|||
it('renders stars count', () => {
|
||||
createComponent();
|
||||
|
||||
const starsLink = wrapper.findByTestId('stars-btn');
|
||||
const tooltip = getBinding(starsLink.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(ProjectsListItem.i18n.stars);
|
||||
expect(starsLink.attributes('href')).toBe(`${project.webUrl}/-/starrers`);
|
||||
expect(starsLink.text()).toBe(project.starCount.toString());
|
||||
expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o');
|
||||
expect(wrapper.findByTestId('stars-btn').props()).toEqual({
|
||||
href: `${project.webUrl}/-/starrers`,
|
||||
tooltipText: 'Stars',
|
||||
iconName: 'star-o',
|
||||
stat: project.starCount.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
|
@ -233,13 +236,12 @@ describe('ProjectsListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const mergeRequestsLink = findMergeRequestsLink();
|
||||
const tooltip = getBinding(mergeRequestsLink.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(ProjectsListItem.i18n.mergeRequests);
|
||||
expect(mergeRequestsLink.attributes('href')).toBe(`${project.webUrl}/-/merge_requests`);
|
||||
expect(mergeRequestsLink.text()).toBe('5');
|
||||
expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('merge-request');
|
||||
expect(findMergeRequestsStat().props()).toEqual({
|
||||
href: `${project.webUrl}/-/merge_requests`,
|
||||
tooltipText: 'Merge requests',
|
||||
iconName: 'merge-request',
|
||||
stat: '5',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -254,7 +256,7 @@ describe('ProjectsListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(findMergeRequestsLink().exists()).toBe(false);
|
||||
expect(findMergeRequestsStat().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -262,13 +264,12 @@ describe('ProjectsListItem', () => {
|
|||
it('renders issues count', () => {
|
||||
createComponent();
|
||||
|
||||
const issuesLink = findIssuesLink();
|
||||
const tooltip = getBinding(issuesLink.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(ProjectsListItem.i18n.issues);
|
||||
expect(issuesLink.attributes('href')).toBe(`${project.webUrl}/-/issues`);
|
||||
expect(issuesLink.text()).toBe(project.openIssuesCount.toString());
|
||||
expect(issuesLink.findComponent(GlIcon).props('name')).toBe('issues');
|
||||
expect(findIssuesStat().props()).toEqual({
|
||||
href: `${project.webUrl}/-/issues`,
|
||||
tooltipText: 'Issues',
|
||||
iconName: 'issues',
|
||||
stat: project.openIssuesCount.toString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -283,7 +284,7 @@ describe('ProjectsListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(findIssuesLink().exists()).toBe(false);
|
||||
expect(findIssuesStat().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -291,13 +292,12 @@ describe('ProjectsListItem', () => {
|
|||
it('renders forks count', () => {
|
||||
createComponent();
|
||||
|
||||
const forksLink = findForksLink();
|
||||
const tooltip = getBinding(forksLink.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(ProjectsListItem.i18n.forks);
|
||||
expect(forksLink.attributes('href')).toBe(`${project.webUrl}/-/forks`);
|
||||
expect(forksLink.text()).toBe(project.openIssuesCount.toString());
|
||||
expect(forksLink.findComponent(GlIcon).props('name')).toBe('fork');
|
||||
expect(findForksStat().props()).toEqual({
|
||||
href: `${project.webUrl}/-/forks`,
|
||||
tooltipText: 'Forks',
|
||||
iconName: 'fork',
|
||||
stat: project.forksCount.toString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -320,7 +320,7 @@ describe('ProjectsListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(findForksLink().exists()).toBe(false);
|
||||
expect(findForksStat().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { GlTruncateText } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue';
|
||||
|
||||
describe('ListItemDescription', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultPropsData = {
|
||||
descriptionHtml: '<p>Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
|
||||
};
|
||||
|
||||
const createComponent = ({ propsData = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(ListItemDescription, {
|
||||
propsData: { ...defaultPropsData, ...propsData },
|
||||
});
|
||||
};
|
||||
|
||||
it('renders description', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlTruncateText).element.firstChild.innerHTML).toBe(
|
||||
defaultPropsData.descriptionHtml,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ListItem from '~/vue_shared/components/resource_lists/list_item.vue';
|
||||
import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue';
|
||||
import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
|
||||
|
|
@ -27,20 +28,21 @@ describe('ListItem', () => {
|
|||
resource,
|
||||
};
|
||||
|
||||
const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
|
||||
const createComponent = ({ propsData = {}, stubs = {}, scopedSlots = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(ListItem, {
|
||||
propsData: { ...defaultPropsData, ...propsData },
|
||||
scopedSlots: {
|
||||
'avatar-meta': '<div data-testid="avatar-meta"></div>',
|
||||
stats: '<div data-testid="stats"></div>',
|
||||
footer: '<div data-testid="footer"></div>',
|
||||
...scopedSlots,
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
};
|
||||
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
const findGroupDescription = () => wrapper.findByTestId('description');
|
||||
const findDescription = () => wrapper.findComponent(ListItemDescription);
|
||||
const findListActions = () => wrapper.findComponent(ListActions);
|
||||
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
|
||||
|
||||
|
|
@ -80,35 +82,47 @@ describe('ListItem', () => {
|
|||
expect(wrapper.findByTestId('footer').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when resource has a description', () => {
|
||||
it('renders description', () => {
|
||||
const descriptionHtml = '<p>Foo bar</p>';
|
||||
|
||||
describe('when avatar-default slot is provided', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
resource: {
|
||||
...resource,
|
||||
descriptionHtml,
|
||||
},
|
||||
},
|
||||
scopedSlots: { 'avatar-default': '<div data-testid="avatar-default"></div>' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(findGroupDescription().element.innerHTML).toBe(descriptionHtml);
|
||||
it('renders slot instead of description', () => {
|
||||
expect(wrapper.findByTestId('avatar-default').exists()).toBe(true);
|
||||
expect(findDescription().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resource does not have a description', () => {
|
||||
it('does not render description', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
resource: {
|
||||
...resource,
|
||||
descriptionHtml: null,
|
||||
},
|
||||
},
|
||||
describe('when avatar-default slot is not provided', () => {
|
||||
describe('when resource has a description', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
expect(findGroupDescription().exists()).toBe(false);
|
||||
it('renders description', () => {
|
||||
expect(findDescription().props('descriptionHtml')).toBe(
|
||||
defaultPropsData.resource.descriptionHtml,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resource does not have a description', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
resource: {
|
||||
...resource,
|
||||
descriptionHtml: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render description', () => {
|
||||
expect(findDescription().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -130,17 +144,37 @@ describe('ListItem', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when resource has available actions', () => {
|
||||
it('displays actions dropdown', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
describe('when actions prop is passed', () => {
|
||||
describe('when resource has available actions', () => {
|
||||
it('displays actions dropdown', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findListActions().props()).toMatchObject({
|
||||
actions,
|
||||
},
|
||||
availableActions: resource.availableActions,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resource does not have available actions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
resource: {
|
||||
...resource,
|
||||
availableActions: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(findListActions().props()).toMatchObject({
|
||||
actions,
|
||||
availableActions: resource.availableActions,
|
||||
it('does not display actions dropdown', () => {
|
||||
expect(findListActions().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -155,12 +189,20 @@ describe('ListItem', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when resource does not have available actions', () => {
|
||||
describe('when actions slot is provided', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
createComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
},
|
||||
scopedSlots: {
|
||||
actions: '<div data-testid="actions"></div>',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display actions dropdown', () => {
|
||||
it('renders slot instead of list actions component', () => {
|
||||
expect(wrapper.findByTestId('actions').exists()).toBe(true);
|
||||
expect(findListActions().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ListItemStat from '~/vue_shared/components/resource_lists/list_item_stat.vue';
|
||||
|
|
@ -21,13 +21,26 @@ describe('ListItemStat', () => {
|
|||
});
|
||||
};
|
||||
|
||||
it('renders stat with icon and tooltip', () => {
|
||||
it('renders stat in div with icon and tooltip', () => {
|
||||
createComponent();
|
||||
|
||||
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
|
||||
|
||||
expect(wrapper.element.tagName).toBe('DIV');
|
||||
expect(wrapper.text()).toBe(defaultPropsData.stat);
|
||||
expect(tooltip.value).toBe(defaultPropsData.tooltipText);
|
||||
expect(wrapper.findComponent(GlIcon).props('name')).toBe(defaultPropsData.iconName);
|
||||
});
|
||||
|
||||
describe('when href prop is passed', () => {
|
||||
const href = 'http://gdk.test:3000/foo/bar/-/forks`';
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ propsData: { href } });
|
||||
});
|
||||
|
||||
it('renders `GlLink` component', () => {
|
||||
expect(wrapper.findComponent(GlLink).attributes('href')).toBe(href);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { GlAlert, GlForm } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import EditedAt from '~/issues/show/components/edited.vue';
|
||||
import { updateDraft } from '~/lib/utils/autosave';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
|
|
@ -11,8 +11,10 @@ import { ENTER_KEY } from '~/lib/utils/keys';
|
|||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
|
||||
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
|
||||
import WorkItemDescriptionTemplatesListbox from '~/work_items/components/work_item_description_template_listbox.vue';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import workItemDescriptionTemplateQuery from '~/work_items/graphql/work_item_description_template.query.graphql';
|
||||
import { autocompleteDataSources, markdownPreviewPath, newWorkItemId } from '~/work_items/utils';
|
||||
import {
|
||||
updateWorkItemMutationResponse,
|
||||
|
|
@ -34,14 +36,32 @@ describe('WorkItemDescription', () => {
|
|||
const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered);
|
||||
const findEditedAt = () => wrapper.findComponent(EditedAt);
|
||||
const findConflictsAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findConflictedDescription = () => wrapper.find('[data-testid="conflicted-description"]');
|
||||
const findConflictedDescription = () => wrapper.findByTestId('conflicted-description');
|
||||
const findDescriptionTemplateListbox = () =>
|
||||
wrapper.findComponent(WorkItemDescriptionTemplatesListbox);
|
||||
const findDescriptionTemplateWarning = () => wrapper.findByTestId('description-template-warning');
|
||||
const findDescriptionTemplateWarningButton = (type) =>
|
||||
findDescriptionTemplateWarning().find(`[data-testid="template-${type}"]`);
|
||||
|
||||
const editDescription = (newText) => findMarkdownEditor().vm.$emit('input', newText);
|
||||
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
|
||||
const findSubmitButton = () => wrapper.find('[data-testid="save-description"]');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel');
|
||||
const findSubmitButton = () => wrapper.findByTestId('save-description');
|
||||
const clickCancel = () => findForm().vm.$emit('reset', new Event('reset'));
|
||||
|
||||
const successfulTemplateHandler = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
|
||||
workItemDescriptionTemplates: {
|
||||
__typename: 'WorkItemDescriptionTemplateConnection',
|
||||
nodes: [{ name: 'example', content: 'A template' }],
|
||||
},
|
||||
__typename: 'Namespace',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createComponent = async ({
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
canUpdate = true,
|
||||
|
|
@ -55,11 +75,13 @@ describe('WorkItemDescription', () => {
|
|||
workItemTypeName = workItemQueryResponse.data.workItem.workItemType.name,
|
||||
editMode = false,
|
||||
showButtonsBelowField,
|
||||
descriptionTemplateHandler = successfulTemplateHandler,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemDescription, {
|
||||
wrapper = shallowMountExtended(WorkItemDescription, {
|
||||
apolloProvider: createMockApollo([
|
||||
[workItemByIidQuery, workItemResponseHandler],
|
||||
[updateWorkItemMutation, mutationHandler],
|
||||
[workItemDescriptionTemplateQuery, descriptionTemplateHandler],
|
||||
]),
|
||||
propsData: {
|
||||
fullPath: 'test-project-path',
|
||||
|
|
@ -72,6 +94,9 @@ describe('WorkItemDescription', () => {
|
|||
},
|
||||
provide: {
|
||||
isGroup,
|
||||
glFeatures: {
|
||||
workItemsAlpha: true,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
GlAlert,
|
||||
|
|
@ -269,6 +294,73 @@ describe('WorkItemDescription', () => {
|
|||
expect(wrapper.emitted('updateWorkItem')).toEqual([[{ clearDraft: expect.any(Function) }]]);
|
||||
});
|
||||
|
||||
describe('description templates', () => {
|
||||
it('displays the description template selection listbox', async () => {
|
||||
await createComponent({ isEditing: true });
|
||||
expect(findDescriptionTemplateListbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('selecting a template successfully', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
isEditing: true,
|
||||
workItemId: newWorkItemId(workItemQueryResponse.data.workItem.workItemType.name),
|
||||
});
|
||||
findDescriptionTemplateListbox().vm.$emit('selectTemplate', 'example');
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('queries for the template content when a template is selected', () => {
|
||||
expect(successfulTemplateHandler).toHaveBeenCalledWith({
|
||||
name: 'example',
|
||||
fullPath: 'test-project-path',
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a warning when a description template is selected', () => {
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(true);
|
||||
expect(findDescriptionTemplateWarningButton('cancel').exists()).toBe(true);
|
||||
expect(findDescriptionTemplateWarningButton('apply').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the warning when the cancel button is clicked', async () => {
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(true);
|
||||
findDescriptionTemplateWarningButton('cancel').vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('applies the template when the apply button is clicked', async () => {
|
||||
findDescriptionTemplateWarningButton('apply').vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findMarkdownEditor().props('value')).toBe('A template');
|
||||
});
|
||||
|
||||
it('hides the warning when the template is applied', async () => {
|
||||
findDescriptionTemplateWarningButton('apply').vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findDescriptionTemplateWarning().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selecting a template unsuccessfully', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
isEditing: true,
|
||||
descriptionTemplateHandler: jest.fn().mockRejectedValue(new Error()),
|
||||
});
|
||||
findDescriptionTemplateListbox().vm.$emit('selectTemplate', 'example');
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('emits an error event', () => {
|
||||
expect(wrapper.emitted('error')).toEqual([['Unable to find selected template.']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when description has conflicts', () => {
|
||||
beforeEach(async () => {
|
||||
const workItemResponseHandler = jest
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlCollapsibleListbox, GlSkeletonLoader, GlLink } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemDescriptionTemplateListbox from '~/work_items/components/work_item_description_template_listbox.vue';
|
||||
import descriptionTemplatesListQuery from '~/work_items/graphql/work_item_description_templates_list.query.graphql';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const mockTemplatesList = [
|
||||
{ name: 'template 1', __typename: 'WorkItemDescriptionTemplate' },
|
||||
{ name: 'template 2', __typename: 'WorkItemDescriptionTemplate' },
|
||||
{ name: 'template 3', __typename: 'WorkItemDescriptionTemplate' },
|
||||
{ name: 'template 4', __typename: 'WorkItemDescriptionTemplate' },
|
||||
];
|
||||
|
||||
const mockDescriptionTemplatesResult = {
|
||||
data: {
|
||||
namespace: {
|
||||
__typename: 'Namespace',
|
||||
id: 'gid://gitlab/Project/1',
|
||||
workItemDescriptionTemplates: {
|
||||
__typename: 'WorkItemDescriptionTemplateConnection',
|
||||
nodes: mockTemplatesList,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockEmptyDescriptionTemplatesResult = {
|
||||
data: {
|
||||
namespace: {
|
||||
__typename: 'Namespace',
|
||||
id: 'gid://gitlab/Project/1',
|
||||
workItemDescriptionTemplates: {
|
||||
__typename: 'WorkItemDescriptionTemplateConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('WorkItemDescriptionTemplateListbox', () => {
|
||||
let wrapper;
|
||||
let handler;
|
||||
|
||||
const createComponent = ({ template, templatesResult = mockDescriptionTemplatesResult } = {}) => {
|
||||
handler = jest.fn().mockResolvedValue(templatesResult);
|
||||
wrapper = mountExtended(WorkItemDescriptionTemplateListbox, {
|
||||
apolloProvider: createMockApollo([[descriptionTemplatesListQuery, handler]]),
|
||||
propsData: {
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
template,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findTemplateMessage = () => wrapper.findByTestId('template-message');
|
||||
const findTemplateMessageLink = () => wrapper.findComponent(GlLink);
|
||||
|
||||
it('displays a skeleton loader', () => {
|
||||
createComponent();
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when the templates have been fetched', () => {
|
||||
it('does not display a skeleton loader', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
});
|
||||
describe('and there are templates to display', () => {
|
||||
describe('and there is no template already selected', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders a collapsible-listbox component', () => {
|
||||
expect(findListbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays "Choose a template" by default', () => {
|
||||
expect(findListbox().text()).toContain('Choose a template');
|
||||
});
|
||||
|
||||
it('displays a header in the listbox that says "Select template"', () => {
|
||||
expect(findListbox().text()).toContain('Select template');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is already a template selected', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
template: mockTemplatesList[0].name,
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays the template name in the listbox', () => {
|
||||
expect(findListbox().text()).toContain(mockTemplatesList[0].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the listbox is opened', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findListbox().vm.$emit('shown');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('displays a list of templates', () => {
|
||||
const text = findListbox().text();
|
||||
for (const template of mockTemplatesList) {
|
||||
expect(text).toContain(template.name);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows searching to narrow down results', async () => {
|
||||
// only matches 'template 4'
|
||||
findListbox().vm.$emit('search', '4');
|
||||
await nextTick();
|
||||
expect(findListbox().props('items')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a template is selected from the list', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findListbox().vm.$emit('shown');
|
||||
findListbox().vm.$emit('select', mockTemplatesList[0]);
|
||||
});
|
||||
|
||||
it('emits the selected template', () => {
|
||||
expect(wrapper.emitted('selectTemplate')).toEqual([[mockTemplatesList[0]]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('but there are no templates to display', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ templatesResult: mockEmptyDescriptionTemplatesResult });
|
||||
await waitForPromises();
|
||||
});
|
||||
it('displays a message about adding description templates', () => {
|
||||
expect(findTemplateMessage().text()).toMatchInterpolatedText(
|
||||
'Add description templates to help your contributors communicate effectively!',
|
||||
);
|
||||
});
|
||||
it('displays a link to the docs', () => {
|
||||
expect(findTemplateMessageLink().attributes('href')).toBe(
|
||||
'/help/user/project/description_templates',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -120,6 +120,32 @@ RSpec.describe Resolvers::TodosResolver, feature_category: :notifications do
|
|||
|
||||
expect(todos).to contain_exactly(todo4, todo5)
|
||||
end
|
||||
|
||||
context 'when filtering by is_snoozed' do
|
||||
let_it_be(:new_user) { create(:user) }
|
||||
|
||||
let_it_be(:todo1) { create(:todo, user: new_user, project: project) }
|
||||
let_it_be(:todo2) { create(:todo, user: new_user, snoozed_until: 1.month.from_now, project: project) }
|
||||
let_it_be(:todo3) { create(:todo, user: new_user, snoozed_until: 1.hour.from_now, project: project) }
|
||||
|
||||
it 'only returns snoozed todos' do
|
||||
todos = resolve_todos(args: { is_snoozed: true, sort: 'CREATED_ASC' }, context: { current_user: new_user })
|
||||
|
||||
expect(todos.items).to eq([todo2, todo3])
|
||||
end
|
||||
|
||||
context 'when todos_snoozing feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(todos_snoozing: false)
|
||||
end
|
||||
|
||||
it 'ignores the is_snoozed filter' do
|
||||
todos = resolve_todos(args: { is_snoozed: true, sort: 'CREATED_ASC' }, context: { current_user: new_user })
|
||||
|
||||
expect(todos.items).to eq([todo1, todo2, todo3])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sort is provided' do
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ merge_requests:
|
|||
- applicable_post_merge_approval_rules
|
||||
- requested_changes
|
||||
- scan_result_policy_reads_through_violations
|
||||
- security_policies_through_violations
|
||||
- scan_result_policy_reads_through_approval_rules
|
||||
- running_scan_result_policy_violations
|
||||
- failed_scan_result_policy_violations
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
let(:step_url) { '_some_string_' }
|
||||
let(:email_opt_in) { true }
|
||||
let(:registration_type) { 'free' }
|
||||
let(:registration_objective) { 0 }
|
||||
let(:glm_source) { 'glm_source' }
|
||||
let(:glm_content) { 'glm_content' }
|
||||
let(:joining_project) { true }
|
||||
|
|
@ -29,7 +30,8 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
glm_source: glm_source,
|
||||
glm_content: glm_content,
|
||||
joining_project: joining_project,
|
||||
role: role
|
||||
role: role,
|
||||
registration_objective: registration_objective
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -99,6 +101,34 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'for registration_objective' do
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
registration_objective: registration_objective
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
|
||||
context "when 'registration_objective' is invalid" do
|
||||
let(:registration_objective) { [] }
|
||||
|
||||
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
end
|
||||
|
||||
context "when 'registration_objective' is invalid integer" do
|
||||
let(:registration_objective) { 10 }
|
||||
|
||||
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
end
|
||||
|
||||
context "when 'registration_objective' is invalid string" do
|
||||
let(:registration_objective) { 'long-string-not-listed' }
|
||||
|
||||
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'for glm_content' do
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do
|
||||
let_it_be(:project) { create(:project, :in_group) }
|
||||
let_it_be(:custom_emoji_in_origin_namespace) { create(:custom_emoji, name: 'partyparrot', namespace: project.group) }
|
||||
let_it_be(:from_awardable) do
|
||||
create(
|
||||
:issue,
|
||||
create(:issue, project: project,
|
||||
award_emoji: [
|
||||
build(:award_emoji, name: AwardEmoji::THUMBS_UP),
|
||||
build(:award_emoji, name: AwardEmoji::THUMBS_DOWN)
|
||||
build(:award_emoji, name: AwardEmoji::THUMBS_DOWN),
|
||||
build(:award_emoji, name: custom_emoji_in_origin_namespace.name)
|
||||
])
|
||||
end
|
||||
|
||||
|
|
@ -23,7 +25,7 @@ RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do
|
|||
|
||||
subject(:execute_service) { described_class.new(from_awardable, to_awardable).execute }
|
||||
|
||||
it 'copies AwardEmojis', :aggregate_failures do
|
||||
it 'copies AwardEmojis that exist in the destination namespace', :aggregate_failures do
|
||||
expect { execute_service }.to change { AwardEmoji.count }.by(2)
|
||||
expect(to_awardable.award_emoji.map(&:name)).to match_array([AwardEmoji::THUMBS_UP, AwardEmoji::THUMBS_DOWN])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ RSpec.describe Todos::SnoozingService, feature_category: :team_planning do
|
|||
context 'when the todo is already snoozed' do
|
||||
let!(:todo) { create(:todo, :pending, snoozed_until: time1, user: user) }
|
||||
|
||||
it 'does not change the snoozed_until timestamp' do
|
||||
it 'changes the snoozed_until timestamp' do
|
||||
service.snooze_todo(todo, time2)
|
||||
todo.reload
|
||||
|
||||
expect(todo.snoozed_until).to eq(time1)
|
||||
expect(todo.snoozed_until).to eq(time2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1554,52 +1554,49 @@ RSpec.shared_examples 'a container registry auth service' do
|
|||
]
|
||||
end
|
||||
|
||||
before do
|
||||
enable_admin_mode!(current_user) if current_user == instance_admin
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
# rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table layout
|
||||
where(:user, :requested_scopes, :expected_access, :expected_deny_patterns) do
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {}
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | nil # developers can't obtain delete access
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | {}
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:*"] } | false | nil # developers can't obtain full access
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,push"] } | true | { 'push' => %w[v1.* latest admin-only] } # single test for edge case where access may be repeated
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,foo"] } | true | { 'push' => %w[v1.* latest admin-only] } # test for (today impossible) case where an access is unknown
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:foo"] } | false | {} # test for (today impossible) case where the access is unknown
|
||||
where(:user, :requested_scopes, :enable_admin_mode, :expected_access, :expected_deny_patterns) do
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {}
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | false | nil # developers can't obtain delete access
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | {}
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[v1.* latest admin-only] }
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:*"] } | false | false | nil # developers can't obtain full access
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,push"] } | false | true | { 'push' => %w[v1.* latest admin-only] } # single test for edge case where access may be repeated
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,foo"] } | false | true | { 'push' => %w[v1.* latest admin-only] } # test for (today impossible) case where an access is unknown
|
||||
ref(:project_developer) | lazy { ["repository:#{container_repository_path}:foo"] } | false | false | {} # test for (today impossible) case where the access is unknown
|
||||
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {}
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[latest admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[latest admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {}
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[latest admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | true | { 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[latest admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | { 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:*"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] }
|
||||
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {}
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[admin-only] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[admin-only] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {}
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[admin-only] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:delete"] } | false | true | { 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[admin-only] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | { 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
ref(:project_owner) | lazy { ["repository:#{container_repository_path}:*"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] }
|
||||
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {}
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull"] } | true | true | {}
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push"] } | true | true | { 'push' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:delete"] } | true | true | { 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | true | { 'push' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | true | { 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | true | true | { 'push' => [], 'delete' => [] }
|
||||
ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | false | false | {} # ensure that admin mode is properly enforced
|
||||
end
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
|
|
@ -1607,6 +1604,10 @@ RSpec.shared_examples 'a container registry auth service' do
|
|||
let(:current_user) { user }
|
||||
let(:current_params) { { scopes: requested_scopes } }
|
||||
|
||||
before do
|
||||
enable_admin_mode!(current_user) if enable_admin_mode
|
||||
end
|
||||
|
||||
it 'returns the expected tag deny access patterns' do
|
||||
is_expected.to include(:token)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue