Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bddd730eaa
commit
a400b7b7ff
|
|
@ -1336,12 +1336,6 @@ lib/gitlab/checks/**
|
|||
/app/services/projects/refresh_build_artifacts_size_statistics_service.rb
|
||||
/app/uploaders/job_artifact_uploader.rb
|
||||
/app/validators/json_schemas/build_metadata_id_tokens.json
|
||||
/app/views/projects/artifacts/
|
||||
/app/views/projects/generic_commit_statuses/
|
||||
/app/views/projects/jobs/
|
||||
/app/views/projects/pipeline_schedules/
|
||||
/app/views/projects/pipelines/
|
||||
/app/views/projects/triggers/
|
||||
/app/workers/build_queue_worker.rb
|
||||
/app/workers/ci_platform_metrics_update_cron_worker.rb
|
||||
/app/workers/create_pipeline_worker.rb
|
||||
|
|
@ -1408,6 +1402,12 @@ lib/gitlab/checks/**
|
|||
/**/javascripts/admin/application_settings/runner_token_expiration/
|
||||
/**/javascripts/editor/schema/ci.json
|
||||
/app/**/ci/*.haml
|
||||
/app/views/projects/artifacts/
|
||||
/app/views/projects/generic_commit_statuses/
|
||||
/app/views/projects/jobs/
|
||||
/app/views/projects/pipeline_schedules/
|
||||
/app/views/projects/pipelines/
|
||||
/app/views/projects/triggers/
|
||||
/ee/app/**/ci/*.haml
|
||||
/ee/app/**/merge_trains/*.haml
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export async function mountIssuesListApp() {
|
|||
emailsHelpPagePath,
|
||||
exportCsvPath,
|
||||
fullPath,
|
||||
groupId,
|
||||
groupPath,
|
||||
hasAnyIssues,
|
||||
hasAnyProjects,
|
||||
|
|
@ -113,10 +114,10 @@ export async function mountIssuesListApp() {
|
|||
rssPath,
|
||||
showNewIssueLink,
|
||||
signInPath,
|
||||
groupId = '',
|
||||
reportAbusePath,
|
||||
registerPath,
|
||||
issuesListPath,
|
||||
wiCanAdminLabel,
|
||||
wiIssuesListPath,
|
||||
wiLabelsManagePath,
|
||||
wiReportAbusePath,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
|
|
@ -155,10 +156,8 @@ export async function mountIssuesListApp() {
|
|||
canReadCrmContact: parseBoolean(canReadCrmContact),
|
||||
canReadCrmOrganization: parseBoolean(canReadCrmOrganization),
|
||||
fullPath,
|
||||
projectPath: fullPath,
|
||||
groupId,
|
||||
groupPath,
|
||||
reportAbusePath,
|
||||
registerPath,
|
||||
hasAnyIssues: parseBoolean(hasAnyIssues),
|
||||
hasAnyProjects: parseBoolean(hasAnyProjects),
|
||||
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
|
||||
|
|
@ -197,8 +196,11 @@ export async function mountIssuesListApp() {
|
|||
markdownHelpPath,
|
||||
quickActionsHelpPath,
|
||||
resetPath,
|
||||
groupId,
|
||||
issuesListPath,
|
||||
// For work item modal
|
||||
canAdminLabel: wiCanAdminLabel,
|
||||
issuesListPath: wiIssuesListPath,
|
||||
labelsManagePath: wiLabelsManagePath,
|
||||
reportAbusePath: wiReportAbusePath,
|
||||
},
|
||||
render: (createComponent) => createComponent(IssuesListApp),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@ import TaskListItemActions from './task_list_item_actions.vue';
|
|||
|
||||
Vue.use(GlToast);
|
||||
|
||||
const workItemTypes = {
|
||||
TASK: 'task',
|
||||
};
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
SafeHtml,
|
||||
|
|
@ -146,19 +142,14 @@ export default {
|
|||
this.initialUpdate = false;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.renderGFM();
|
||||
});
|
||||
this.renderGFM();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('convert-task-list-item', this.convertTaskListItem);
|
||||
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
|
||||
|
||||
// this.renderGFM();
|
||||
this.$nextTick(() => {
|
||||
this.renderGFM();
|
||||
});
|
||||
this.renderGFM();
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
|
||||
|
|
@ -167,7 +158,9 @@ export default {
|
|||
this.removeAllPointerEventListeners();
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
async renderGFM() {
|
||||
await this.$nextTick();
|
||||
|
||||
renderGFM(this.$refs['gfm-content']);
|
||||
|
||||
if (this.canUpdate) {
|
||||
|
|
@ -177,15 +170,13 @@ export default {
|
|||
fieldName: 'description',
|
||||
lockVersion: this.lockVersion,
|
||||
selector: '.detail-page-description',
|
||||
onUpdate: this.taskListUpdateStarted.bind(this),
|
||||
onSuccess: this.taskListUpdateSuccess.bind(this),
|
||||
onUpdate: () => this.$emit('taskListUpdateStarted'),
|
||||
onSuccess: () => this.$emit('taskListUpdateSucceeded'),
|
||||
onError: this.taskListUpdateError.bind(this),
|
||||
});
|
||||
|
||||
this.removeAllPointerEventListeners();
|
||||
|
||||
this.renderSortableLists();
|
||||
|
||||
this.renderTaskListItemActions();
|
||||
}
|
||||
},
|
||||
|
|
@ -263,30 +254,18 @@ export default {
|
|||
this.pointerEventListeners.delete(listItem);
|
||||
});
|
||||
},
|
||||
taskListUpdateStarted() {
|
||||
this.$emit('taskListUpdateStarted');
|
||||
},
|
||||
taskListUpdateSuccess() {
|
||||
this.$emit('taskListUpdateSucceeded');
|
||||
},
|
||||
taskListUpdateError() {
|
||||
createAlert({
|
||||
message: sprintf(
|
||||
__(
|
||||
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
|
||||
),
|
||||
{
|
||||
issueType: this.issuableType,
|
||||
},
|
||||
),
|
||||
});
|
||||
const message = __(
|
||||
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
|
||||
);
|
||||
createAlert({ message: sprintf(message, { issueType: this.issuableType }) });
|
||||
|
||||
this.$emit('taskListUpdateFailed');
|
||||
},
|
||||
createTaskListItemActions(provide) {
|
||||
createTaskListItemActions() {
|
||||
const app = new Vue({
|
||||
el: document.createElement('div'),
|
||||
provide,
|
||||
provide: { issuableType: this.issuableType },
|
||||
render: (createElement) => createElement(TaskListItemActions),
|
||||
});
|
||||
return app.$el;
|
||||
|
|
@ -310,8 +289,7 @@ export default {
|
|||
);
|
||||
|
||||
taskListItems?.forEach((item) => {
|
||||
const provide = { canUpdate: this.canUpdate, issuableType: this.issuableType };
|
||||
const dropdown = this.createTaskListItemActions(provide);
|
||||
const dropdown = this.createTaskListItemActions();
|
||||
this.insertNextToTaskListItemText(dropdown, item);
|
||||
this.addPointerEventListeners(item, '.task-list-item-actions');
|
||||
this.hasTaskListItemActions = true;
|
||||
|
|
@ -419,7 +397,7 @@ export default {
|
|||
},
|
||||
showAlert(message, error) {
|
||||
createAlert({
|
||||
message: sprintfWorkItem(message, workItemTypes.TASK),
|
||||
message: sprintfWorkItem(message, WORK_ITEM_TYPE_VALUE_TASK),
|
||||
error,
|
||||
captureError: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
<script>
|
||||
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
|
||||
import { __, s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
convertToTask: s__('WorkItem|Convert to task'),
|
||||
delete: __('Delete'),
|
||||
taskActions: s__('WorkItem|Task actions'),
|
||||
},
|
||||
components: {
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownItem,
|
||||
},
|
||||
inject: ['canUpdate', 'issuableType'],
|
||||
inject: ['issuableType'],
|
||||
computed: {
|
||||
showConvertToTaskItem() {
|
||||
return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
|
||||
|
|
@ -33,29 +27,28 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-disclosure-dropdown
|
||||
v-if="canUpdate"
|
||||
class="task-list-item-actions-wrapper"
|
||||
category="tertiary"
|
||||
icon="ellipsis_v"
|
||||
no-caret
|
||||
placement="bottom-end"
|
||||
:toggle-text="$options.i18n.taskActions"
|
||||
text-sr-only
|
||||
toggle-class="task-list-item-actions gl-opacity-0 gl-p-2! "
|
||||
toggle-class="task-list-item-actions gl-opacity-0 !gl-p-2"
|
||||
:toggle-text="s__('WorkItem|Task actions')"
|
||||
>
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="showConvertToTaskItem"
|
||||
class="gl-ml-2!"
|
||||
class="!gl-ml-2"
|
||||
data-testid="convert"
|
||||
@action="convertToTask"
|
||||
>
|
||||
<template #list-item>
|
||||
{{ $options.i18n.convertToTask }}
|
||||
{{ s__('WorkItem|Convert to task') }}
|
||||
</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
<gl-disclosure-dropdown-item class="gl-ml-2!" data-testid="delete" @action="deleteTaskListItem">
|
||||
<gl-disclosure-dropdown-item class="!gl-ml-2" data-testid="delete" @action="deleteTaskListItem">
|
||||
<template #list-item>
|
||||
<span class="gl-text-red-500!">{{ $options.i18n.delete }}</span>
|
||||
<span class="gl-text-red-500">{{ __('Delete') }}</span>
|
||||
</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
</gl-disclosure-dropdown>
|
||||
|
|
|
|||
|
|
@ -66,23 +66,23 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="suggested-colors gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-gap-2"
|
||||
class="suggested-colors gl-grid gl-grid-cols-[repeat(auto-fill,2rem)] gl-justify-between gl-gap-2"
|
||||
>
|
||||
<gl-link
|
||||
v-for="(color, index) in suggestedColors"
|
||||
:key="index"
|
||||
v-gl-tooltip:tooltipcontainer
|
||||
class="gl-block color-palette"
|
||||
class="gl-block gl-h-7 gl-w-7 gl-rounded-base"
|
||||
:style="getStyle(color)"
|
||||
:title="getColorName(color)"
|
||||
@click.prevent="handleColorClick(getColorCode(color))"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-display-flex">
|
||||
<gl-form-group class="gl-mb-0!">
|
||||
<div class="gl-flex">
|
||||
<gl-form-group class="gl-mb-0">
|
||||
<gl-form-input
|
||||
v-model.trim="selectedColor"
|
||||
class="-gl-mr-1 gl-mb-2 gl-w-8"
|
||||
class="-gl-mr-1 gl-w-8 gl-rounded-e-none"
|
||||
type="color"
|
||||
:value="selectedColor"
|
||||
:placeholder="__('Select color')"
|
||||
|
|
@ -92,11 +92,11 @@ export default {
|
|||
<gl-form-group
|
||||
:invalid-feedback="errorMessage"
|
||||
:state="validColor"
|
||||
class="gl-mb-0! gl-flex-grow-1"
|
||||
class="gl-mb-0 gl-flex-grow-1"
|
||||
>
|
||||
<gl-form-input
|
||||
v-model.trim="selectedColor"
|
||||
class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
|
||||
class="gl-rounded-s-none gl-mb-2"
|
||||
:placeholder="__('Use custom color #FF0000')"
|
||||
:autofocus="autofocus"
|
||||
:state="validColor"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
|
||||
import NO_USERS_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-user-settings-md.svg';
|
||||
import { GlSkeletonLoader, GlTable, GlEmptyState } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import UserDate from '~/vue_shared/components/user_date.vue';
|
||||
import UserAvatar from './user_avatar.vue';
|
||||
|
|
@ -10,6 +11,7 @@ export default {
|
|||
GlTable,
|
||||
UserAvatar,
|
||||
UserDate,
|
||||
GlEmptyState,
|
||||
},
|
||||
props: {
|
||||
users: {
|
||||
|
|
@ -63,47 +65,50 @@ export default {
|
|||
thClass: 'gl-w-2/20',
|
||||
},
|
||||
],
|
||||
NO_USERS_SVG,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-table
|
||||
:items="users"
|
||||
:fields="$options.fields"
|
||||
:empty-text="s__('AdminUsers|No users found')"
|
||||
show-empty
|
||||
stacked="md"
|
||||
:tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
|
||||
>
|
||||
<template #cell(name)="{ item: user }">
|
||||
<user-avatar :user="user" :admin-user-path="adminUserPath" />
|
||||
</template>
|
||||
<gl-table
|
||||
v-if="users.length > 0"
|
||||
:items="users"
|
||||
:fields="$options.fields"
|
||||
stacked="md"
|
||||
:tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
|
||||
>
|
||||
<template #cell(name)="{ item: user }">
|
||||
<user-avatar :user="user" :admin-user-path="adminUserPath" />
|
||||
</template>
|
||||
|
||||
<template #cell(createdAt)="{ item: { createdAt } }">
|
||||
<user-date :date="createdAt" />
|
||||
</template>
|
||||
<template #cell(createdAt)="{ item: { createdAt } }">
|
||||
<user-date :date="createdAt" />
|
||||
</template>
|
||||
|
||||
<template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
|
||||
<user-date :date="lastActivityOn" show-never />
|
||||
</template>
|
||||
<template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
|
||||
<user-date :date="lastActivityOn" show-never />
|
||||
</template>
|
||||
|
||||
<template #cell(groupCount)="{ item: { id } }">
|
||||
<div :data-testid="`user-group-count-${id}`">
|
||||
<gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
|
||||
<span v-else>{{ groupCounts[id] || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(groupCount)="{ item: { id } }">
|
||||
<div :data-testid="`user-group-count-${id}`">
|
||||
<gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
|
||||
<span v-else>{{ groupCounts[id] || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
|
||||
<div :data-testid="`user-project-count-${id}`">
|
||||
{{ projectsCount || 0 }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
|
||||
<div :data-testid="`user-project-count-${id}`">
|
||||
{{ projectsCount || 0 }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(settings)="{ item: user }">
|
||||
<slot name="user-actions" :user="user"></slot>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
||||
<template #cell(settings)="{ item: user }">
|
||||
<slot name="user-actions" :user="user"></slot>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:svg-path="$options.NO_USERS_SVG"
|
||||
:title="s__('AdminUsers|No users found')"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ export default {
|
|||
<gl-collapsible-listbox
|
||||
:id="inputId"
|
||||
ref="listbox"
|
||||
class="work-item-sidebar-dropdown"
|
||||
:multiple="multiSelect"
|
||||
:searchable="searchable"
|
||||
start-opened
|
||||
|
|
@ -233,7 +234,6 @@ export default {
|
|||
:selected="localSelectedItem"
|
||||
:reset-button-label="resetButton"
|
||||
:infinite-scroll-loading="infiniteScrollLoading"
|
||||
toggle-class="work-item-sidebar-dropdown-toggle"
|
||||
@reset="unassignValue"
|
||||
@search="debouncedSearchKeyUpdate"
|
||||
@select="handleItemClick"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ export default {
|
|||
allowsScopedLabels() {
|
||||
return this.labelsWidget?.allowsScopedLabels;
|
||||
},
|
||||
createLabelText() {
|
||||
return this.isGroup ? __('Create group label') : __('Create project label');
|
||||
},
|
||||
manageLabelText() {
|
||||
return this.isGroup ? __('Manage group labels') : __('Manage project labels');
|
||||
},
|
||||
workspaceType() {
|
||||
return this.isGroup ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
|
||||
},
|
||||
|
|
@ -282,23 +288,28 @@ export default {
|
|||
class="!gl-justify-start"
|
||||
block
|
||||
category="tertiary"
|
||||
data-testid="create-project-label"
|
||||
data-testid="create-label"
|
||||
@click="showLabelForm = true"
|
||||
>
|
||||
{{ __('Create project label') }}
|
||||
{{ createLabelText }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
class="!gl-justify-start !gl-mt-2"
|
||||
block
|
||||
category="tertiary"
|
||||
:href="labelsManagePath"
|
||||
data-testid="manage-project-labels"
|
||||
data-testid="manage-labels"
|
||||
>
|
||||
{{ __('Manage project labels') }}
|
||||
{{ manageLabelText }}
|
||||
</gl-button>
|
||||
</template>
|
||||
<template v-if="showLabelForm" #body>
|
||||
<gl-disclosure-dropdown block start-opened :toggle-text="dropdownText">
|
||||
<gl-disclosure-dropdown
|
||||
class="work-item-sidebar-dropdown"
|
||||
block
|
||||
start-opened
|
||||
:toggle-text="dropdownText"
|
||||
>
|
||||
<div
|
||||
class="gl-text-sm gl-font-bold gl-leading-24 gl-border-b gl-pt-2 gl-pb-3 gl-pl-4 gl-mb-4"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ export default {
|
|||
<gl-collapsible-listbox
|
||||
id="$options.inputId"
|
||||
ref="input"
|
||||
class="gl-block"
|
||||
class="work-item-sidebar-dropdown gl-block"
|
||||
data-testid="work-item-parent-listbox"
|
||||
block
|
||||
searchable
|
||||
|
|
@ -266,7 +266,6 @@ export default {
|
|||
is-check-centered
|
||||
category="primary"
|
||||
fluid-width
|
||||
toggle-class="work-item-sidebar-dropdown-toggle"
|
||||
positioning-strategy="fixed"
|
||||
:searching="isLoading"
|
||||
:header-text="$options.i18n.assignParentLabel"
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@
|
|||
inset-inline-end: -2rem;
|
||||
}
|
||||
|
||||
.task-list-item-actions-wrapper.show .task-list-item-actions,
|
||||
.task-list-item-actions[aria-expanded="true"],
|
||||
.task-list-item-actions:is(:focus, :hover) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.md.has-task-list-item-actions > :is(ul, ol) > li {
|
||||
.has-task-list-item-actions > :is(ul, ol) > li {
|
||||
margin-inline-end: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -36,10 +36,6 @@
|
|||
inset-inline-start: -0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item.text-danger p {
|
||||
color: var(--red-500, $red-500); /* Override typography.scss making text black */
|
||||
}
|
||||
}
|
||||
|
||||
.is-ghost {
|
||||
|
|
|
|||
|
|
@ -345,12 +345,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
}
|
||||
}
|
||||
|
||||
/** Ideally should be fixed in gitlab-ui but fixing it using classes for now **/
|
||||
.work-item-sidebar-dropdown-toggle {
|
||||
justify-content: start !important;
|
||||
}
|
||||
|
||||
.work-item-sidebar-dropdown-toggle ~ .gl-new-dropdown-panel {
|
||||
.work-item-sidebar-dropdown .gl-new-dropdown-panel {
|
||||
width: 100% !important;
|
||||
max-width: 19rem !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
&:nth-of-type(7) {
|
||||
border-top-right-radius: $gl-border-radius-base;
|
||||
}
|
||||
|
||||
|
||||
&:nth-last-child(7) {
|
||||
border-bottom-left-radius: $gl-border-radius-base;
|
||||
}
|
||||
|
|
@ -78,11 +78,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.suggested-colors {
|
||||
.color-palette {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ module IssuesHelper
|
|||
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
|
||||
calendar_path: url_for(safe_params.merge(calendar_url_options)),
|
||||
full_path: namespace.full_path,
|
||||
has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s,
|
||||
initial_sort: current_user&.user_preference&.issues_sort,
|
||||
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
|
||||
is_public_visibility_restricted:
|
||||
|
|
@ -143,7 +144,7 @@ module IssuesHelper
|
|||
is_signed_in: current_user.present?.to_s,
|
||||
rss_path: url_for(safe_params.merge(rss_url_options)),
|
||||
sign_in_path: new_user_session_path,
|
||||
has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s
|
||||
wi: work_items_show_data(namespace)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -179,10 +180,7 @@ module IssuesHelper
|
|||
quick_actions_help_path: help_page_path('user/project/quick_actions'),
|
||||
releases_path: project_releases_path(project, format: :json),
|
||||
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
|
||||
show_new_issue_link: show_new_issue_link?(project).to_s,
|
||||
report_abuse_path: add_category_abuse_reports_path,
|
||||
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
|
||||
issues_list_path: project_issues_path(project)
|
||||
show_new_issue_link: show_new_issue_link?(project).to_s
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -191,11 +189,10 @@ module IssuesHelper
|
|||
can_create_projects: can?(current_user, :create_projects, group).to_s,
|
||||
can_read_crm_contact: can?(current_user, :read_crm_contact, group).to_s,
|
||||
can_read_crm_organization: can?(current_user, :read_crm_organization, group).to_s,
|
||||
group_id: group.id,
|
||||
has_any_issues: @has_issues.to_s,
|
||||
has_any_projects: @has_projects.to_s,
|
||||
new_project_path: new_project_path(namespace_id: group.id),
|
||||
group_id: group.id,
|
||||
issues_list_path: issues_group_path(group)
|
||||
new_project_path: new_project_path(namespace_id: group.id)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@ module ResourceAccessTokens
|
|||
|
||||
access_token.revoke!
|
||||
|
||||
destroy_bot_user
|
||||
success_message = "Access token #{access_token.name} has been revoked"
|
||||
unless Feature.enabled?(:retain_resource_access_token_user_after_revoke, resource)
|
||||
destroy_bot_user
|
||||
success_message += " and the bot user has been scheduled for deletion"
|
||||
end
|
||||
|
||||
log_event
|
||||
|
||||
success("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.")
|
||||
success("#{success_message}.")
|
||||
rescue StandardError => error
|
||||
log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
|
||||
error(error.message)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
- page_title _("Users")
|
||||
- add_to_breadcrumbs _("Users"), admin_users_path
|
||||
- breadcrumb_title _("Cohorts")
|
||||
- page_title _("Cohorts"), _("Users")
|
||||
|
||||
= render ::Layouts::PageHeadingComponent.new(_('Cohorts')) do |c|
|
||||
- c.with_actions do
|
||||
= render_if_exists 'admin/users/admin_email_users'
|
||||
= render_if_exists 'admin/users/admin_export_user_permissions'
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
|
||||
= s_('AdminUsers|New user')
|
||||
|
||||
= render 'admin/users/tabs'
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
.gl-display-flex.gl-align-items-flex-start.gl-flex-wrap.gl-md-flex-nowrap.gl-gap-4.row-content-block.gl-border-0{ data: { testid: "filtered-search-block" } }
|
||||
#js-admin-users-filter-app
|
||||
.gl-flex-shrink-0
|
||||
= label_tag s_('AdminUsers|Sort by')
|
||||
= gl_redirect_listbox_tag admin_users_sort_options(filter: params[:filter], search_query: params[:search_query]), @sort, data: { placement: 'right' }
|
||||
|
||||
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
- page_title _("Users")
|
||||
|
||||
.top-area{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_users_pageload' } }
|
||||
= render 'tabs'
|
||||
.nav-controls
|
||||
= render ::Layouts::PageHeadingComponent.new(_('Users'), options: { data: { event_tracking_load: 'true', event_tracking: 'view_admin_users_pageload' } }) do |c|
|
||||
- c.with_actions do
|
||||
= render_if_exists 'admin/users/admin_email_users'
|
||||
= render_if_exists 'admin/users/admin_export_user_permissions'
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
|
||||
= s_('AdminUsers|New user')
|
||||
|
||||
.top-area
|
||||
= render 'tabs'
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active
|
||||
= render 'users'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
- page_title _("Wiki")
|
||||
- @right_sidebar = true
|
||||
- @gfm_form = true
|
||||
- @noteable_type = 'Wiki'
|
||||
- add_page_specific_style 'page_bundles/wiki'
|
||||
|
||||
- if @error.present?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: retain_resource_access_token_user_after_revoke
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462217
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157130
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468606
|
||||
milestone: '17.2'
|
||||
group: group::authentication
|
||||
type: beta
|
||||
default_enabled: false
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
# Warning: gitlab.LatinTerms
|
||||
#
|
||||
# Checks for use of Latin terms.
|
||||
# Uses https://github.com/errata-ai/Google/blob/master/Google/Latin.yml for ideas.
|
||||
#
|
||||
# For a list of all options, see https://vale.sh/docs/topics/styles/
|
||||
extends: substitution
|
||||
|
|
@ -11,8 +12,6 @@ level: warning
|
|||
nonword: true
|
||||
ignorecase: true
|
||||
swap:
|
||||
e\.g\.: for example
|
||||
e\. g\.: for example
|
||||
i\.e\.: that is
|
||||
i\. e\.: that is
|
||||
via: "with', 'through', or 'by using"
|
||||
'\b(?:e\.?g[\s.,;:])': for example
|
||||
'\b(?:i\.?e[\s.,;:])': that is
|
||||
'\bvia\b': "with', 'through', or 'by using"
|
||||
|
|
|
|||
|
|
@ -97,10 +97,12 @@ Alternatively, you might want to [install the GitLab for Jira Cloud app manually
|
|||
- The instance must be on GitLab version 15.7 or later.
|
||||
- You must set up [OAuth authentication](#set-up-oauth-authentication).
|
||||
- If your instance uses HTTPS, your GitLab certificate must be publicly trusted or contain the full chain certificate.
|
||||
- Your network must allow inbound and outbound connections between GitLab and Jira. For self-managed instances that are behind a
|
||||
- Your network must allow inbound and outbound connections between your self-managed instance,
|
||||
Jira, and GitLab.com. For self-managed instances that are behind a
|
||||
firewall and cannot be directly accessed from the internet, you must:
|
||||
1. Set up an internet-facing [reverse proxy](#using-a-reverse-proxy) in front of your self-managed instance.
|
||||
1. Open your firewall and allow inbound traffic from [Atlassian IP addresses](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/#Outgoing-Connections) only.
|
||||
1. Add [GitLab IP addresses](../../user/gitlab_com/index.md#ip-range) to the allowlist of your firewall.
|
||||
- The Jira user that installs and configures the app must meet certain [requirements](#jira-user-requirements).
|
||||
|
||||
### Set up your instance
|
||||
|
|
|
|||
|
|
@ -221,7 +221,9 @@ For the second log, you might have one of the following scenarios:
|
|||
- `json.jira_status_code` and `json.jira_body` might contain the response received from the self-managed instance or a proxy in front of the instance.
|
||||
- If `json.jira_status_code` is `401 Unauthorized` and `json.jira_body` is `(empty)`:
|
||||
- [**Jira Connect Proxy URL**](jira_cloud_app.md#set-up-your-instance) might not be set to `https://gitlab.com`.
|
||||
- The self-managed instance might be blocking outgoing connections. Ensure that the self-managed instance can connect to `connect-install-keys.atlassian.com`.
|
||||
- The self-managed instance might be blocking outgoing connections. Ensure that your
|
||||
self-managed instance can connect to both `connect-install-keys.atlassian.com`
|
||||
and `gitlab.com`.
|
||||
- The self-managed instance is unable to decrypt the JWT token from Jira. [From GitLab 16.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147234),
|
||||
the [`exceptions_json.log`](../logs/index.md#exceptions_jsonlog) contains more information about the error.
|
||||
- If a [reverse proxy](jira_cloud_app.md#using-a-reverse-proxy) is in front of your self-managed instance,
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
|
|
@ -0,0 +1,290 @@
|
|||
---
|
||||
status: proposed
|
||||
creation-date: "2023-06-03"
|
||||
authors: [ "@dmishunov" ]
|
||||
coach: "@jessieay"
|
||||
approvers: [ ]
|
||||
owning-stage: "~devops::ai-powered"
|
||||
participating-stages: ["~devops::create"]
|
||||
---
|
||||
|
||||
# AI Context Management
|
||||
|
||||
## Glossary
|
||||
|
||||
- **AI Context**. In the scope of this technical blueprint, the term "AI Context" refers to supplementary information
|
||||
provided to the AI system alongside the primary prompts.
|
||||
- **AI Context Policy**. The "AI Context Policy" is a user-defined and user-managed mechanism allowing precise
|
||||
control over the content that can be sent to the AI as contextual information. In the context of this blueprint, the
|
||||
_AI Context Policy_ is suggested as a YAML configuration file.
|
||||
- **AI Context Policy Management**. Within this blueprint, "Management" encompasses the user-driven processes of
|
||||
creating, modifying, and removing AI Context Policies according to specific requirements and preferences.
|
||||
- **Automatic AI Context**. _AI Context_, retrieved automatically based on the active document. _*Automatic AI Contex_
|
||||
can be the active document's dependencies (modules, methods, etc., imported into the active document), some
|
||||
search-based, or other mechanisms over which the user has limited control.
|
||||
- **Supplementary User Context**: User-defined _AI Context_, such as open tabs in IDEs, local files, and folders, that the user
|
||||
provides from their local environment to extend the default _AI Context_.
|
||||
- **AI Context Retriever**: A backend system capable of:
|
||||
- communicating with _AI Context Policy Management_
|
||||
- fetching content defined in _Automatic AI Context_ and _Supplementary User Context_ (complete files, definitions,
|
||||
methods, etc.), based on the _AI Context Policy Management_
|
||||
- correctly augment the user prompt with AI Context before sending it to LLM. Presumably, this part is already
|
||||
handled by [AI Gateway](../ai_gateway/index.md).
|
||||
- **Project Administrator**. In the context of this blueprint, "Project Administrator" means any individual with the
|
||||
"Edit project settings" permission ("Maintainer" or "Owner" roles, as defined in [Project members permissions](../../../user/permissions.md#project-members-permissions)).
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
Correct context can dramatically improve the quality of AI responses. This blueprint aims to accommodate AI Context
|
||||
seamlessly into our offering by architecting a solution that is ready for this additional context coming from different
|
||||
AI features.
|
||||
|
||||
However, we recognize the importance of security and trust, which automatic solutions do not necessarily provide. To
|
||||
address any concerns users might have about the content fed into the AI Context, this blueprint suggests providing them
|
||||
with control and customization options. This way, users can adjust the content according to their preferences and have a
|
||||
clear understanding of what information is being utilized.
|
||||
|
||||
This blueprint proposes a system for managing _AI Context_ at the _Project Administrator_ and individual
|
||||
user levels. Its goal is to allow _Project Administrator_ to set high-level rules for what content can be included as context for AI
|
||||
prompts while enabling users to specify _Supplementary User Context_ for their prompts. The global _AI Context Policy_ will use a YAML
|
||||
configuration file format stored in the same Git repository. The suggested format of the YAML configuration files
|
||||
is discussed below.
|
||||
|
||||
## Motivation
|
||||
|
||||
Ensuring the AI has the correct context is crucial for generating accurate and relevant code suggestions or responses.
|
||||
As the adoption of AI-assisted development grows, it's essential to give organizations and users control over what project
|
||||
content is sent as context to AI models. Some files or directories may contain sensitive information that should not
|
||||
be shared. At the same time, users may want to provide additional context for their prompts to get more
|
||||
relevant suggestions. We need a flexible _AI Context_ management system to handle these cases.
|
||||
|
||||
### Goals
|
||||
|
||||
### For _Project Administrators_
|
||||
|
||||
- Allow _Project Administrators_ set the default _AI Context Policy_ to control whether content can or cannot be
|
||||
automatically included in the _AI Context_ when making requests to LLMs
|
||||
- Allow _Project Administrators_ to specify exceptions to the default _AI Context Policy_
|
||||
- Provide a UI to manage the default _AI Context Policy_ and its exceptions list easily
|
||||
|
||||
### For users
|
||||
|
||||
- Allow to set _Supplementary User Context_ to include as AI context for their prompts
|
||||
- Provide a UI to manage _Supplementary User Context_ easily
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- _AI Context Retriever_ architecture - different environments (Web, IDEs) will probably implement their retrievers.
|
||||
However, the unified public interface of the retrievers should be considered.
|
||||
- Extremely granular controls like allowing/excluding individual lines of code
|
||||
- Storing entire file contents from user projects, only paths will be persisted
|
||||
|
||||
## Proposal
|
||||
|
||||
The proposed architecture consists of 3 main parts:
|
||||
|
||||
- _AI Context Retriever_
|
||||
- _AI Context Policy Management_
|
||||
- _Supplementary User Context_
|
||||
|
||||
There are several different ongoing efforts related to various implementations of _AI Context Retriever_ both
|
||||
[for Web](https://gitlab.com/groups/gitlab-org/-/epics/14040), and [for IDEs](https://gitlab.com/groups/gitlab-org/editor-extensions/-/epics/55).
|
||||
Because of that, the architecture for _AI Context Retriever_ is beyond the scope of this blueprint. However, in the
|
||||
context of this blueprint, it is assumed that:
|
||||
|
||||
- _AI Context Retriever_ is capable of automatically retrieving and fetching _Automatic AI Context_ and passing it
|
||||
on as _AI Context_ to LLM.
|
||||
- _AI Context Retriever_ can automatically retrieve and fetch _Supplementary User Context_and pass
|
||||
it on as _AI Context_ to LLM.
|
||||
- _AI Context Retriever_ implementation can ensure that any content passed as _AI Context_ to a model
|
||||
adheres to the global _AI Context Policy_.
|
||||
- _AI Context Retriever_ can trim the _AI Context_ to meet the contextual window requirement for a
|
||||
specific LLM used for that or another Duo feature.
|
||||
|
||||
### _AI Context Policy Management_ proposal
|
||||
|
||||
To implement the _AI Context Policy Management_ system, it is proposed to:
|
||||
|
||||
- Introduce the YAML file format for configuring global policies
|
||||
- In the YAML configuration file, support two `ai_context_policy` types:
|
||||
- `block`: blocks all content except for the specified `exclude` paths. Excluded files are allowed. (**Default**)
|
||||
- `allow`: allows all content except for the specified `exclude` paths. Excluded files are blocked.
|
||||
- `version`: specifies the schema version of the AI context file. Starting with `version: 1`. If omitted treated as the latest version known to the client.
|
||||
- In the YAML configuration file, support glob patterns to exclude certain paths from the global policy
|
||||
- Support nested _AI Context Policies_ to provide a more granular control of _AI Context_ in sub-folders. For
|
||||
example, a policy in `/src/tests` would override a policy in `/src`, which, in its turn, would override a
|
||||
global _AI Context Policy_ in `/`.
|
||||
|
||||
### _Supplementary User Context_ proposal
|
||||
|
||||
To implement the _Supplementary User Context_ system, it is proposed to:
|
||||
|
||||
- Introduce user-level UI to specify _Supplementary User Context_ for prompts. A particular implementation of the UI could
|
||||
differ in different environments (IDEs, Web, etc.), but the actual design of these implementations is beyond the scope of
|
||||
this architecture blueprint
|
||||
- The user-level UI should communicate to the user what is in the _Supplementary User Context_ at any moment.
|
||||
- The user-level UI should allow the user to edit the contents of the _Supplementary User Context_.
|
||||
|
||||
### Optional steps
|
||||
|
||||
- Provide UI for _Project Administrators_ to configure global _AI Context Policy_. [Source Editor](../../../development/fe_guide/source_editor.md)
|
||||
can be used as the editor for this type of YAML file format, similar to the
|
||||
[Security Policy Editor](../../../user/application_security/policies/index.md#policy-editor).
|
||||
- Implement a validation mechanism for _AI Context Policies_ to somehow notify the _Project Administrators_ in case
|
||||
of the invalid format of the YAML configuration file. It could be a job in CI. But to catch possible issues proactively, it is
|
||||
also advised to introduce the validation step as part of the
|
||||
[pre-push static analysis](../../../development/contributing/style_guides.md#pre-push-static-analysis-with-lefthook)
|
||||
|
||||
## Design and implementation details
|
||||
|
||||
- **YAML Configuration File Format**: The proposed YAML configuration file format for defining the global
|
||||
_AI Context Policy_ is as follows:
|
||||
|
||||
```yaml
|
||||
ai_context_policy: [allow|block]
|
||||
|
||||
exclude:
|
||||
- glob/**/pattern
|
||||
```
|
||||
|
||||
The `ai_context_policy` section specifies the current policy for this and all underlying folders in a repo.
|
||||
|
||||
The `exclude` section specifies the exceptions to the `ai_context_policy`. Technically, it's an inversion of the policy.
|
||||
For example, if we specify `foo_bar.js` in `exclude`:
|
||||
|
||||
- for the `allow` policy, it means that `foo_bar.js` will be blocked
|
||||
- for the `block` policy, it means that `foo_bar.js` will be allowed
|
||||
|
||||
- **User-Level UI for _Supplementary User Context_**: The UI for specifying _Supplementary User Context_ for prompts
|
||||
can be implemented differently depending on the environment (IDEs, Web, etc.). However, the implementation should
|
||||
ensure users can provide additional context for their prompts. The specified _Supplementary User Context_ for
|
||||
each user can be stored as:
|
||||
|
||||
- a preference stored in the user profile in GitLab
|
||||
|
||||
- **Pros**: Consistent across devices and environments (Web, IDEs, etc.)
|
||||
- **Cons**: Additional work in the monolith, potentially a lot of new read/writes to a database
|
||||
|
||||
- a preference stored in the local IDE/Web storage
|
||||
|
||||
- **Pros**: User-centric, local to user environment
|
||||
- **Cons**: Different implementations for different environments (Web, IDEs, etc.), doesn't survive switching
|
||||
environment or device
|
||||
|
||||
In both cases, the storage should allow the preference to be associated with a particular repository. Factors
|
||||
like data consistency, performance, and implementation complexity should guide the decision on what type of storage
|
||||
to use.
|
||||
|
||||
- To mitigate potential performance and scalability issues, it would make sense to keep _AI Context Retriever_, and
|
||||
_AI Context Policy Management_ in the same environment as the feature needing those. It would be
|
||||
[Language Server](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp) for Duo features in IDEs and different
|
||||
services in the monolith for Duo features on the Web.
|
||||
|
||||
### Data flow
|
||||
|
||||
Here's the draft of the data flow demonstrating the role of _AI Context_ using the Code Suggestions feature as an example.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CS as Code Suggestions
|
||||
participant CR as AI Context Retriever
|
||||
participant PM as AI Context Policy Management
|
||||
participant LLM as Language Model
|
||||
|
||||
CS->>CR: Request Code Suggestion
|
||||
CR->>CR: Retrieve Supplementary User Context list
|
||||
CR->>CR: Retrieve Automatic AI Context list
|
||||
CR->>PM: Check AI Context against Policy
|
||||
PM-->>CR: Return valid AI Context list
|
||||
CR->>CR: Fetch valid AI Context
|
||||
CR->>LLM: Send prompt with final AI Context
|
||||
LLM->>LLM: Generate code suggestions
|
||||
LLM-->>CS: Return code suggestions
|
||||
CS->>CS: Present code suggestions to the user
|
||||
```
|
||||
|
||||
In case the _AI Context Retriever_ fails to fetch any content from the _AI Context_, the prompt is sent with
|
||||
_AI Context_, which was successfully fetched. In a low-probability case, when _AI Context Retriever_ cannot fetch any content, the prompt should be sent out as-is.
|
||||
|
||||
## Alternative solutions
|
||||
|
||||
### JSON Configuration Files
|
||||
|
||||
- **Pros**: Widely used, easier integration with web technologies.
|
||||
- **Cons**: Less readable compared to YAML for complex configurations.
|
||||
|
||||
### Database-Backed Configuration
|
||||
|
||||
- **Pros**: Centralized management, dynamic updates.
|
||||
- **Cons**: Not version controlled.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- **Pros**: Simplifies configuration for deployment and scaling.
|
||||
- **Cons**: Less suitable for complex configurations.
|
||||
|
||||
### Policy as Code (without YAML)
|
||||
|
||||
- **Pros**: Better control and auditing with versioned code.
|
||||
- **Cons**: It requires users to write code and us to invent a language for it.
|
||||
|
||||
### Policy in `.ai_ignore` and other Git-like files
|
||||
|
||||
- **Pros**: Provides a straightforward approach, identical to the `allow` policy with the list of `exclude` suggested in this blueprint
|
||||
- **Cons**: Supports only the `allow` policy; the processing of this file type still has to be implemented
|
||||
|
||||
Based on these alternatives, the YAML file was chosen as a format for this blueprint because of versioning
|
||||
in Git, and more versatility compared to the `.ai_ignore` alternative.
|
||||
|
||||
## Suggested iterative implementation plan
|
||||
|
||||
Please refer to the [Proposal](#proposal) for a detailed explanation of the items in every iteration.
|
||||
|
||||
### Iteration 1
|
||||
|
||||
- Introduce the global `.ai-context-policy.yaml` YAML configuration file format and schema for this file type
|
||||
as part of _AI Context Policy Management_.
|
||||
- _AI Context Retrievers_ introduce support for _Supplementary User Context_.
|
||||
- Optional: validation mechanism (like CI job and pre-push static analysis) for `.ai-context-policy.yaml`
|
||||
|
||||
**Success criteria for the iteration:** Prompts sent from the Code Suggestions feature in IDEs contain
|
||||
_AI Context_ only with the open IDE tabs, which adhere to the global _AI Context Policy_ in the root of a repository.
|
||||
|
||||
### Iteration 2
|
||||
|
||||
- In _AI Context Retrievers_ introduce support for _Automatic AI Context_.
|
||||
- Connect more features to the _AI Context Management_ system.
|
||||
|
||||
**Success criteria for the iteration:** Prompts sent from the Code Suggestions feature in IDEs contain _AI Context_
|
||||
with items of _Automatic AI Context_, which adhere to the global _AI Context Policy_ in the root of a repository.
|
||||
|
||||
### Iteration 3
|
||||
|
||||
- Connect all Duo features on the Web and in IDEs to _AI Context Retrievers_ and adhere to the global
|
||||
_AI Context Policy_.
|
||||
|
||||
**Success criteria for the iteration:** All Duo features in all environments send _AI Context_ which adheres to the
|
||||
global _AI Context Policy_
|
||||
|
||||
### Iteration 4
|
||||
|
||||
- Support nested `.ai-context-policy.yaml` YAML configuration files.
|
||||
|
||||
**Success criteria for the iteration:** _AI Context Policy_ placed into the sub-folders of a repository, override
|
||||
higher-level policies when sending prompts.
|
||||
|
||||
### Iteration 5
|
||||
|
||||
- User-level UI for _Supplementary User Context_.
|
||||
|
||||
**Success criteria for the iteration:** Users can see and edit the contents of the _Supplementary User Context_ and
|
||||
the context is shared between all Duo features within the environment (Web, IDEs, etc.)
|
||||
|
||||
### Iteration 6
|
||||
|
||||
- Optional: UI for configuring the global _AI Context Policy_.
|
||||
|
||||
**Success criteria for the iteration:** Users can see and edit the contents of the _AI Context Policies_ in a UI
|
||||
editor.
|
||||
|
|
@ -768,14 +768,13 @@ We want to avoid introducing a changelog when features are not accessible by an
|
|||
ACF(added / changed / fixed / '...')
|
||||
RF{Remove flag}
|
||||
RF2{Remove flag}
|
||||
NC(No changelog)
|
||||
RC(removed / changed)
|
||||
OTHER(other)
|
||||
|
||||
FDOFF -->CDO-->ACF
|
||||
FDOFF -->RF
|
||||
RF-->|Keep new code?| ACF
|
||||
RF-->|Keep old code?| NC
|
||||
RF-->|Keep old code?| OTHER
|
||||
|
||||
FDON -->RF2
|
||||
RF2-->|Keep old code?| RC
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ Even when creation is disabled, you can still use and revoke existing project ac
|
|||
|
||||
## Bot users for projects
|
||||
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/462217) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `retain_resource_access_token_user_after_revoke`. Disabled by default. When enabled, the bot user is retained. It is not deleted and its records are not moved to the Ghost User.
|
||||
|
||||
FLAG:
|
||||
The behavior of the bot user after the project access token is revoked is controlled by a feature flag. For more information, see the history.
|
||||
|
||||
Bot users for projects are [GitLab-created service accounts](../../../subscriptions/self_managed/index.md#billable-users).
|
||||
Each time you create a project access token, a bot user is created and added to the project.
|
||||
This user is not a billable user, so it does not count toward the license limit.
|
||||
|
|
|
|||
|
|
@ -4467,9 +4467,6 @@ msgstr ""
|
|||
msgid "AdminUsers|Skype"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Sort by"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Stop monitoring %{username} for possible spam?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5514,6 +5511,9 @@ msgstr ""
|
|||
msgid "An error occurred while checking group path. Please refresh and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while creating the group. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while creating the issue. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12916,6 +12916,9 @@ msgstr ""
|
|||
msgid "CodeownersValidation|Zero owners"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cohorts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cohorts|Active users"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20305,6 +20308,9 @@ msgstr ""
|
|||
msgid "Enter epic URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter group name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter in your Bitbucket Server URL and personal access token below"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51740,10 +51746,13 @@ msgstr ""
|
|||
msgid "SubscriptionBanner|Upload new license"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Group name %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Select a group for your %{planName} subscription"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Select a group for your subscription"
|
||||
msgid "SubscriptionGroupsNew|Select a group for your subscription."
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|The group is a top-level group on a Free tier"
|
||||
|
|
@ -51755,6 +51764,9 @@ msgstr ""
|
|||
msgid "SubscriptionGroupsNew|You're assigned the Owner role of the group"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Your group will be created at:"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Your group will only be displayed in the list above if:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe('TaskListItemActions component', () => {
|
|||
document.body.appendChild(li);
|
||||
|
||||
wrapper = shallowMountExtended(TaskListItemActions, {
|
||||
provide: { canUpdate: true, issuableType },
|
||||
provide: { issuableType },
|
||||
attachTo: document.querySelector('div'),
|
||||
});
|
||||
};
|
||||
|
|
@ -32,8 +32,8 @@ describe('TaskListItemActions component', () => {
|
|||
category: 'tertiary',
|
||||
icon: 'ellipsis_v',
|
||||
placement: 'bottom-end',
|
||||
toggleText: TaskListItemActions.i18n.taskActions,
|
||||
textSrOnly: true,
|
||||
toggleText: 'Task actions',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@ describe('WorkItemLabels component', () => {
|
|||
const findRegularLabel = () => findAllLabels().at(0);
|
||||
const findLabelWithDescription = () => findAllLabels().at(2);
|
||||
const findDropdownContentsCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
|
||||
const findCreateLabelButton = () => wrapper.findByTestId('create-label');
|
||||
const findManageLabelsButton = () => wrapper.findByTestId('manage-labels');
|
||||
|
||||
const showDropdown = () => {
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('dropdownShown');
|
||||
|
|
@ -422,71 +424,94 @@ describe('WorkItemLabels component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('creating project label', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
describe('create/manage label buttons', () => {
|
||||
describe('when project context', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ isGroup: false });
|
||||
});
|
||||
|
||||
wrapper.findByTestId('create-project-label').vm.$emit('click');
|
||||
await nextTick();
|
||||
});
|
||||
it('renders "Create project label" button', () => {
|
||||
expect(findCreateLabelButton().text()).toBe('Create project label');
|
||||
});
|
||||
|
||||
describe('when "Create project label" button is clicked', () => {
|
||||
it('renders "Create label" dropdown', () => {
|
||||
expect(findDisclosureDropdown().props()).toMatchObject({
|
||||
block: true,
|
||||
startOpened: true,
|
||||
toggleText: 'No labels',
|
||||
});
|
||||
expect(findDropdownContentsCreateView().props()).toEqual({
|
||||
attrWorkspacePath: 'test-project-path',
|
||||
fullPath: 'test-project-path',
|
||||
labelCreateType: 'project',
|
||||
searchKey: '',
|
||||
workspaceType: 'project',
|
||||
});
|
||||
it('renders "Manage project labels" link', () => {
|
||||
expect(findManageLabelsButton().text()).toBe('Manage project labels');
|
||||
expect(findManageLabelsButton().attributes('href')).toBe('test-project-path/labels');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "hideCreateView" event is emitted', () => {
|
||||
it('hides dropdown', async () => {
|
||||
expect(findDisclosureDropdown().exists()).toBe(true);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(true);
|
||||
describe('when group context', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ isGroup: true });
|
||||
});
|
||||
|
||||
findDropdownContentsCreateView().vm.$emit('hideCreateView');
|
||||
it('renders "Create group label" button', () => {
|
||||
expect(findCreateLabelButton().text()).toBe('Create group label');
|
||||
});
|
||||
|
||||
it('renders "Manage group labels" link', () => {
|
||||
expect(findManageLabelsButton().text()).toBe('Manage group labels');
|
||||
expect(findManageLabelsButton().attributes('href')).toBe('test-project-path/labels');
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating project label', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
findCreateLabelButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findDisclosureDropdown().exists()).toBe(false);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "labelCreated" event is emitted', () => {
|
||||
it('updates "createdLabelId" value and hides dropdown', async () => {
|
||||
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
|
||||
expect(findDisclosureDropdown().exists()).toBe(true);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(true);
|
||||
|
||||
findDropdownContentsCreateView().vm.$emit('labelCreated', {
|
||||
id: 'gid://gitlab/Label/55',
|
||||
name: 'New label',
|
||||
describe('when "Create project label" button is clicked', () => {
|
||||
it('renders "Create label" dropdown', () => {
|
||||
expect(findDisclosureDropdown().props()).toMatchObject({
|
||||
block: true,
|
||||
startOpened: true,
|
||||
toggleText: 'No labels',
|
||||
});
|
||||
expect(findDropdownContentsCreateView().props()).toEqual({
|
||||
attrWorkspacePath: 'test-project-path',
|
||||
fullPath: 'test-project-path',
|
||||
labelCreateType: 'project',
|
||||
searchKey: '',
|
||||
workspaceType: 'project',
|
||||
});
|
||||
});
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
|
||||
'gid://gitlab/Label/55',
|
||||
);
|
||||
expect(findDisclosureDropdown().exists()).toBe(false);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(false);
|
||||
describe('when "hideCreateView" event is emitted', () => {
|
||||
it('hides dropdown', async () => {
|
||||
expect(findDisclosureDropdown().exists()).toBe(true);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(true);
|
||||
|
||||
findDropdownContentsCreateView().vm.$emit('hideCreateView');
|
||||
await nextTick();
|
||||
|
||||
expect(findDisclosureDropdown().exists()).toBe(false);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "labelCreated" event is emitted', () => {
|
||||
it('updates "createdLabelId" value and hides dropdown', async () => {
|
||||
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
|
||||
expect(findDisclosureDropdown().exists()).toBe(true);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(true);
|
||||
|
||||
findDropdownContentsCreateView().vm.$emit('labelCreated', {
|
||||
id: 'gid://gitlab/Label/55',
|
||||
name: 'New label',
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
|
||||
'gid://gitlab/Label/55',
|
||||
);
|
||||
expect(findDisclosureDropdown().exists()).toBe(false);
|
||||
expect(findDropdownContentsCreateView().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders "Manage project labels" link in dropdown', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByTestId('manage-project-labels').text()).toBe('Manage project labels');
|
||||
expect(wrapper.findByTestId('manage-project-labels').attributes('href')).toBe(
|
||||
'test-project-path/labels',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it "returns only draft notes authored by the current user" do
|
||||
it "returns only draft notes authored by the current user",
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/448707' do
|
||||
get api(base_url, user)
|
||||
|
||||
draft_note_ids = json_response.pluck("id")
|
||||
|
|
|
|||
|
|
@ -243,13 +243,49 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
end
|
||||
|
||||
context "when the user has valid permissions" do
|
||||
context "when retain bot user ff is disabled" do
|
||||
before do
|
||||
stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
|
||||
end
|
||||
|
||||
it "deletes the #{source_type} access token from the #{source_type}" do
|
||||
delete_token
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(token.reload).to be_revoked
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: project_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
end
|
||||
|
||||
context "when using #{source_type} access token to DELETE other #{source_type} access token" do
|
||||
let_it_be(:other_project_bot) { create(:user, :project_bot) }
|
||||
let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
|
||||
let_it_be(:token_id) { other_token.id }
|
||||
|
||||
before do
|
||||
resource.add_maintainer(other_project_bot)
|
||||
end
|
||||
|
||||
it "deletes the #{source_type} access token from the #{source_type}" do
|
||||
delete_token
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(token.reload).not_to be_revoked
|
||||
expect(other_token.reload).to be_revoked
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: other_project_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "deletes the #{source_type} access token from the #{source_type}" do
|
||||
delete_token
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: project_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
expect(token.reload).to be_revoked
|
||||
expect(User.exists?(project_bot.id)).to be_truthy
|
||||
end
|
||||
|
||||
context "when using #{source_type} access token to DELETE other #{source_type} access token" do
|
||||
|
|
@ -265,9 +301,9 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
delete_token
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: other_project_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
expect(token.reload).not_to be_revoked
|
||||
expect(other_token.reload).to be_revoked
|
||||
expect(User.exists?(other_project_bot.id)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -15,26 +15,18 @@ RSpec.describe ResourceAccessTokens::RevokeService, feature_category: :system_ac
|
|||
shared_examples 'revokes access token' do
|
||||
it { expect(subject.success?).to be true }
|
||||
|
||||
it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.") }
|
||||
it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked.") }
|
||||
|
||||
it 'calls delete user worker' do
|
||||
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, resource_bot.id, skip_authorization: true)
|
||||
it 'does not call the delete user worker' do
|
||||
expect(DeleteUserWorker).not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'removes membership of bot user' do
|
||||
it 'bot user retains membership' do
|
||||
subject
|
||||
|
||||
expect(resource.reload).not_to have_user(resource_bot)
|
||||
end
|
||||
|
||||
it 'initiates user removal' do
|
||||
subject
|
||||
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: resource_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
expect(resource.reload).to have_user(resource_bot)
|
||||
end
|
||||
|
||||
it 'logs the event' do
|
||||
|
|
@ -44,6 +36,34 @@ RSpec.describe ResourceAccessTokens::RevokeService, feature_category: :system_ac
|
|||
|
||||
expect(Gitlab::AppLogger).to have_received(:info).with("PROJECT ACCESS TOKEN REVOCATION: revoked_by: #{user.username}, project_id: #{resource.id}, token_user: #{resource_bot.name}, token_id: #{access_token.id}")
|
||||
end
|
||||
|
||||
context 'with retain user feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
|
||||
end
|
||||
|
||||
it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.") }
|
||||
|
||||
it 'calls delete user worker' do
|
||||
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, resource_bot.id, skip_authorization: true)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'removes membership of bot user' do
|
||||
subject
|
||||
|
||||
expect(resource.reload).not_to have_user(resource_bot)
|
||||
end
|
||||
|
||||
it 'initiates user removal' do
|
||||
subject
|
||||
|
||||
expect(
|
||||
Users::GhostUserMigration.where(user: resource_bot, initiator_user: user)
|
||||
).to be_exists
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'rollback revoke steps' do
|
||||
|
|
|
|||
|
|
@ -159,27 +159,64 @@ RSpec.shared_examples 'POST resource access tokens available' do
|
|||
end
|
||||
|
||||
RSpec.shared_examples 'PUT resource access tokens available' do
|
||||
it 'calls delete user worker' do
|
||||
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true)
|
||||
context "when retain bot user ff is disabled" do
|
||||
before do
|
||||
stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
|
||||
end
|
||||
|
||||
it 'revokes the token' do
|
||||
subject
|
||||
expect(resource_access_token.reload).to be_revoked
|
||||
end
|
||||
|
||||
it 'calls delete user worker' do
|
||||
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'removes membership of bot user' do
|
||||
subject
|
||||
|
||||
resource_bots = if resource.is_a?(Project)
|
||||
resource.bots
|
||||
elsif resource.is_a?(Group)
|
||||
User.bots.id_in(resource.all_group_members.non_invite.pluck(:user_id))
|
||||
end
|
||||
|
||||
expect(resource_bots).not_to include(access_token_user)
|
||||
end
|
||||
|
||||
it 'creates GhostUserMigration records to handle migration in a worker' do
|
||||
expect { subject }.to(
|
||||
change { Users::GhostUserMigration.count }.from(0).to(1))
|
||||
end
|
||||
end
|
||||
|
||||
it 'revokes the token' do
|
||||
subject
|
||||
expect(resource_access_token.reload).to be_revoked
|
||||
end
|
||||
|
||||
it 'does not call delete user worker' do
|
||||
expect(DeleteUserWorker).not_to receive(:perform_async)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'removes membership of bot user' do
|
||||
it 'does not remove membership of the bot' do
|
||||
subject
|
||||
|
||||
resource_bots = if resource.is_a?(Project)
|
||||
resource.bots
|
||||
elsif resource.is_a?(Group)
|
||||
User.bots.id_in(resource.all_group_members.non_invite.pluck_primary_key)
|
||||
User.bots.id_in(resource.all_group_members.non_invite.pluck(:user_id))
|
||||
end
|
||||
|
||||
expect(resource_bots).not_to include(access_token_user)
|
||||
expect(resource_bots).to include(access_token_user)
|
||||
end
|
||||
|
||||
it 'creates GhostUserMigration records to handle migration in a worker' do
|
||||
expect { subject }.to(
|
||||
change { Users::GhostUserMigration.count }.from(0).to(1))
|
||||
it 'does not create GhostUserMigration records to handle migration in a worker' do
|
||||
expect { subject }.not_to change { Users::GhostUserMigration.count }
|
||||
end
|
||||
|
||||
context 'when unsuccessful' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue