Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
35d7589851
commit
45eb4bc75f
|
|
@ -1375,7 +1375,6 @@ Gitlab/BoundedContexts:
|
|||
- 'app/services/authorized_project_update/project_recalculate_per_user_service.rb'
|
||||
- 'app/services/authorized_project_update/project_recalculate_service.rb'
|
||||
- 'app/services/auto_merge/base_service.rb'
|
||||
- 'app/services/auto_merge/merge_when_pipeline_succeeds_service.rb'
|
||||
- 'app/services/auto_merge_service.rb'
|
||||
- 'app/services/award_emojis/add_service.rb'
|
||||
- 'app/services/award_emojis/base_service.rb'
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ Layout/LineLength:
|
|||
- 'app/serializers/note_entity.rb'
|
||||
- 'app/services/application_settings/update_service.rb'
|
||||
- 'app/services/auth/container_registry_authentication_service.rb'
|
||||
- 'app/services/auto_merge/merge_when_pipeline_succeeds_service.rb'
|
||||
- 'app/services/boards/base_items_list_service.rb'
|
||||
- 'app/services/boards/issues/create_service.rb'
|
||||
- 'app/services/boards/issues/move_service.rb'
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ RSpec/AnyInstanceOf:
|
|||
- 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
|
||||
- 'spec/serializers/merge_request_poll_widget_entity_spec.rb'
|
||||
- 'spec/services/application_settings/update_service_spec.rb'
|
||||
- 'spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb'
|
||||
- 'spec/services/ci/create_pipeline_service_spec.rb'
|
||||
- 'spec/services/ci/register_job_service_spec.rb'
|
||||
- 'spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@ RSpec/ReturnFromStub:
|
|||
- 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
|
||||
- 'spec/services/application_settings/update_service_spec.rb'
|
||||
- 'spec/services/auto_merge/base_service_spec.rb'
|
||||
- 'spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb'
|
||||
- 'spec/services/captcha/captcha_verification_service_spec.rb'
|
||||
- 'spec/services/ci/create_pipeline_service/logger_spec.rb'
|
||||
- 'spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ Style/IfUnlessModifier:
|
|||
- 'app/serializers/merge_request_diff_entity.rb'
|
||||
- 'app/services/application_settings/update_service.rb'
|
||||
- 'app/services/authorized_project_update/find_records_due_for_refresh_service.rb'
|
||||
- 'app/services/auto_merge/merge_when_pipeline_succeeds_service.rb'
|
||||
- 'app/services/award_emojis/destroy_service.rb'
|
||||
- 'app/services/badges/update_service.rb'
|
||||
- 'app/services/boards/issues/list_service.rb'
|
||||
|
|
|
|||
6
Gemfile
6
Gemfile
|
|
@ -45,7 +45,11 @@ gem 'bootsnap', '~> 1.18.3', require: false, feature_category: :shared
|
|||
|
||||
# Avoid the precompiled native gems because Omnibus needs to build this to ensure
|
||||
# LD_LIBRARY_PATH is correct: https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/7730
|
||||
gem 'ffi', '~> 1.17', force_ruby_platform: true, feature_category: :shared
|
||||
if RUBY_PLATFORM.include?('darwin')
|
||||
gem 'ffi', '~> 1.17', feature_category: :shared
|
||||
else
|
||||
gem 'ffi', '~> 1.17', force_ruby_platform: true, feature_category: :shared
|
||||
end
|
||||
|
||||
gem 'openssl', '~> 3.0', feature_category: :shared
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,11 @@ export function parseChronicDuration(string, opts = {}) {
|
|||
// Given an integer and an optional format,
|
||||
// returns a formatted string representing elapsed time
|
||||
export function outputChronicDuration(seconds, opts = {}) {
|
||||
if (opts.hoursOnly) {
|
||||
const hours = Math.round(seconds / 3600);
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
const units = {
|
||||
years: 0,
|
||||
months: 0,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@
|
|||
import {
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlTooltip,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { __ } from '~/locale';
|
||||
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
|
||||
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
|
|
@ -24,12 +26,13 @@ export default {
|
|||
CiIcon,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlTooltip,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
},
|
||||
styles: {
|
||||
actionSizeClasses: ['gl-h-7 gl-w-7'],
|
||||
flatLeftBorder: ['!gl-rounded-bl-none', '!gl-rounded-tl-none'],
|
||||
flatRightBorder: ['!gl-rounded-br-none', '!gl-rounded-tr-none'],
|
||||
},
|
||||
|
|
@ -108,18 +111,33 @@ export default {
|
|||
expandBtnText() {
|
||||
return this.expanded ? __('Collapse jobs') : __('Expand jobs');
|
||||
},
|
||||
childPipeline() {
|
||||
return this.isDownstream && this.isSameProject;
|
||||
isChildPipeline() {
|
||||
return this.isDownstream && !this.isMultiProject;
|
||||
},
|
||||
downstreamTitle() {
|
||||
return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
|
||||
if (this.hasPipelineName) {
|
||||
return this.pipelineName;
|
||||
}
|
||||
if (!this.hasSourceJob) {
|
||||
return this.projectName;
|
||||
}
|
||||
|
||||
return this.isMultiProject
|
||||
? `${this.sourceJobName}: ${this.projectName}`
|
||||
: this.sourceJobName;
|
||||
},
|
||||
graphqlPipelineId() {
|
||||
return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id);
|
||||
},
|
||||
hasPipelineName() {
|
||||
return Boolean(this.pipelineName);
|
||||
},
|
||||
hasUpdatePipelinePermissions() {
|
||||
return Boolean(this.pipeline?.userPermissions?.updatePipeline);
|
||||
},
|
||||
hasSourceJob() {
|
||||
return Boolean(this.pipeline?.sourceJob?.id);
|
||||
},
|
||||
isCancelable() {
|
||||
return Boolean(this.pipeline?.cancelable && this.hasUpdatePipelinePermissions);
|
||||
},
|
||||
|
|
@ -129,8 +147,8 @@ export default {
|
|||
isRetryable() {
|
||||
return Boolean(this.pipeline?.retryable && this.hasUpdatePipelinePermissions);
|
||||
},
|
||||
isSameProject() {
|
||||
return !this.pipeline.multiproject;
|
||||
isMultiProject() {
|
||||
return this.pipeline.multiproject;
|
||||
},
|
||||
isUpstream() {
|
||||
return this.type === UPSTREAM;
|
||||
|
|
@ -139,39 +157,59 @@ export default {
|
|||
if (this.parentPipeline) {
|
||||
return __('Parent');
|
||||
}
|
||||
if (this.childPipeline) {
|
||||
if (this.isChildPipeline) {
|
||||
return __('Child');
|
||||
}
|
||||
return __('Multi-project');
|
||||
},
|
||||
parentPipeline() {
|
||||
return this.isUpstream && this.isSameProject;
|
||||
return this.isUpstream && !this.isMultiProject;
|
||||
},
|
||||
pipelineIsLoading() {
|
||||
return Boolean(this.isLoading || this.pipeline.isLoading);
|
||||
},
|
||||
pipelineName() {
|
||||
return this.pipeline?.name || '';
|
||||
},
|
||||
pipelineStatus() {
|
||||
return this.pipeline.status;
|
||||
},
|
||||
projectName() {
|
||||
return this.pipeline.project.name;
|
||||
return this.pipeline?.project?.name || '';
|
||||
},
|
||||
popoverItems() {
|
||||
return [
|
||||
{
|
||||
condition: this.hasPipelineName,
|
||||
message: __('%{boldStart}Pipeline:%{boldEnd} %{value}'),
|
||||
value: this.pipelineName,
|
||||
},
|
||||
{
|
||||
condition: !this.isChildPipeline,
|
||||
message: __('%{boldStart}Project:%{boldEnd} %{value}'),
|
||||
value: this.projectName,
|
||||
},
|
||||
{
|
||||
condition: this.hasSourceJob,
|
||||
message: __('%{boldStart}Created by:%{boldEnd} %{value}'),
|
||||
value: this.sourceJobName,
|
||||
},
|
||||
{
|
||||
condition: true,
|
||||
message: __('%{boldStart}Status:%{boldEnd} %{value}'),
|
||||
value: this.pipeline.status.label,
|
||||
},
|
||||
].filter((item) => item.condition);
|
||||
},
|
||||
showAction() {
|
||||
return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
|
||||
},
|
||||
showCardTooltip() {
|
||||
showCardPopover() {
|
||||
return !this.hasActionTooltip && !this.isExpandBtnFocus;
|
||||
},
|
||||
sourceJobName() {
|
||||
return this.pipeline.sourceJob?.name ?? '';
|
||||
},
|
||||
sourceJobInfo() {
|
||||
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
|
||||
},
|
||||
cardTooltipText() {
|
||||
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
|
||||
${this.sourceJobInfo}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancelPipeline() {
|
||||
|
|
@ -234,14 +272,26 @@ export default {
|
|||
@mouseover="onDownstreamHovered"
|
||||
@mouseleave="onDownstreamHoverLeave"
|
||||
>
|
||||
<gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
|
||||
{{ cardTooltipText }}
|
||||
</gl-tooltip>
|
||||
<gl-popover
|
||||
v-if="showCardPopover"
|
||||
:target="() => $refs.linkedPipeline"
|
||||
triggers="hover"
|
||||
placement="bottom"
|
||||
>
|
||||
<div v-for="(item, index) in popoverItems" :key="index">
|
||||
<gl-sprintf :message="item.message">
|
||||
<template #bold="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #value>{{ item.value }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</gl-popover>
|
||||
<div
|
||||
class="gl-border gl-w-full gl-rounded-lg gl-border-l-section gl-bg-section gl-p-3"
|
||||
class="gl-border gl-flex gl-w-full gl-flex-col gl-gap-y-2 gl-rounded-lg gl-border-l-section gl-bg-section gl-p-3"
|
||||
:class="cardClasses"
|
||||
>
|
||||
<div class="gl-flex gl-gap-x-3">
|
||||
<div class="gl-flex gl-w-26 gl-gap-x-3">
|
||||
<ci-icon
|
||||
v-if="!pipelineIsLoading"
|
||||
:status="pipelineStatus"
|
||||
|
|
@ -249,11 +299,11 @@ export default {
|
|||
class="gl-self-start"
|
||||
/>
|
||||
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
|
||||
<div class="gl-downstream-pipeline-job-width gl-flex gl-flex-col gl-leading-normal">
|
||||
<div class="gl-flex gl-min-w-0 gl-flex-1 gl-flex-col">
|
||||
<span class="gl-truncate" data-testid="downstream-title-content">
|
||||
{{ downstreamTitle }}
|
||||
</span>
|
||||
<div class="-gl-m-2 gl-truncate gl-p-2">
|
||||
<div class="gl-truncate">
|
||||
<gl-link class="gl-text-sm" :href="pipeline.path" data-testid="pipelineLink"
|
||||
>#{{ pipeline.id }}</gl-link
|
||||
>
|
||||
|
|
@ -265,16 +315,21 @@ export default {
|
|||
:title="action.ariaLabel"
|
||||
:loading="isActionLoading"
|
||||
:icon="action.icon"
|
||||
class="!gl-rounded-full"
|
||||
:class="$options.styles.actionSizeClasses"
|
||||
class="gl-h-7 gl-w-7 !gl-rounded-full"
|
||||
:aria-label="action.ariaLabel"
|
||||
@click="action.method"
|
||||
@mouseover="setActionTooltip(true)"
|
||||
@mouseout="setActionTooltip(false)"
|
||||
/>
|
||||
<div v-else :class="$options.styles.actionSizeClasses"></div>
|
||||
</div>
|
||||
<div class="gl-ml-7 gl-pt-2">
|
||||
<span
|
||||
v-if="hasSourceJob"
|
||||
class="gl-flex gl-w-26 gl-items-center gl-gap-2 gl-text-sm gl-text-subtle"
|
||||
>
|
||||
<gl-icon name="trigger-source" :size="12" class="gl-flex-shrink-0" />
|
||||
<span class="gl-truncate"> {{ sourceJobName }} </span>
|
||||
</span>
|
||||
<div class="gl-cursor-default gl-pt-2">
|
||||
<gl-badge variant="info" data-testid="downstream-pipeline-label">
|
||||
{{ label }}
|
||||
</gl-badge>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
|
|||
id
|
||||
detailsPath
|
||||
icon
|
||||
group
|
||||
text
|
||||
}
|
||||
errorMessages {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
|
|||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import { setUrlFragment, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __, n__, sprintf, formatNumber } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
|
||||
import { reportToSentry } from '~/ci/utils';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import pipelineCiStatusUpdatedSubscription from '~/graphql_shared/subscriptions/pipeline_ci_status_updated.subscription.graphql';
|
||||
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
|
||||
import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
|
||||
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
|
||||
|
|
@ -54,6 +57,7 @@ export default {
|
|||
[DELETE_FAILURE]: __('An error occurred while deleting the pipeline.'),
|
||||
[DEFAULT]: __('An unknown error occurred.'),
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: {
|
||||
graphqlResourceEtag: {
|
||||
default: '',
|
||||
|
|
@ -64,6 +68,9 @@ export default {
|
|||
pipelineIid: {
|
||||
default: '',
|
||||
},
|
||||
pipelineId: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
pipeline: {
|
||||
|
|
@ -93,6 +100,41 @@ export default {
|
|||
this.isRetrying = false;
|
||||
}
|
||||
},
|
||||
subscribeToMore: {
|
||||
document() {
|
||||
return pipelineCiStatusUpdatedSubscription;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
pipelineId: convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipelineId),
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.showRealTimePipelineStatus;
|
||||
},
|
||||
updateQuery(
|
||||
previousData,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { ciPipelineStatusUpdated },
|
||||
},
|
||||
},
|
||||
) {
|
||||
if (ciPipelineStatusUpdated) {
|
||||
return {
|
||||
project: {
|
||||
...previousData.project,
|
||||
pipeline: {
|
||||
...previousData.project.pipeline,
|
||||
detailedStatus: ciPipelineStatusUpdated.detailedStatus,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return previousData;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
@ -225,6 +267,9 @@ export default {
|
|||
isMergeTrainPipeline() {
|
||||
return this.pipeline.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE;
|
||||
},
|
||||
showRealTimePipelineStatus() {
|
||||
return this.glFeatures.ciPipelineStatusRealtime;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reportFailure(errorType, errorMessages = []) {
|
||||
|
|
@ -328,7 +373,7 @@ export default {
|
|||
{{ commitTitle }}
|
||||
</h3>
|
||||
<div>
|
||||
<ci-icon :status="detailedStatus" show-status-text :show-link="false" class="gl-mb-3" />
|
||||
<ci-icon :status="detailedStatus" show-status-text class="gl-mb-3" />
|
||||
<div class="gl-mb-3 gl-ml-2 gl-inline-block gl-h-6">
|
||||
<gl-link
|
||||
v-if="user"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
|
|||
const {
|
||||
fullPath,
|
||||
pipelineIid,
|
||||
pipelineId,
|
||||
pipelinesPath,
|
||||
identityVerificationPath,
|
||||
identityVerificationRequired,
|
||||
|
|
@ -35,6 +36,7 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
|
|||
pipelinesPath,
|
||||
},
|
||||
pipelineIid,
|
||||
pipelineId,
|
||||
identityVerificationPath,
|
||||
identityVerificationRequired: parseBoolean(identityVerificationRequired),
|
||||
mergeTrainsAvailable: parseBoolean(mergeTrainsAvailable),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
subscription pipelineCiStatusUpdatedSubscription($pipelineId: CiPipelineID!) {
|
||||
ciPipelineStatusUpdated(pipelineId: $pipelineId) {
|
||||
id
|
||||
retryable
|
||||
cancelable
|
||||
detailedStatus {
|
||||
label
|
||||
...CiIcon
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import initWorkItemLinks from '~/work_items/components/work_item_links';
|
|||
import ZenMode from '~/zen_mode';
|
||||
import initAwardsApp from '~/emoji/awards_app';
|
||||
import { __ } from '~/locale';
|
||||
import { NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY } from '~/work_items/constants';
|
||||
import { issuableInitialDataById, isLegacyIssueType } from './show/utils/issuable_data';
|
||||
|
||||
const feedback = {};
|
||||
|
|
@ -31,6 +32,7 @@ if (gon.features?.workItemViewForIssues) {
|
|||
);
|
||||
feedback.title = __('New issue look');
|
||||
feedback.featureName = 'work_item_epic_feedback';
|
||||
feedback.expiry = NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY;
|
||||
}
|
||||
|
||||
export function initForm() {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export async function mountIssuesListApp() {
|
|||
wiReportAbusePath,
|
||||
wiNewCommentTemplatePaths,
|
||||
hasLinkedItemsEpicsFeature,
|
||||
timeTrackingLimitToHours,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
|
|
@ -202,6 +203,7 @@ export async function mountIssuesListApp() {
|
|||
hasSubepicsFeature: false,
|
||||
hasLinkedItemsEpicsFeature: parseBoolean(hasLinkedItemsEpicsFeature),
|
||||
commentTemplatePaths: JSON.parse(wiNewCommentTemplatePaths),
|
||||
timeTrackingLimitToHours: parseBoolean(timeTrackingLimitToHours),
|
||||
},
|
||||
render: (createComponent) => createComponent(IssuesListApp),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
info() {
|
||||
return Object.freeze(this.candidate.info);
|
||||
return this.candidate.info;
|
||||
},
|
||||
ciJob() {
|
||||
return Object.freeze(this.info.ciJob);
|
||||
return this.info.ciJob;
|
||||
},
|
||||
hasMetadata() {
|
||||
return !isEmpty(this.candidate.metadata);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export const i18n = {
|
|||
reassign_merge_request: s__('NotificationEvent|Merge request is reassigned'),
|
||||
reopen_issue: s__('NotificationEvent|Issue is reopened'),
|
||||
reopen_merge_request: s__('NotificationEvent|Merge request is reopened'),
|
||||
merge_when_pipeline_succeeds: s__('NotificationEvent|Merge request is set to auto-merge'),
|
||||
success_pipeline: s__('NotificationEvent|Pipeline is successful'),
|
||||
approver: s__('NotificationEvent|You are added as an approver on a merge request'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { addShortcutsExtension } from '~/behaviors/shortcuts';
|
|||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list';
|
||||
import { initWorkItemsRoot } from '~/work_items';
|
||||
import { NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY } from '~/work_items/constants';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
mountIssuesListApp();
|
||||
|
|
@ -20,6 +21,7 @@ if (gon.features.workItemViewForIssues) {
|
|||
);
|
||||
feedback.title = __('New issue look');
|
||||
feedback.featureName = 'work_item_epic_feedback';
|
||||
feedback.expiry = NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY;
|
||||
}
|
||||
|
||||
if (gon.features.workItemsViewPreference || gon.features.workItemViewForIssues) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { initForm } from 'ee_else_ce/issues';
|
|||
import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor';
|
||||
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
|
||||
import { initWorkItemsRoot } from '~/work_items';
|
||||
import { NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY } from '~/work_items/constants';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
initForm();
|
||||
|
|
@ -20,6 +21,7 @@ if (gon.features.workItemViewForIssues) {
|
|||
);
|
||||
feedback.title = __('New issue look');
|
||||
feedback.featureName = 'work_item_epic_feedback';
|
||||
feedback.expiry = NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY;
|
||||
}
|
||||
|
||||
if (gon.features.workItemsViewPreference || gon.features.workItemViewForIssues) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import { InternalEvents } from '~/tracking';
|
|||
import { HISTORY_BUTTON_CLICK } from '~/tracking/constants';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import PipelineCiStatus from '~/vue_shared/components/ci_status/pipeline_ci_status.vue';
|
||||
import SignatureBadge from '~/commit/components/signature_badge.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import pipelineCiStatusUpdatedSubscription from '~/graphql_shared/subscriptions/pipeline_ci_status_updated.subscription.graphql';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import { getRefType } from '../utils/ref_type';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
|
|
@ -19,6 +20,7 @@ import CollapsibleCommitInfo from './collapsible_commit_info.vue';
|
|||
const trackingMixin = InternalEvents.mixin();
|
||||
export default {
|
||||
components: {
|
||||
CiIcon,
|
||||
CommitInfo,
|
||||
CollapsibleCommitInfo,
|
||||
ClipboardButton,
|
||||
|
|
@ -26,7 +28,6 @@ export default {
|
|||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
PipelineCiStatus,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -59,7 +60,40 @@ export default {
|
|||
error(error) {
|
||||
throw error;
|
||||
},
|
||||
pollInterval: 30000,
|
||||
subscribeToMore: {
|
||||
document() {
|
||||
return pipelineCiStatusUpdatedSubscription;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
pipelineId: this.commit?.pipeline?.id,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.showRealTimePipelineStatus || !this.commit?.pipeline?.id;
|
||||
},
|
||||
updateQuery(
|
||||
previousData,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { ciPipelineStatusUpdated },
|
||||
},
|
||||
},
|
||||
) {
|
||||
if (ciPipelineStatusUpdated) {
|
||||
const updatedData = structuredClone(previousData);
|
||||
const pipeline =
|
||||
updatedData.project?.repository?.paginatedTree?.nodes[0]?.lastCommit?.pipelines
|
||||
?.edges[0]?.node || {};
|
||||
|
||||
pipeline.detailedStatus = ciPipelineStatusUpdated.detailedStatus;
|
||||
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
return previousData;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
|
|
@ -126,12 +160,7 @@ export default {
|
|||
<div class="commit-actions gl-my-2 gl-flex gl-items-start gl-gap-3">
|
||||
<signature-badge v-if="commit.signature" :signature="commit.signature" class="gl-h-7" />
|
||||
<div v-if="commit.pipeline.id" class="gl-ml-5 gl-flex gl-h-7 gl-items-center">
|
||||
<pipeline-ci-status
|
||||
:pipeline-id="commit.pipeline.id"
|
||||
:project-full-path="projectPath"
|
||||
:can-subscribe="showRealTimePipelineStatus"
|
||||
class="gl-mr-2"
|
||||
/>
|
||||
<ci-icon :status="commit.pipeline.detailedStatus" class="gl-mr-2" />
|
||||
</div>
|
||||
<gl-button-group class="js-commit-sha-group gl-ml-4 gl-flex gl-items-center">
|
||||
<gl-button
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export default {
|
|||
this.$el.parentElement.dispatchEvent(new Event('assignYourself'));
|
||||
|
||||
this.mediator.addSelfReview();
|
||||
this.trackEvent('assign_self_as_reviewer_in_mr');
|
||||
this.saveReviewers();
|
||||
},
|
||||
saveReviewers() {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default {
|
|||
projectsLoadingError: false,
|
||||
sortKey: this.customSortKey ?? 'STORAGE_SIZE_DESC',
|
||||
initialSortBy: this.customSortKey ? null : 'storage',
|
||||
sortableFields: { storage: !this.customSortKey },
|
||||
enableSortableFields: !this.customSortKey,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -116,10 +116,6 @@ export default {
|
|||
}
|
||||
},
|
||||
onSortChanged({ sortBy, sortDesc }) {
|
||||
if (!this.sortableFields[sortBy]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortDir = sortDesc ? 'desc' : 'asc';
|
||||
const sortKey = `${convertToSnakeCase(sortBy)}_size_${sortDir}`.toUpperCase();
|
||||
this.sortKey = sortKey;
|
||||
|
|
@ -195,7 +191,7 @@ export default {
|
|||
:is-loading="$apollo.queries.projects.loading"
|
||||
:help-links="helpLinks"
|
||||
:sort-by="initialSortBy"
|
||||
:sortable-fields="sortableFields"
|
||||
:enable-sortable-fields="enableSortableFields"
|
||||
@sortChanged="onSortChanged"
|
||||
/>
|
||||
<div class="gl-mt-5 gl-flex gl-justify-center">
|
||||
|
|
|
|||
|
|
@ -46,26 +46,27 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
sortableFields: {
|
||||
type: Object,
|
||||
enableSortableFields: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fields = [
|
||||
{ key: 'name', label: __('Project') },
|
||||
{ key: 'storage', label: __('Total'), sortable: this.sortableFields.storage },
|
||||
{ key: 'repository', label: __('Repository') },
|
||||
{ key: 'snippets', label: __('Snippets') },
|
||||
{ key: 'buildArtifacts', label: __('Job artifacts') },
|
||||
{ key: 'lfsObjects', label: __('LFS') },
|
||||
{ key: 'packages', label: __('Packages') },
|
||||
{ key: 'wiki', label: __('Wiki') },
|
||||
{ key: 'storage', label: __('Total'), sortable: this.enableSortableFields },
|
||||
{ key: 'repository', label: __('Repository'), sortable: this.enableSortableFields },
|
||||
{ key: 'snippets', label: __('Snippets'), sortable: this.enableSortableFields },
|
||||
{ key: 'buildArtifacts', label: __('Job artifacts'), sortable: this.enableSortableFields },
|
||||
{ key: 'lfsObjects', label: __('LFS'), sortable: this.enableSortableFields },
|
||||
{ key: 'packages', label: __('Packages'), sortable: this.enableSortableFields },
|
||||
{ key: 'wiki', label: __('Wiki'), sortable: this.enableSortableFields },
|
||||
{
|
||||
key: 'containerRegistry',
|
||||
label: __('Containers'),
|
||||
thClass: '!gl-border-l',
|
||||
tdClass: '!gl-border-l',
|
||||
sortable: this.enableSortableFields,
|
||||
},
|
||||
].map((f) => ({
|
||||
...f,
|
||||
|
|
|
|||
|
|
@ -12,19 +12,13 @@ export const SUCCESS = 'success';
|
|||
export const WARNING = 'warning';
|
||||
export const INFO = 'info';
|
||||
|
||||
export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
|
||||
export const MWCP_MERGE_STRATEGY = 'merge_when_checks_pass';
|
||||
export const MTWCP_MERGE_STRATEGY = 'add_to_merge_train_when_checks_pass';
|
||||
export const MT_MERGE_STRATEGY = 'merge_train';
|
||||
|
||||
export const PIPELINE_FAILED_STATE = 'failed';
|
||||
|
||||
export const AUTO_MERGE_STRATEGIES = [
|
||||
MWPS_MERGE_STRATEGY,
|
||||
MTWCP_MERGE_STRATEGY,
|
||||
MT_MERGE_STRATEGY,
|
||||
MWCP_MERGE_STRATEGY,
|
||||
];
|
||||
export const AUTO_MERGE_STRATEGIES = [MTWCP_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWCP_MERGE_STRATEGY];
|
||||
|
||||
// SP - "Suggest Pipelines"
|
||||
export const SP_TRACK_LABEL = 'no_pipeline_noticed';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
|
|||
import {
|
||||
MT_MERGE_STRATEGY,
|
||||
MWCP_MERGE_STRATEGY,
|
||||
MWPS_MERGE_STRATEGY,
|
||||
MTWCP_MERGE_STRATEGY,
|
||||
STATE_MACHINE,
|
||||
stateToTransitionMap,
|
||||
|
|
@ -362,9 +361,6 @@ export default class MergeRequestStore {
|
|||
if (availableAutoMergeStrategies.includes(MWCP_MERGE_STRATEGY)) {
|
||||
return MWCP_MERGE_STRATEGY;
|
||||
}
|
||||
if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) {
|
||||
return MWPS_MERGE_STRATEGY;
|
||||
}
|
||||
if (availableAutoMergeStrategies.includes(MTWCP_MERGE_STRATEGY)) {
|
||||
return MTWCP_MERGE_STRATEGY;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
#import "~/graphql_shared/fragments/ci_icon.fragment.graphql"
|
||||
|
||||
query pipelineCiStatus($fullPath: ID!, $pipelineId: CiPipelineID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
pipeline(id: $pipelineId) {
|
||||
id
|
||||
detailedStatus {
|
||||
label
|
||||
...CiIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { createAlert } from '~/alert';
|
||||
import { captureException } from '~/sentry/sentry_browser_wrapper';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import pipelineCiStatus from './graphql/pipeline_ci_status.query.graphql';
|
||||
import pipelineCiStatusUpdatedSubscription from './graphql/pipeline_ci_status_updated.subscription.graphql';
|
||||
|
||||
/*
|
||||
* Renders a real-time CI status for pipelines.
|
||||
* This component utilizes a GraphQL subscription to
|
||||
* get real-time status updates over a WebSocket.
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CiIcon,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
pipelineId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectFullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// Pass a feature flag value from the related view to this prop
|
||||
// to ensure skipping the subscription is possible based on the flags value.
|
||||
canSubscribe: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
status: {
|
||||
query: pipelineCiStatus,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectFullPath,
|
||||
pipelineId: this.pipelineId,
|
||||
};
|
||||
},
|
||||
update({ project }) {
|
||||
return project?.pipeline?.detailedStatus;
|
||||
},
|
||||
skip() {
|
||||
return !this.pipelineId || !this.projectFullPath;
|
||||
},
|
||||
error(error) {
|
||||
createAlert({ message: __('An error occurred fetching the pipeline status.') });
|
||||
|
||||
captureException(error);
|
||||
},
|
||||
subscribeToMore: {
|
||||
document() {
|
||||
return pipelineCiStatusUpdatedSubscription;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
pipelineId: this.pipelineId,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.canSubscribe || !this.status;
|
||||
},
|
||||
updateQuery(
|
||||
previousData,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { ciPipelineStatusUpdated },
|
||||
},
|
||||
},
|
||||
) {
|
||||
if (previousData && ciPipelineStatusUpdated)
|
||||
return {
|
||||
project: {
|
||||
...previousData.project,
|
||||
pipeline: {
|
||||
...previousData.project.pipeline,
|
||||
detailedStatus: ciPipelineStatusUpdated.detailedStatus,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return previousData;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.status.loading;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<div v-else>
|
||||
<ci-icon v-if="status" :status="status" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -129,7 +129,7 @@ export default {
|
|||
<span class="gl-block">{{ getAuthorName(item.author) }}</span>
|
||||
<time-ago
|
||||
v-if="item.createdAt"
|
||||
class="text-1"
|
||||
class="gl-text-sm"
|
||||
:time="item.createdAt"
|
||||
tooltip-placement="bottom"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-flex gl-w-full gl-justify-between">
|
||||
<div class="gl-flex gl-w-full gl-justify-between gl-gap-3">
|
||||
<div class="flex-xl-nowrap gl-flex gl-w-19/20 gl-flex-wrap gl-items-center gl-gap-3">
|
||||
<gl-icon
|
||||
v-gl-tooltip
|
||||
|
|
|
|||
|
|
@ -118,9 +118,9 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-flex gl-w-full gl-items-start gl-justify-between">
|
||||
<div class="gl-flex gl-w-full gl-items-start gl-justify-between gl-gap-3">
|
||||
<div
|
||||
class="flex-xl-nowrap gl-flex gl-min-w-0 gl-grow gl-flex-wrap gl-items-center gl-justify-between gl-gap-2 gl-pr-2"
|
||||
class="gl-flex gl-min-w-0 gl-grow gl-flex-wrap gl-items-center gl-justify-between gl-gap-3 xl:gl-flex-nowrap"
|
||||
>
|
||||
<div class="item-title gl-flex gl-min-w-0 gl-items-center gl-gap-3">
|
||||
<gl-icon
|
||||
|
|
@ -141,7 +141,7 @@ export default {
|
|||
{{ itemContent.title }}
|
||||
</gl-link>
|
||||
</div>
|
||||
<div class="item-info-area gl-flex gl-shrink-0 gl-grow gl-gap-3">
|
||||
<div class="item-info-area gl-mt-1 gl-flex gl-shrink-0 gl-grow gl-gap-3">
|
||||
<div class="item-meta gl-flex gl-grow gl-flex-wrap-reverse gl-gap-3 sm:gl-justify-between">
|
||||
<div class="item-path-area item-path-id gl-flex gl-flex-wrap gl-items-center gl-gap-3">
|
||||
<span class="gl-font-sm gl-text-subtle"> !{{ itemContent.iid }} </span>
|
||||
|
|
|
|||
|
|
@ -127,15 +127,11 @@ export default {
|
|||
<div>
|
||||
<ul
|
||||
ref="list-body"
|
||||
class="gl-m-0 gl-list-none gl-p-0"
|
||||
class="content-list"
|
||||
data-testid="work-item-dev-items-list"
|
||||
:data-list-length="list.length"
|
||||
>
|
||||
<li
|
||||
v-for="item in list"
|
||||
:key="itemId(item)"
|
||||
class="gl-border-b gl-py-4 first:!gl-pt-0 last:gl-border-none last:!gl-pb-0"
|
||||
>
|
||||
<li v-for="item in list" :key="itemId(item)">
|
||||
<component
|
||||
:is="itemComponent(item)"
|
||||
:item-content="item"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog
|
|||
import SetTimeEstimateForm from '~/sidebar/components/time_tracking/set_time_estimate_form.vue';
|
||||
import TimeTrackingReport from '~/sidebar/components/time_tracking/time_tracking_report.vue';
|
||||
|
||||
const options = { hoursPerDay: 8, daysPerMonth: 20, format: 'short' };
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
addTimeTrackingMessage: s__(
|
||||
|
|
@ -39,6 +37,7 @@ export default {
|
|||
GlModal: GlModalDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['timeTrackingLimitToHours'],
|
||||
props: {
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
|
|
@ -75,14 +74,14 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
humanTimeEstimate() {
|
||||
return outputChronicDuration(this.timeEstimate, options);
|
||||
return outputChronicDuration(this.timeEstimate, this.options);
|
||||
},
|
||||
humanTotalTimeSpent() {
|
||||
return outputChronicDuration(this.totalTimeSpent, options) ?? '0h';
|
||||
return outputChronicDuration(this.totalTimeSpent, this.options) ?? '0h';
|
||||
},
|
||||
progressBarTooltipText() {
|
||||
const timeDifference = this.totalTimeSpent - this.timeEstimate;
|
||||
const time = outputChronicDuration(Math.abs(timeDifference), options) ?? '0h';
|
||||
const time = outputChronicDuration(Math.abs(timeDifference), this.options) ?? '0h';
|
||||
return isPositiveInteger(timeDifference)
|
||||
? sprintf(s__('TimeTracking|%{time} over'), { time })
|
||||
: sprintf(s__('TimeTracking|%{time} remaining'), { time });
|
||||
|
|
@ -108,6 +107,17 @@ export default {
|
|||
timeTrackingModalId() {
|
||||
return `time-tracking-modal-${this.workItemId}`;
|
||||
},
|
||||
limitToHours() {
|
||||
return this.timeTrackingLimitToHours || false;
|
||||
},
|
||||
options() {
|
||||
return {
|
||||
hoursPerDay: 8,
|
||||
daysPerMonth: 20,
|
||||
format: 'short',
|
||||
hoursOnly: this.limitToHours,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -221,7 +231,11 @@ export default {
|
|||
size="lg"
|
||||
:title="__('Time tracking report')"
|
||||
>
|
||||
<time-tracking-report :timelogs="timelogs" :work-item-iid="workItemIid" />
|
||||
<time-tracking-report
|
||||
:limit-to-hours="limitToHours"
|
||||
:timelogs="timelogs"
|
||||
:work-item-iid="workItemIid"
|
||||
/>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ export const NEW_WORK_ITEM_IID = 'new-work-item-iid';
|
|||
export const NEW_WORK_ITEM_GID = 'gid://gitlab/WorkItem/new';
|
||||
|
||||
export const NEW_EPIC_FEEDBACK_PROMPT_EXPIRY = '2024-12-31';
|
||||
export const NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY = '2025-04-25';
|
||||
export const FEATURE_NAME = 'work_item_epic_feedback';
|
||||
|
||||
export const DETAIL_VIEW_QUERY_PARAM_NAME = 'show';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
|
|||
import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
|
||||
import { apolloProvider } from '~/graphql_shared/issuable_client';
|
||||
import { __ } from '~/locale';
|
||||
import { NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY } from '~/work_items/constants';
|
||||
import App from './components/app.vue';
|
||||
import WorkItemBreadcrumb from './components/work_item_breadcrumb.vue';
|
||||
import activeDiscussionQuery from './components/design_management/graphql/client/active_design_discussion.query.graphql';
|
||||
|
|
@ -59,6 +60,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
|
|||
canCreateProjects,
|
||||
newProjectPath,
|
||||
hasIssueDateFilterFeature,
|
||||
timeTrackingLimitToHours,
|
||||
} = el.dataset;
|
||||
|
||||
const isGroup = workspaceType === WORKSPACE_GROUP;
|
||||
|
|
@ -97,6 +99,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
|
|||
);
|
||||
feedback.title = __('New issue look');
|
||||
feedback.featureName = 'work_item_epic_feedback';
|
||||
feedback.expiry = NEW_ISSUE_FEEDBACK_PROMPT_EXPIRY;
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -151,6 +154,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
|
|||
newIssuePath: '',
|
||||
newProjectPath,
|
||||
hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
|
||||
timeTrackingLimitToHours: parseBoolean(timeTrackingLimitToHours),
|
||||
},
|
||||
mounted() {
|
||||
performanceMarkAndMeasure({
|
||||
|
|
|
|||
|
|
@ -1,9 +1 @@
|
|||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
// CSS util classes
|
||||
/**
|
||||
These are deprecated in favor of the Tailwind CSS utilities.
|
||||
Please check https://gitlab-org.gitlab.io/frontend/tailwind-documentation
|
||||
to see the available utility classes.
|
||||
**/
|
||||
@import 'utilities';
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
/**
|
||||
Please note: These are deprecated in favor of the Tailwind CSS utilities.
|
||||
Check https://gitlab-org.gitlab.io/frontend/tailwind-documentation
|
||||
to see the available utility classes. If you cannot find the class you need,
|
||||
see https://docs.gitlab.com/ee/development/fe_guide/style/scss.html#where-should-you-put-new-utility-classes.
|
||||
**/
|
||||
|
||||
$type-scale: (
|
||||
1: 12px,
|
||||
2: 14px,
|
||||
4: 20px,
|
||||
);
|
||||
|
||||
@each $index, $size in $type-scale {
|
||||
#{'.text-#{$index}'} {
|
||||
font-size: $size;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,10 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:ci_improved_project_pipeline_analytics, project)
|
||||
end
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:ci_pipeline_status_realtime, project)
|
||||
end
|
||||
|
||||
before_action only: [:new, :create, :manual_variables] do
|
||||
push_frontend_feature_flag(:ci_inputs_for_pipelines, project)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
__typename
|
||||
id
|
||||
iid
|
||||
complete
|
||||
usesNeeds
|
||||
userPermissions {
|
||||
updatePipeline
|
||||
|
|
@ -62,6 +61,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
nodes {
|
||||
# eslint-disable-next-line @graphql-eslint/require-id-when-available -- for detailedStatus.id
|
||||
...LinkedPipelineData
|
||||
name
|
||||
}
|
||||
}
|
||||
upstream {
|
||||
|
|
@ -108,7 +108,6 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
|
|||
id
|
||||
name
|
||||
kind
|
||||
scheduledAt
|
||||
needs {
|
||||
__typename
|
||||
nodes {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!, $refType:
|
|||
node {
|
||||
__typename
|
||||
id
|
||||
detailedStatus {
|
||||
__typename
|
||||
id
|
||||
detailsPath
|
||||
icon
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module Types
|
|||
description: 'Name of the input.'
|
||||
|
||||
field :value,
|
||||
Inputs::ValueInputType,
|
||||
Inputs::ValueType,
|
||||
null: true,
|
||||
description: 'Value of the input.'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This class represents a GraphQL `Input` object that allows users to set CI inputs values when running a pipeline or
|
||||
# configuring a pipeline schedule. The `Inputs` namespace refers to CI inputs, and the `InputType` class name refers to
|
||||
# a GraphQL `Input` object.
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
module Inputs
|
||||
|
|
@ -13,7 +17,7 @@ module Types
|
|||
description: 'Name of the input.'
|
||||
|
||||
argument :value,
|
||||
Inputs::ValueInputType,
|
||||
Inputs::ValueType,
|
||||
required: true,
|
||||
description: 'Value of the input.'
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ module Types
|
|||
description: 'Indicates whether the input is required.',
|
||||
method: :required?
|
||||
|
||||
field :default, Types::Ci::Inputs::ValueInputType,
|
||||
field :default, Types::Ci::Inputs::ValueType,
|
||||
null: true,
|
||||
description: 'Default value for the input, if provided.'
|
||||
|
||||
field :options, Types::Ci::Inputs::ValueInputType,
|
||||
field :options, Types::Ci::Inputs::ValueType,
|
||||
null: true,
|
||||
description: 'Possible values that the input can take, if provided.'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This class represents the value of a CI input. It is used to provide default values to UI forms that users can
|
||||
# use to populate inputs for a pipeline, and to configure inputs values for pipeline schedules.
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
module Inputs
|
||||
class ValueInputType < BaseScalar
|
||||
graphql_name 'CiInputsValueInputType'
|
||||
class ValueType < BaseScalar
|
||||
graphql_name 'CiInputsValue'
|
||||
description 'Value for a CI input. Can be a string, array, number, or boolean.'
|
||||
|
||||
def self.coerce_input(value, _ctx)
|
||||
|
|
@ -181,7 +181,8 @@ module IssuesHelper
|
|||
quick_actions_help_path: help_page_path('user/project/quick_actions.md'),
|
||||
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
|
||||
show_new_issue_link: show_new_issue_link?(project).to_s,
|
||||
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -193,7 +194,8 @@ module IssuesHelper
|
|||
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)
|
||||
new_project_path: new_project_path(namespace_id: group.id),
|
||||
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ module Projects
|
|||
full_path: project.full_path,
|
||||
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
|
||||
pipeline_iid: pipeline.iid,
|
||||
pipeline_id: pipeline.id,
|
||||
pipelines_path: project_pipelines_path(project)
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ module WorkItemsHelper
|
|||
can_create_projects: can?(current_user, :create_projects, group).to_s,
|
||||
new_project_path: new_project_path(namespace_id: group&.id),
|
||||
group_id: group&.id,
|
||||
has_issue_date_filter_feature: has_issue_date_filter_feature?(resource_parent, current_user).to_s
|
||||
has_issue_date_filter_feature: has_issue_date_filter_feature?(resource_parent, current_user).to_s,
|
||||
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AutoMerge
|
||||
class MergeWhenPipelineSucceedsService < AutoMerge::BaseService
|
||||
def execute(merge_request)
|
||||
super do
|
||||
add_system_note(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
def process(merge_request)
|
||||
logger.info("Processing Automerge - MWPS")
|
||||
return unless merge_request.diff_head_pipeline_success?
|
||||
|
||||
logger.info("Pipeline Success - MWPS")
|
||||
return unless merge_request.mergeable?
|
||||
|
||||
logger.info("Merge request mergeable - MWPS")
|
||||
|
||||
merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
|
||||
end
|
||||
|
||||
def cancel(merge_request)
|
||||
super do
|
||||
SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def abort(merge_request, reason)
|
||||
super do
|
||||
SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason)
|
||||
end
|
||||
end
|
||||
|
||||
def available_for?(_merge_request)
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_system_note(merge_request)
|
||||
SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_pipeline.sha) if merge_request.saved_change_to_auto_merge_enabled?
|
||||
end
|
||||
|
||||
def notify(merge_request)
|
||||
notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,11 +3,10 @@
|
|||
class AutoMergeService < BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds'
|
||||
STRATEGY_MERGE_WHEN_CHECKS_PASS = 'merge_when_checks_pass'
|
||||
# Only used in EE
|
||||
STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS = 'add_to_merge_train_when_checks_pass'
|
||||
STRATEGIES = [STRATEGY_MERGE_WHEN_CHECKS_PASS, STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze
|
||||
STRATEGIES = [STRATEGY_MERGE_WHEN_CHECKS_PASS].freeze
|
||||
|
||||
class << self
|
||||
def all_strategies_ordered_by_preference
|
||||
|
|
@ -22,7 +21,6 @@ class AutoMergeService < BaseService
|
|||
|
||||
def strategy_to_class_map
|
||||
{
|
||||
STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS => AutoMerge::MergeWhenPipelineSucceedsService,
|
||||
STRATEGY_MERGE_WHEN_CHECKS_PASS => AutoMerge::MergeWhenChecksPassService
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -205,8 +205,7 @@ module MergeRequests
|
|||
return unless @project.ff_merge_must_be_possible?
|
||||
|
||||
merge_requests_with_auto_merge_enabled_to(@push.branch_name).each do |merge_request|
|
||||
unless merge_request.auto_merge_strategy == AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS ||
|
||||
merge_request.auto_merge_strategy == AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS
|
||||
unless merge_request.auto_merge_strategy == AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS
|
||||
next
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -175,21 +175,6 @@ module SystemNoteService
|
|||
::SystemNotes::MergeRequestsService.new(noteable: noteable, container: project, author: author).abort_auto_merge(reason)
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is executed
|
||||
def merge_when_pipeline_succeeds(noteable, project, author, sha)
|
||||
::SystemNotes::MergeRequestsService.new(noteable: noteable, container: project, author: author).merge_when_pipeline_succeeds(sha)
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is canceled
|
||||
def cancel_merge_when_pipeline_succeeds(noteable, project, author)
|
||||
::SystemNotes::MergeRequestsService.new(noteable: noteable, container: project, author: author).cancel_merge_when_pipeline_succeeds
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is aborted
|
||||
def abort_merge_when_pipeline_succeeds(noteable, project, author, reason)
|
||||
::SystemNotes::MergeRequestsService.new(noteable: noteable, container: project, author: author).abort_merge_when_pipeline_succeeds(reason)
|
||||
end
|
||||
|
||||
def handle_merge_request_draft(noteable, project, author)
|
||||
::SystemNotes::MergeRequestsService.new(noteable: noteable, container: project, author: author).handle_merge_request_draft
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,30 +26,6 @@ module SystemNotes
|
|||
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is executed
|
||||
def merge_when_pipeline_succeeds(sha)
|
||||
body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
|
||||
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is canceled
|
||||
def cancel_merge_when_pipeline_succeeds
|
||||
body = 'canceled the automatic merge'
|
||||
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
|
||||
end
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is aborted
|
||||
def abort_merge_when_pipeline_succeeds(reason)
|
||||
body = "aborted the automatic merge because #{format_reason(reason)}"
|
||||
|
||||
##
|
||||
# TODO: Abort message should be sent by the system, not a particular user.
|
||||
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/63187.
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
|
||||
end
|
||||
|
||||
def handle_merge_request_draft
|
||||
action = noteable.draft? ? "draft" : "ready"
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
%ul
|
||||
%li
|
||||
%span= safe_format(s_('BulkImport|%{count} placeholder users matched to users.'), count: @success_count)
|
||||
%span= safe_format(ns_('BulkImport|%{count} placeholder user matched to user.', 'BulkImport|%{count} placeholder users matched to users.', @success_count), count: @success_count)
|
||||
- if @has_errors
|
||||
%li{ style: error_style }
|
||||
%strong= safe_format(s_('BulkImport|%{count} placeholder users not matched to users.'), count: @failed_count)
|
||||
%strong= safe_format(ns_('BulkImport|%{count} placeholder user not matched to user.', 'BulkImport|%{count} placeholder users not matched to users.', @failed_count), count: @failed_count)
|
||||
- if @skipped_count > 0
|
||||
%li{ style: emphasis_style }
|
||||
= safe_format(s_('BulkImport|%{count} placeholder users skipped.'), count: @skipped_count)
|
||||
= safe_format(ns_('BulkImport|%{count} placeholder user skipped.', 'BulkImport|%{count} placeholder users skipped.', @skipped_count), count: @skipped_count)
|
||||
|
||||
- if @has_errors
|
||||
%p{ style: text_style }
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<%= safe_format(s_('BulkImport|All items assigned to placeholder users were reassigned to users in %{strong_open}%{group}%{strong_close} according to the uploaded CSV file.'), strong_open: '', strong_close: '', group: @group.name) %>
|
||||
<% end %>
|
||||
|
||||
- <%= safe_format(s_('BulkImport|%{count} placeholder users matched to users.'), count: @success_count) %>
|
||||
- <%= safe_format(ns_('BulkImport|%{count} placeholder user matched to user.', 'BulkImport|%{count} placeholder users matched to users.', @success_count), count: @success_count) %>
|
||||
<% if @has_errors %>
|
||||
- <%= safe_format(s_('BulkImport|%{count} placeholder users not matched to users.'), count: @failed_count) %>
|
||||
- <%= safe_format(ns_('BulkImport|%{count} placeholder user not matched to user.', 'BulkImport|%{count} placeholder users not matched to users.', @failed_count), count: @failed_count) %>
|
||||
<% end %>
|
||||
<% if @skipped_count > 0 %>
|
||||
- <%= safe_format(s_('BulkImport|%{count} placeholder users skipped.'), count: @skipped_count) %>
|
||||
- <%= safe_format(ns_('BulkImport|%{count} placeholder user skipped.', 'BulkImport|%{count} placeholder users skipped.', @skipped_count), count: @skipped_count) %>
|
||||
<% end %>
|
||||
|
||||
<% if @has_errors %>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
description: Number of times the "assign self" button is pressed in a Merge Request
|
||||
internal_events: true
|
||||
action: assign_self_as_reviewer_in_mr
|
||||
identifiers:
|
||||
- project
|
||||
- namespace
|
||||
- user
|
||||
product_group: code_review
|
||||
product_categories:
|
||||
- code_review_workflow
|
||||
milestone: '17.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183774
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: add_timezone_to_web_operations
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64084
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitaly/-/issues/3655
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
key_path: counts.count_total_assign_self_as_reviewer_in_mr
|
||||
description: Count of times that the 'assign self' button in the reviewers section is pressed in an MR
|
||||
product_group: code_review
|
||||
product_categories:
|
||||
- code_review_workflow
|
||||
performance_indicator_type: []
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '17.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183774
|
||||
time_frame:
|
||||
- 28d
|
||||
- 7d
|
||||
- all
|
||||
data_source: internal_events
|
||||
data_category: optional
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
events:
|
||||
- name: assign_self_as_reviewer_in_mr
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
common = 'app/assets/stylesheets/framework/common.scss'
|
||||
utilities = 'app/assets/stylesheets/utilities.scss'
|
||||
|
||||
def get_css_files(files, common_filepath, utilities_filepath)
|
||||
def get_css_files(files, common_filepath)
|
||||
files.select do |file|
|
||||
file.include?(common_filepath) ||
|
||||
file.include?(utilities_filepath)
|
||||
file.include?(common_filepath)
|
||||
end
|
||||
end
|
||||
|
||||
changed_util_files = get_css_files(helper.all_changed_files.to_a, common, utilities)
|
||||
changed_util_files = get_css_files(helper.all_changed_files.to_a, common)
|
||||
|
||||
unless changed_util_files.empty?
|
||||
|
||||
|
|
@ -27,25 +25,10 @@ unless changed_util_files.empty?
|
|||
|
||||
If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, you can ignore this warning.
|
||||
|
||||
If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please add it to `#{utilities}`, following the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
|
||||
|
||||
MARKDOWN
|
||||
|
||||
end
|
||||
|
||||
if changed_util_files.include?(utilities)
|
||||
markdown(<<~MARKDOWN)
|
||||
### Addition to `#{utilities}`
|
||||
|
||||
You have added a new rule to `#{utilities}`. Are you sure you need this rule?
|
||||
|
||||
If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, consider adding it to `#{common}`
|
||||
|
||||
If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please be sure this addition follows the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss) so it may be removed when these rules are included. See [Include gitlab-ui utility-class library](https://gitlab.com/gitlab-org/gitlab/issues/36857) for more about this project.
|
||||
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
warn "This merge request adds a new rule to #{common} or #{utilities}."
|
||||
warn "This merge request adds a new rule to #{common}."
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -260,6 +260,10 @@ you need the following information:
|
|||
[Microsoft Quickstart Register an Application](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) documentation
|
||||
to obtain the tenant ID, client ID, and client secret for your app.
|
||||
|
||||
When you register a Microsoft Azure application, you must grant API permissions to allow GitLab to
|
||||
retrieve required details. You must provide at least the `openid`, `profile`, and `email` permissions.
|
||||
For more information, see the [Microsoft documentation for configuring app permissions for a web API](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis#add-permissions-to-access-microsoft-graph).
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
All accounts provisioned by Azure must have an email address defined. If an email address is not defined, Azure assigns a randomly generated address. If you've configured [domain sign-up restrictions](../settings/sign_up_restrictions.md#allow-or-deny-sign-ups-using-specific-email-domains), this random address might prevent the account from being created.
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ attribute. As a prerequisite, you must use an LDAP server that:
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328074) in GitLab 16.9.
|
||||
- [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/514025) `reverse_issuer_and_subject` and `reverse_issuer_and_serial_number` formats in GitLab 17.11.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -134,12 +135,17 @@ Use the following attributes to customize the field GitLab checks and the format
|
|||
| -------------------------- | ------------------------------------------------------------ |
|
||||
| `principal_name` | `X509:<PN>alice@example.com` |
|
||||
| `rfc822_name` | `X509:<RFC822>bob@example.com` |
|
||||
| `issuer_and_subject` | `X509:<I>DC=com,DC=example,CN=EXAMPLE-DC-CA<S>DC=com,DC=example,OU=UserAccounts,CN=cynthia` |
|
||||
| `subject` | `X509:<S>DC=com,DC=example,OU=UserAccounts,CN=dennis` |
|
||||
| `issuer_and_serial_number` | `X509:<I>DC=com,DC=example,CN=CONTOSO-DC-CA<SR>1181914561` |
|
||||
| `issuer_and_subject` | `X509:<I>DC=com,DC=example,CN=EXAMPLE-DC-CA<S>DC=com,DC=example,OU=UserAccounts,CN=cynthia` |
|
||||
| `reverse_issuer_and_serial_number` | `X509:<I>CN=CONTOSO-DC-CA,DC=example,DC=com<SR>1181914561` |
|
||||
| `reverse_issuer_and_subject` | `X509:<I>CN=EXAMPLE-DC-CA,DC=example,DC=com<S>DC=com,DC=example,OU=UserAccounts,CN=cynthia` |
|
||||
|
||||
For `issuer_and_serial_number`, the `<SR>` portion is in reverse-byte-order, with the least-significant byte first. For more information, see [how to map a user to a certificate using the altSecurityIdentities attribute](https://learn.microsoft.com/en-us/archive/blogs/spatdsg/howto-map-a-user-to-a-certificate-via-all-the-methods-available-in-the-altsecurityidentities-attribute).
|
||||
|
||||
The reverse issuer formats sort the issuer string from the smallest unit to the largest. Some
|
||||
Active Directory servers store certificates in this format.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
If no `smartcard_ad_cert_format` is specified, but an LDAP server is configured with `active_directory: true` and smart cards enabled, GitLab defaults to the behavior of 16.8 and earlier, and uses `certificateExactMatch` on the `userCertificate` attribute.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ The package registry supports the following formats:
|
|||
| [Generic packages](../../user/packages/generic_packages/_index.md) | 13.5+ |
|
||||
| [Helm Charts](../../user/packages/helm_repository/_index.md) | 14.1+ |
|
||||
|
||||
The package registry is also used to store [model registry data](../../user/project/ml/model_registry/_index.md).
|
||||
|
||||
## Accepting contributions
|
||||
|
||||
The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages/_index.md)
|
||||
|
|
|
|||
|
|
@ -22369,7 +22369,7 @@ CI input saved for a pipeline schedule.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="ciinputsfieldname"></a>`name` | [`String!`](#string) | Name of the input. |
|
||||
| <a id="ciinputsfieldvalue"></a>`value` | [`CiInputsValueInputType`](#ciinputsvalueinputtype) | Value of the input. |
|
||||
| <a id="ciinputsfieldvalue"></a>`value` | [`CiInputsValue`](#ciinputsvalue) | Value of the input. |
|
||||
|
||||
### `CiInputsSpec`
|
||||
|
||||
|
|
@ -22379,10 +22379,10 @@ Input for pipeline creation.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="ciinputsspecdefault"></a>`default` | [`CiInputsValueInputType`](#ciinputsvalueinputtype) | Default value for the input, if provided. |
|
||||
| <a id="ciinputsspecdefault"></a>`default` | [`CiInputsValue`](#ciinputsvalue) | Default value for the input, if provided. |
|
||||
| <a id="ciinputsspecdescription"></a>`description` | [`String`](#string) | Description of the input. |
|
||||
| <a id="ciinputsspecname"></a>`name` | [`String!`](#string) | Name of the input. |
|
||||
| <a id="ciinputsspecoptions"></a>`options` | [`CiInputsValueInputType`](#ciinputsvalueinputtype) | Possible values that the input can take, if provided. |
|
||||
| <a id="ciinputsspecoptions"></a>`options` | [`CiInputsValue`](#ciinputsvalue) | Possible values that the input can take, if provided. |
|
||||
| <a id="ciinputsspecregex"></a>`regex` | [`String`](#string) | Regular expression pattern that the input value must match if provided. |
|
||||
| <a id="ciinputsspecrequired"></a>`required` | [`Boolean!`](#boolean) | Indicates whether the input is required. |
|
||||
| <a id="ciinputsspectype"></a>`type` | [`CiInputsType!`](#ciinputstype) | Input data type. |
|
||||
|
|
@ -43568,7 +43568,6 @@ Representation of whether a GitLab merge request can be merged.
|
|||
| <a id="mergestrategyenumadd_to_merge_train_when_checks_pass"></a>`ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS` | Use the add_to_merge_train_when_checks_pass merge strategy. |
|
||||
| <a id="mergestrategyenummerge_train"></a>`MERGE_TRAIN` | Use the merge_train merge strategy. |
|
||||
| <a id="mergestrategyenummerge_when_checks_pass"></a>`MERGE_WHEN_CHECKS_PASS` | Use the merge_when_checks_pass merge strategy. |
|
||||
| <a id="mergestrategyenummerge_when_pipeline_succeeds"></a>`MERGE_WHEN_PIPELINE_SUCCEEDS` | Use the merge_when_pipeline_succeeds merge strategy. |
|
||||
|
||||
### `MergeTrainStatus`
|
||||
|
||||
|
|
@ -45485,7 +45484,7 @@ A `CiCatalogResourcesVersionID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `CiCatalogResourcesVersionID` is: `"gid://gitlab/Ci::Catalog::Resources::Version/1"`.
|
||||
|
||||
### `CiInputsValueInputType`
|
||||
### `CiInputsValue`
|
||||
|
||||
Value for a CI input. Can be a string, array, number, or boolean.
|
||||
|
||||
|
|
@ -47903,7 +47902,7 @@ Attributes for defining an input.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="ciinputsinputdestroy"></a>`destroy` | [`Boolean`](#boolean) | Set to `true` to delete the input. |
|
||||
| <a id="ciinputsinputname"></a>`name` | [`String!`](#string) | Name of the input. |
|
||||
| <a id="ciinputsinputvalue"></a>`value` | [`CiInputsValueInputType!`](#ciinputsvalueinputtype) | Value of the input. |
|
||||
| <a id="ciinputsinputvalue"></a>`value` | [`CiInputsValue!`](#ciinputsvalue) | Value of the input. |
|
||||
|
||||
### `CiVariableInput`
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ Supported attributes:
|
|||
| `last_used_before` | datetime (ISO 8601) | No | If defined, returns tokens last used before the specified time. |
|
||||
| `revoked` | boolean | No | If `true`, only returns revoked tokens. |
|
||||
| `search` | string | No | If defined, returns tokens that include the specified value in the name. |
|
||||
| `sort` | string | No | If defined, sorts the results by the specified value. Possible values: `created_asc`, `created_desc`, `expires_before_asc`, `expires_after_desc`, `last_used_before_asc`, `last_used_after_desc`, `name_asc`, `name_desc`. |
|
||||
| `sort` | string | No | If defined, sorts the results by the specified value. Possible values: `created_asc`, `created_desc`, `expires_asc`, `expires_desc`, `last_used_asc`, `last_used_desc`, `name_asc`, `name_desc`. |
|
||||
| `state` | string | No | If defined, returns tokens with the specified state. Possible values: `active` and `inactive`. |
|
||||
|
||||
Example request:
|
||||
|
|
@ -259,7 +259,7 @@ Example response:
|
|||
Example of unsuccessful responses:
|
||||
|
||||
- `401: Unauthorized`
|
||||
- `404 Personal access token(s) Not Found`
|
||||
- `404 Group Not Found`
|
||||
|
||||
## Create a personal access token for a service account user
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ Utility classes are generated by [Tailwind CSS](https://tailwindcss.com/). There
|
|||
|
||||
### What CSS utility classes are deprecated?
|
||||
|
||||
Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/utilities.scss)
|
||||
and [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss)
|
||||
Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss)
|
||||
are being deprecated. Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss)
|
||||
that use non-design-system values should be avoided. Use classes with conforming values instead.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Project Management
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
title: 'Tutorial: Set up a group with multiple projects for issue triage'
|
||||
---
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Free, Premium, Ultimate
|
||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
<!-- vale gitlab_base.FutureTense = NO -->
|
||||
|
||||
Issue triage is the process of categorization according to type and severity.
|
||||
As your project grows and people create more issues, it's worth creating a workflow for how you'll
|
||||
triage incoming issues.
|
||||
|
||||
In this tutorial, you'll learn how to set up a GitLab group with multiple projects for this scenario.
|
||||
|
||||
To set up GitLab for issue triage in a project:
|
||||
|
||||
1. [Create a group](#create-a-group)
|
||||
1. [Create projects inside a group](#create-projects-inside-a-group)
|
||||
1. [Decide on the criteria for types, severity, and priority](#decide-on-the-criteria-for-types-severity-and-priority)
|
||||
1. [Document your criteria](#document-your-criteria)
|
||||
1. [Create scoped labels](#create-scoped-labels)
|
||||
1. [Prioritize the new labels](#prioritize-the-new-labels)
|
||||
1. [Create a group issue triage board](#create-a-group-issue-triage-board)
|
||||
1. [Create issues for features](#create-issues-for-features)
|
||||
|
||||
## Before you begin
|
||||
|
||||
- If you're using an existing project for this tutorial, make sure you have at least the Reporter role
|
||||
for the project.
|
||||
- If your existing project does not have a parent group, create a group and [promote the project labels to group labels](../../user/project/labels.md#promote-a-project-label-to-a-group-label).
|
||||
|
||||
## Create a group
|
||||
|
||||
A [group](../../user/group/_index.md) is, in essence, a container for multiple projects. It allows users to manage multiple projects and communicate with group members all at once.
|
||||
|
||||
To create a new group:
|
||||
|
||||
1. On the left sidebar, at the top, select **Create new** ({{< icon name="plus" >}}) and select **New group**.
|
||||
1. Select **Create group**.
|
||||
1. Enter the group details.
|
||||
- For **Group name**, enter `triage-tutorial`.
|
||||
1. Select **Create group** at the bottom of the page.
|
||||
|
||||
## Create projects inside a group
|
||||
|
||||
To manage issue-tracking across multiple projects, you need to create at least two projects inside your group.
|
||||
|
||||
To create a new project:
|
||||
|
||||
1. On the left sidebar, at the top, select **Create new** ({{< icon name="plus" >}}) and **New project/repository**.
|
||||
1. Select **Create blank project**.
|
||||
1. Enter the project details:
|
||||
- For **Project name**, enter `test-project-1`. For more information, see project [naming rules](../../user/reserved_names.md#rules-for-usernames-project-and-group-names-and-slugs).
|
||||
1. Select **Create project** at the bottom of the page.
|
||||
1. Repeat this process to create a second project named `test-project-2`.
|
||||
|
||||
## Decide on the criteria for types, severity, and priority
|
||||
|
||||
Next, you'll need to determine:
|
||||
|
||||
- **Types** of issues you want to recognize. If you need a more granular approach, you
|
||||
can also create subtypes for each type. Types help categorize work to get an understanding of the
|
||||
kind of work that is requested of your team.
|
||||
- Levels of **priorities** and **severities** to define the impact that incoming work has on end
|
||||
users and to assist in prioritization.
|
||||
|
||||
For this tutorial, suppose you've decided on the following:
|
||||
|
||||
- Type: `Bug`, `Feature`, and `Maintenance`
|
||||
- Priority: `1`, `2`, `3`, and `4`
|
||||
- Severity: `1`, `2`, `3`, and `4`
|
||||
|
||||
For inspiration, see how we define these at GitLab:
|
||||
|
||||
- [Types and subtypes](https://handbook.gitlab.com/handbook/engineering/metrics/#work-type-classification)
|
||||
- [Priority](https://handbook.gitlab.com/handbook/engineering/infrastructure/engineering-productivity/issue-triage/#priority)
|
||||
- [Severity](https://handbook.gitlab.com/handbook/engineering/infrastructure/engineering-productivity/issue-triage/#severity)
|
||||
|
||||
## Document your criteria
|
||||
|
||||
After you agree on all the criteria, write it all down somewhere your team mates can always access.
|
||||
|
||||
For example, add it to a [wiki](../../user/project/wiki/_index.md) in your project, or your company
|
||||
handbook published with [GitLab Pages](../../user/project/pages/_index.md).
|
||||
|
||||
<!-- Idea for expanding this tutorial:
|
||||
Add steps for [creating a wiki page](../../user/project/wiki/_index.md#create-a-new-wiki-page). -->
|
||||
|
||||
## Create scoped labels
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
Next, you'll create labels to add to issues to categorize them.
|
||||
|
||||
The best tool for this is [scoped labels](../../user/project/labels.md#scoped-labels), which you
|
||||
can use to set mutually exclusive attributes.
|
||||
|
||||
Checking with the list of types, severities, and priorities you've assembled
|
||||
[previously](#decide-on-the-criteria-for-types-severity-and-priority), you'll want to create matching
|
||||
scoped labels.
|
||||
|
||||
The double colon (`::`) in the name of a scoped label prevents two labels of the same scope being
|
||||
used together.
|
||||
For example, if you add the `type::feature` label to an issue that already has `type::bug`, the
|
||||
previous one is removed.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Scoped labels are available in the Premium and Ultimate tier.
|
||||
If you're on the Free tier, you can use regular labels instead.
|
||||
However, they aren't mutually exclusive.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
To create each label:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Manage > Labels**.
|
||||
1. Select **New label**.
|
||||
1. In the **Title** field, enter the name of the label. Start with `type::bug`.
|
||||
1. Optional. Select a color from the available colors, or enter a hex color value for
|
||||
a specific color in the **Background color** field.
|
||||
1. Select **Create label**.
|
||||
|
||||
Repeat steps 3-6 to create all the labels you need.
|
||||
Here are some examples:
|
||||
|
||||
- `type::bug`
|
||||
- `type::feature`
|
||||
- `type::maintenance`
|
||||
- `priority::1`
|
||||
- `priority::2`
|
||||
- `priority::3`
|
||||
- `priority::4`
|
||||
- `severity::1`
|
||||
- `severity::2`
|
||||
- `severity::3`
|
||||
- `severity::4`
|
||||
|
||||
## Prioritize the new labels
|
||||
|
||||
Now, set the new labels as priority labels.
|
||||
Doing this ensures that the most important issues show on top
|
||||
of the issue list if you sort by priority or label priority.
|
||||
|
||||
To learn what happens when you sort by priority or label priority, see
|
||||
[Sorting and ordering issue lists](../../user/project/issues/sorting_issue_lists.md).
|
||||
|
||||
To prioritize labels:
|
||||
|
||||
1. On the Labels page, next to a label you want to prioritize, select **Prioritize** ({{< icon name="star-o" >}}).
|
||||
This label now appears at the top of the label list, under **Prioritized labels**.
|
||||
1. To change the relative priority of these labels, drag them up and down the list.
|
||||
Labels higher in the list get higher priority.
|
||||
1. Prioritize all the labels you previously created.
|
||||
Make sure that labels of higher priority and severity are higher on the list than those of lower values.
|
||||
|
||||

|
||||
|
||||
## Create a group issue triage board
|
||||
|
||||
To prepare for the incoming issue backlog, create an [issue board](../../user/project/issue_board.md) that organizes issues by label.
|
||||
You'll use it to quickly create issues and add labels to them by dragging cards to various lists.
|
||||
|
||||
To set up your issue board:
|
||||
|
||||
1. Decide on the scope of the board.
|
||||
For example, create a group issue board](../../user/project/issue_board.md#group-issue-boards that you'll use to assign
|
||||
severity to issues.
|
||||
1. On the left sidebar, select **Search or go to** and find your **triage-tutorial** group.
|
||||
1. Select **Plan > Issue boards**.
|
||||
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
|
||||
1. Select **Create new board**.
|
||||
1. In the **Title** field, enter `Issue triage (by severity)`.
|
||||
1. Keep the **Show the Open list** checkbox selected and clear the **Show the Closed list** one.
|
||||
1. Select **Create board**. You should see an empty board.
|
||||
1. Create a list for the `severity::1` label:
|
||||
1. In the upper-right corner of the issue board page, select **Create list**.
|
||||
1. In the column that appears, from the **Value** dropdown list, select the `severity::1` label.
|
||||
1. At the bottom of the list, select **Add to board**.
|
||||
1. Repeat the previous step for labels `severity::2`, `severity::3`, and `severity::4`.
|
||||
|
||||
For now, the lists in your board should be empty. Next, you'll populate them with some issues.
|
||||
|
||||
## Create issues for features
|
||||
|
||||
To track upcoming features and bugs, you must create some issues.
|
||||
Issues belong in projects, but you can also create them directly from your group issue board.
|
||||
|
||||
Start by creating some issues for planned features.
|
||||
You can create issues for bugs as you find them (hopefully not too many!).
|
||||
|
||||
To create an issue from your **Issue triage (by severity)** board:
|
||||
|
||||
1. Go to the **Open** list.
|
||||
This list shows issues that don't fit any other board list.
|
||||
If you already know which severity label your issue should have, you can create it directly from
|
||||
that label's list.
|
||||
Keep in mind that each issue created from a label list is given that label.
|
||||
|
||||
For now, we'll proceed with using the **Open** list.
|
||||
1. On the **Open** list, select the **Create new issue** icon ({{< icon name="plus" >}}).
|
||||
1. Complete the fields:
|
||||
- Under **Title**, enter `User registration`.
|
||||
- Select the project to which this issue applies. We will select `test-project-1`.
|
||||
1. Select **Create issue**.
|
||||
1. Repeat these steps to create a few more issues.
|
||||
|
||||
For example, if you're building an app, imagine that `test-project-1` and `test-project-2` refer to
|
||||
the application's backend and frontend respectively.
|
||||
Create the following issues, assigning to projects as you see fit:
|
||||
|
||||
- `User registration`
|
||||
- `Profile creation`
|
||||
- `Search functionality`
|
||||
- `Add to favorites`
|
||||
- `Push notifications`
|
||||
- `Social sharing`
|
||||
- `In-app messaging`
|
||||
- `Track progress`
|
||||
- `Feedback and ratings`
|
||||
- `Settings and preferences`
|
||||
|
||||
Your first triage issue board is ready!
|
||||
Try it out by dragging some issues from the **Open** list to one of the label lists to add one of
|
||||
the severity labels.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
Next, you can:
|
||||
|
||||
- Tweak how you use issue boards. Some options include:
|
||||
- Edit your current issue board to also have lists for priority and type labels.
|
||||
This way, you'll make the board wider and might require some horizontal scrolling.
|
||||
- Create separate issue boards named `Issue triage (by priority)` and `Issue triage (by type)`.
|
||||
This way, you'll keep various types of triage work separate, but will require switching between
|
||||
boards.
|
||||
- [Set up issue boards for team hand-off](../boards_for_teams/_index.md).
|
||||
- Browse issues by priority or severity in issue lists,
|
||||
[filtered by each label](../../user/project/issues/managing_issues.md#filter-the-list-of-issues).
|
||||
If it's available to you, make use of
|
||||
[the "is one of" filter operator](../../user/project/issues/managing_issues.md#filter-with-the-or-operator).
|
||||
- Break the issues down into [tasks](../../user/tasks.md).
|
||||
- Create policies that help automate issue triage in a project with the [`gitlab-triage` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage).
|
||||
Generate summary reports with heatmaps like the following:
|
||||
|
||||

|
||||
|
||||
To learn more about issue triage at GitLab, see [Issue Triage](https://handbook.gitlab.com/handbook/engineering/infrastructure/engineering-productivity/issue-triage/)
|
||||
and [Triage Operations](https://handbook.gitlab.com/handbook/engineering/infrastructure/engineering-productivity/triage-operations/).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
|
|
@ -28,8 +28,6 @@ Custom roles allow an organization to create user roles with the precise privile
|
|||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo of the custom roles feature, see [[Demo] Ultimate Guest can view code on private repositories via custom role](https://www.youtube.com/watch?v=46cp_-Rtxps).
|
||||
|
||||
You can discuss individual custom role and permission requests in [issue 391760](https://gitlab.com/gitlab-org/gitlab/-/issues/391760).
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Most custom roles are considered [billable users that use a seat](#billing-and-seat-usage). When you add a user to your group with a custom role and you are about to incur additional charges for having more seats than are included in your subscription, a warning is displayed.
|
||||
|
|
@ -362,10 +360,9 @@ For information on available permissions, see [custom permissions](abilities.md)
|
|||
|
||||
## Contribute new permissions
|
||||
|
||||
If a permission doesn't exist, you can:
|
||||
|
||||
If a permission does not exist, you can:
|
||||
|
||||
- Discuss individual custom role and permission requests in [issue 391760](https://gitlab.com/gitlab-org/gitlab/-/issues/391760).
|
||||
- Create an issue to request the permission with the [permission proposal issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Permission%2520Proposal).
|
||||
- Contribute to GitLab and [add the permission](../../development/permissions/custom_roles.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,102 @@ If through group sync the user was assigned:
|
|||
- A higher role, they are a direct member of the group.
|
||||
- A role that is the same as or lower, they are an inherited member of the group.
|
||||
|
||||
## Automatic member removal
|
||||
|
||||
After a group sync, users who are not members of a mapped SAML group are removed from the group.
|
||||
On GitLab.com, users in the top-level group are assigned the
|
||||
default membership role instead of being removed.
|
||||
|
||||
For example, in the following diagram:
|
||||
|
||||
- Alex Garcia signs into GitLab and is removed from GitLab Group C because they don't belong
|
||||
to SAML Group C.
|
||||
- Sidney Jones belongs to SAML Group C, but is not added to GitLab Group C because they have
|
||||
not yet signed in.
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: How group membership of users is determined before sign in if group sync is set up.
|
||||
|
||||
subgraph SAML users
|
||||
SAMLUserA[Sidney Jones]
|
||||
SAMLUserB[Zhang Wei]
|
||||
SAMLUserC[Alex Garcia]
|
||||
SAMLUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph SAML groups
|
||||
SAMLGroupA["Group A"] --> SAMLGroupB["Group B"]
|
||||
SAMLGroupA --> SAMLGroupC["Group C"]
|
||||
SAMLGroupA --> SAMLGroupD["Group D"]
|
||||
end
|
||||
|
||||
SAMLGroupB --> |Member|SAMLUserA
|
||||
SAMLGroupB --> |Member|SAMLUserB
|
||||
|
||||
SAMLGroupC --> |Member|SAMLUserA
|
||||
SAMLGroupC --> |Member|SAMLUserB
|
||||
|
||||
SAMLGroupD --> |Member|SAMLUserD
|
||||
SAMLGroupD --> |Member|SAMLUserC
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: User membership for Sidney when she has not signed into group C, and group B has not configured group links.
|
||||
|
||||
subgraph GitLab users
|
||||
GitLabUserA[Sidney Jones]
|
||||
GitLabUserB[Zhang Wei]
|
||||
GitLabUserC[Alex Garcia]
|
||||
GitLabUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph GitLab groups
|
||||
GitLabGroupA["Group A<br> (SAML configured)"] --> GitLabGroupB["Group B<br> (SAML Group Link not configured)"]
|
||||
GitLabGroupA --> GitLabGroupC["Group C<br> (SAML Group Link configured)"]
|
||||
GitLabGroupA --> GitLabGroupD["Group D<br> (SAML Group Link configured)"]
|
||||
end
|
||||
|
||||
GitLabGroupB --> |Member|GitLabUserA
|
||||
|
||||
GitLabGroupC --> |Member|GitLabUserB
|
||||
GitLabGroupC --> |Member|GitLabUserC
|
||||
|
||||
GitLabGroupD --> |Member|GitLabUserC
|
||||
GitLabGroupD --> |Member|GitLabUserD
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: How membership of Alex Garcia works once she has signed into a group that has group links enabled.
|
||||
|
||||
subgraph GitLab users
|
||||
GitLabUserA[Sidney Jones]
|
||||
GitLabUserB[Zhang Wei]
|
||||
GitLabUserC[Alex Garcia]
|
||||
GitLabUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph GitLab groups after Alex Garcia signs in
|
||||
GitLabGroupA[Group A]
|
||||
GitLabGroupA["Group A<br> (SAML configured)"] --> GitLabGroupB["Group B<br> (SAML Group Link not configured)"]
|
||||
GitLabGroupA --> GitLabGroupC["Group C<br> (SAML Group Link configured)"]
|
||||
GitLabGroupA --> GitLabGroupD["Group D<br> (SAML Group Link configured)"]
|
||||
end
|
||||
|
||||
GitLabGroupB --> |Member|GitLabUserA
|
||||
GitLabGroupC --> |Member|GitLabUserB
|
||||
GitLabGroupD --> |Member|GitLabUserC
|
||||
GitLabGroupD --> |Member|GitLabUserD
|
||||
```
|
||||
|
||||
## Configure SAML Group Sync
|
||||
|
||||
Adding or changing a group sync configuration may remove users from the mapped GitLab group,
|
||||
|
|
@ -232,34 +328,34 @@ Microsoft has [announced](https://azure.microsoft.com/en-us/updates/azure-ad-is-
|
|||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo of group sync using Microsoft Azure, see [Demo: SAML Group Sync](https://youtu.be/Iqvo2tJfXjg).
|
||||
|
||||
Azure AD sends up to 150 groups in the groups claim. When users are members of more than 150 groups Azure AD sends a
|
||||
group overage claim attribute in the SAML response. Then group memberships must be obtained using the Microsoft Graph API.
|
||||
Azure AD sends up to 150 groups in the `groups` claim.
|
||||
When using Azure AD with SAML Group Sync, if a user in your organization is a member of more than 150 groups,
|
||||
Azure AD sends a `groups` claim attribute in the SAML response for [group overages](https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles#group-overages),
|
||||
and the user may be automatically removed from groups.
|
||||
|
||||
The [Graph API endpoint](https://learn.microsoft.com/en-us/graph/api/user-list-transitivememberof?view=graph-rest-1.0&tabs=http#http-request) supports only a
|
||||
[user object ID](https://learn.microsoft.com/en-us/partner-center/find-ids-and-domain-names#find-the-user-object-id) or
|
||||
[userPrincipalName](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/plan-connect-userprincipalname#what-is-userprincipalname)
|
||||
as the [configured](_index.md#azure) Unique User Identifier (Name identifier) attribute.
|
||||
To avoid this issue, you can use the Azure AD integration, which:
|
||||
|
||||
When the integration processes Group Sync, only Group Links configured with
|
||||
group unique identifiers (like `12345678-9abc-def0-1234-56789abcde`) are supported.
|
||||
- Is not limited to 150 groups.
|
||||
- Uses the Microsoft Graph API to obtain all user memberships.
|
||||
The [Graph API endpoint](https://learn.microsoft.com/en-us/graph/api/user-list-transitivememberof?view=graph-rest-1.0&tabs=http#http-request) accepts only a
|
||||
[user object ID](https://learn.microsoft.com/en-us/partner-center/find-ids-and-domain-names#find-the-user-object-id) or
|
||||
[userPrincipalName](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/plan-connect-userprincipalname#what-is-userprincipalname)
|
||||
as the [Azure-configured](_index.md#azure) Unique User Identifier (Name identifier) attribute.
|
||||
- Supports only Group Links configured with group unique identifiers (like `12345678-9abc-def0-1234-56789abcde`)
|
||||
when it processes Group Sync.
|
||||
|
||||
To integrate Microsoft Azure AD, you must:
|
||||
Alternatively, you can change the [group claims](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-group-claims#configure-the-microsoft-entra-application-registration-for-group-attributes) to use the **Groups assigned to the application** option.
|
||||
|
||||
1. Configure Azure AD to enable GitLab to communicate with the Microsoft Graph API.
|
||||
1. Configure GitLab.
|
||||
|
||||
The following table lists the GitLab settings and the corresponding Azure AD fields for the configuration:
|
||||
|
||||
| GitLab setting | Azure field |
|
||||
| -------------- | ------------------------------------------ |
|
||||
| Tenant ID | Directory (tenant) ID |
|
||||
| Client ID | Application (client) ID |
|
||||
| Client Secret | Value (on **Certificates & secrets** page) |
|
||||

|
||||
|
||||
### Configure Azure AD
|
||||
|
||||
As part of the integration, you must allow GitLab to communicate with the Microsoft Graph API.
|
||||
|
||||
<!-- vale gitlab_base.SentenceSpacing = NO -->
|
||||
|
||||
To configure Azure AD:
|
||||
|
||||
1. In the [Azure Portal](https://portal.azure.com), go to **Microsoft Entra ID > App registrations > All applications**, and select your GitLab SAML application.
|
||||
1. Under **Essentials**, the **Application (client) ID** and **Directory (tenant) ID** values are displayed. Copy these values, because you need them for the GitLab configuration.
|
||||
1. In the left navigation, select **Certificates & secrets**.
|
||||
|
|
@ -278,10 +374,20 @@ The following table lists the GitLab settings and the corresponding Azure AD fie
|
|||
|
||||
### Configure GitLab
|
||||
|
||||
With this configuration, if a user signs in with SAML and Azure sends a group overage claim in the response,
|
||||
After you configure Azure AD, you must configure GitLab to communicate with Azure AD.
|
||||
|
||||
With this configuration, if a user signs in with SAML and Azure sends a `group` claim in the response,
|
||||
GitLab initiates a Group Sync job to call the Microsoft Graph API and retrieve the user's group membership.
|
||||
Then the GitLab group membership is updated according to SAML Group Links.
|
||||
|
||||
The following table lists the GitLab settings and the corresponding Azure AD fields for the configuration:
|
||||
|
||||
| GitLab setting | Azure field |
|
||||
| -------------- | ------------------------------------------ |
|
||||
| Tenant ID | Directory (tenant) ID |
|
||||
| Client ID | Application (client) ID |
|
||||
| Client Secret | Value (on **Certificates & secrets** page) |
|
||||
|
||||
{{< tabs >}}
|
||||
|
||||
{{< tab title="GitLab.com" >}}
|
||||
|
|
@ -358,113 +464,3 @@ To enable global group memberships lock:
|
|||
1. Select **Settings > General**.
|
||||
1. Expand the **Visibility and access controls** section.
|
||||
1. Ensure that **Lock memberships to SAML Group Links synchronization** is selected.
|
||||
|
||||
## Automatic member removal
|
||||
|
||||
After a group sync, users who are not members of a mapped SAML group are removed from the group.
|
||||
On GitLab.com, users in the top-level group are assigned the
|
||||
default membership role instead of being removed.
|
||||
|
||||
For example, in the following diagram:
|
||||
|
||||
- Alex Garcia signs into GitLab and is removed from GitLab Group C because they don't belong
|
||||
to SAML Group C.
|
||||
- Sidney Jones belongs to SAML Group C, but is not added to GitLab Group C because they have
|
||||
not yet signed in.
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: How group membership of users is determined before sign in if group sync is set up.
|
||||
|
||||
subgraph SAML users
|
||||
SAMLUserA[Sidney Jones]
|
||||
SAMLUserB[Zhang Wei]
|
||||
SAMLUserC[Alex Garcia]
|
||||
SAMLUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph SAML groups
|
||||
SAMLGroupA["Group A"] --> SAMLGroupB["Group B"]
|
||||
SAMLGroupA --> SAMLGroupC["Group C"]
|
||||
SAMLGroupA --> SAMLGroupD["Group D"]
|
||||
end
|
||||
|
||||
SAMLGroupB --> |Member|SAMLUserA
|
||||
SAMLGroupB --> |Member|SAMLUserB
|
||||
|
||||
SAMLGroupC --> |Member|SAMLUserA
|
||||
SAMLGroupC --> |Member|SAMLUserB
|
||||
|
||||
SAMLGroupD --> |Member|SAMLUserD
|
||||
SAMLGroupD --> |Member|SAMLUserC
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: User membership for Sidney when she has not signed into group C, and group B has not configured group links.
|
||||
|
||||
subgraph GitLab users
|
||||
GitLabUserA[Sidney Jones]
|
||||
GitLabUserB[Zhang Wei]
|
||||
GitLabUserC[Alex Garcia]
|
||||
GitLabUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph GitLab groups
|
||||
GitLabGroupA["Group A<br> (SAML configured)"] --> GitLabGroupB["Group B<br> (SAML Group Link not configured)"]
|
||||
GitLabGroupA --> GitLabGroupC["Group C<br> (SAML Group Link configured)"]
|
||||
GitLabGroupA --> GitLabGroupD["Group D<br> (SAML Group Link configured)"]
|
||||
end
|
||||
|
||||
GitLabGroupB --> |Member|GitLabUserA
|
||||
|
||||
GitLabGroupC --> |Member|GitLabUserB
|
||||
GitLabGroupC --> |Member|GitLabUserC
|
||||
|
||||
GitLabGroupD --> |Member|GitLabUserC
|
||||
GitLabGroupD --> |Member|GitLabUserD
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
graph TB
|
||||
accTitle: Automatic member removal
|
||||
accDescr: How membership of Alex Garcia works once she has signed into a group that has group links enabled.
|
||||
|
||||
subgraph GitLab users
|
||||
GitLabUserA[Sidney Jones]
|
||||
GitLabUserB[Zhang Wei]
|
||||
GitLabUserC[Alex Garcia]
|
||||
GitLabUserD[Charlie Smith]
|
||||
end
|
||||
|
||||
subgraph GitLab groups after Alex Garcia signs in
|
||||
GitLabGroupA[Group A]
|
||||
GitLabGroupA["Group A<br> (SAML configured)"] --> GitLabGroupB["Group B<br> (SAML Group Link not configured)"]
|
||||
GitLabGroupA --> GitLabGroupC["Group C<br> (SAML Group Link configured)"]
|
||||
GitLabGroupA --> GitLabGroupD["Group D<br> (SAML Group Link configured)"]
|
||||
end
|
||||
|
||||
GitLabGroupB --> |Member|GitLabUserA
|
||||
GitLabGroupC --> |Member|GitLabUserB
|
||||
GitLabGroupD --> |Member|GitLabUserC
|
||||
GitLabGroupD --> |Member|GitLabUserD
|
||||
```
|
||||
|
||||
### User that belongs to many SAML groups automatically removed from GitLab group
|
||||
|
||||
When using Azure AD with SAML, if any user in your organization is a member of more than 150 groups and you use SAML Group Sync,
|
||||
that user may lose their group memberships.
|
||||
For more information, see
|
||||
[Microsoft Group overages](https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles#group-overages).
|
||||
|
||||
The Microsoft Azure AD integration uses the Microsoft Graph API to obtain all user memberships and is
|
||||
not limited to 150 groups.
|
||||
|
||||
Otherwise, you can work around this issue by changing the [group claims](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-group-claims#configure-the-microsoft-entra-application-registration-for-group-attributes) to use the `Groups assigned to the application` option instead.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ If all users are receiving a `404` after signing in to the identity provider (Id
|
|||
- In the GitLab configuration by [matching it to the HTTPS endpoint of GitLab](../../../integration/saml.md#configure-saml-support-in-gitlab).
|
||||
- As the `Assertion Consumer Service URL` or equivalent when setting up the SAML app on your IdP.
|
||||
|
||||
- Verify if the `404` is related to [the user having too many groups assigned to them in their Azure IdP](group_sync.md#user-that-belongs-to-many-saml-groups-automatically-removed-from-gitlab-group).
|
||||
- Verify if the `404` is related to [the user having too many groups assigned to them in their Azure IdP](group_sync.md#microsoft-azure-active-directory-integration).
|
||||
|
||||
If a subset of users are receiving a `404` after signing in to the IdP, first verify audit events if the user gets added to the group and then immediately removed. Alternatively, if the user can successfully sign in, but they do not show as [a member of the top-level group](../_index.md#search-a-group):
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ Users are notified of the following events:
|
|||
| Group access expired | Group members | Sent when user's access to a group expires in seven days. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12704) in GitLab 16.3._ |
|
||||
| Project access expired | Project members | Sent when user's access to a project expires in seven days. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12704) in GitLab 16.3._ |
|
||||
| Group scheduled for deletion | Group Owners | Sent when group is scheduled for deletion. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/522883) in GitLab 17.11_ |
|
||||
| Project scheduled for deletion | Project Owners | Sent when project is scheduled for deletion. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/522883) in GitLab 17.11_ |
|
||||
|
||||
## Notifications on issues, merge requests, and epics
|
||||
|
||||
|
|
|
|||
|
|
@ -115,9 +115,11 @@ The **Add status check** form is then shown.
|
|||
|
||||
Filling in the form and selecting the **Add status check** button creates a new status check.
|
||||
|
||||
The status check is applied to all new merge requests, but does not apply retroactively to existing merge requests.
|
||||
|
||||
### Update a status check service
|
||||
|
||||
Within the **Status checks** sub-section, select **Edit** ({{< icon name="pencil" >}})
|
||||
In the **Status checks** sub-section, select **Edit** ({{< icon name="pencil" >}})
|
||||
next to the status check you want to edit.
|
||||
The **Update status check** form is then shown.
|
||||
|
||||
|
|
@ -129,7 +131,9 @@ You cannot see or modify the value of the HMAC shared secret. To change the shar
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
Changing the values in the form and selecting the **Update status check** button updates the status check.
|
||||
To update the status check, change the values in the form and select **Update status check**.
|
||||
|
||||
Status check updates are applied to all new merge requests, but do not apply retroactively to existing merge requests.
|
||||
|
||||
### Form values
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ For more information about model registry features and capabilities, see [epic 9
|
|||
|
||||
## Access the model registry
|
||||
|
||||
The model registry is controlled by the package registry setting.
|
||||
Before using the model registry, make
|
||||
sure the [package registry is enabled](../../../../administration/packages/_index.md#enable-or-disable-the-package-registry).
|
||||
|
||||
To access the model registry, on the left sidebar, select **Deploy > Model registry**.
|
||||
|
||||
If **Model registry** is not available, ensure that it has been enabled.
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ module Gitlab
|
|||
@email = email
|
||||
@gl_id = gl_id
|
||||
|
||||
@timezone = if Feature.enabled?(:add_timezone_to_web_operations)
|
||||
convert_timezone(timezone)
|
||||
else
|
||||
system_timezone
|
||||
end
|
||||
@timezone = convert_timezone(timezone)
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
|
|
|
|||
|
|
@ -640,6 +640,9 @@ msgstr ""
|
|||
msgid "%{board_target} not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Created by:%{boldEnd} %{value}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Merge blocked:%{boldEnd} %{count} check failed"
|
||||
msgid_plural "%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed"
|
||||
msgstr[0] ""
|
||||
|
|
@ -648,6 +651,12 @@ msgstr[1] ""
|
|||
msgid "%{boldStart}Merge with caution%{boldEnd}: Override added"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Pipeline:%{boldEnd} %{value}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Project:%{boldEnd} %{value}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Ready to be merged with caution%{boldEnd}: Override added"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -657,6 +666,9 @@ msgstr ""
|
|||
msgid "%{boldStart}Ready to merge!%{boldEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{boldStart}Status:%{boldEnd} %{value}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{bold_start}%{count}%{bold_end} issue"
|
||||
msgid_plural "%{bold_start}%{count}%{bold_end} issues"
|
||||
msgstr[0] ""
|
||||
|
|
@ -6652,9 +6664,6 @@ msgstr ""
|
|||
msgid "An error occurred fetching the dropdown data."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred fetching the pipeline status."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred fetching the project authors."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11037,14 +11046,20 @@ msgstr ""
|
|||
msgid "BulkImport| %{host} is running outdated GitLab version (v%{version})"
|
||||
msgstr ""
|
||||
|
||||
msgid "BulkImport|%{count} placeholder users matched to users."
|
||||
msgstr ""
|
||||
msgid "BulkImport|%{count} placeholder user matched to user."
|
||||
msgid_plural "BulkImport|%{count} placeholder users matched to users."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "BulkImport|%{count} placeholder users not matched to users."
|
||||
msgstr ""
|
||||
msgid "BulkImport|%{count} placeholder user not matched to user."
|
||||
msgid_plural "BulkImport|%{count} placeholder users not matched to users."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "BulkImport|%{count} placeholder users skipped."
|
||||
msgstr ""
|
||||
msgid "BulkImport|%{count} placeholder user skipped."
|
||||
msgid_plural "BulkImport|%{count} placeholder users skipped."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "BulkImport|%{feature} (require v%{version})"
|
||||
msgstr ""
|
||||
|
|
@ -17935,9 +17950,6 @@ msgstr ""
|
|||
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "Created by %{job}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Created by me"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30496,9 +30508,15 @@ msgstr ""
|
|||
msgid "If this was a mistake, you can %{link_start}retain the group%{link_end} before %{deletion_date}."
|
||||
msgstr ""
|
||||
|
||||
msgid "If this was a mistake, you can %{link_start}retain the project%{link_end} before %{deletion_date}."
|
||||
msgstr ""
|
||||
|
||||
msgid "If this was a mistake, you can retain the group before %{deletion_date}: %{retention_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If this was a mistake, you can retain the project before %{deletion_date}: %{retention_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39829,9 +39847,6 @@ msgstr ""
|
|||
msgid "NotificationEvent|Merge request is reopened"
|
||||
msgstr ""
|
||||
|
||||
msgid "NotificationEvent|Merge request is set to auto-merge"
|
||||
msgstr ""
|
||||
|
||||
msgid "NotificationEvent|Merge request receives a push"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -65322,6 +65337,9 @@ msgstr ""
|
|||
msgid "View your group: %{group_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "View your project: %{project_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Viewed"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -69396,6 +69414,9 @@ msgstr ""
|
|||
msgid "Your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your project %{project_name} has been marked for deletion and will be removed in %{days}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your projects"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language-rust": "0.5.2",
|
||||
"@gitlab/svgs": "3.126.0",
|
||||
"@gitlab/ui": "111.10.2",
|
||||
"@gitlab/ui": "112.0.0",
|
||||
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
|
||||
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20250401183248",
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
"deckar01-task_list": "^2.3.1",
|
||||
"dexie": "^3.2.3",
|
||||
"diff": "^3.4.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.2.5",
|
||||
"dropzone": "^4.2.0",
|
||||
"editorconfig": "^0.15.3",
|
||||
"emoji-regex": "^10.3.0",
|
||||
|
|
@ -305,7 +305,7 @@
|
|||
"swagger-cli": "^4.0.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"timezone-mock": "^1.0.8",
|
||||
"vite": "^6.2.3",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-ruby": "^5.1.1",
|
||||
"vue-loader-vue3": "npm:vue-loader@17.4.2",
|
||||
"vue-test-utils-compat": "0.0.14",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ module QA
|
|||
|
||||
expect(page).to have_content(jira_issue_description)
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
work_item_enabled = Page::Project::Issue::Show.perform(&:work_item_enabled?)
|
||||
page_type = work_item_enabled ? Page::Project::WorkItem::Show : Page::Project::Issue::Show
|
||||
|
||||
page_type.perform do |issue|
|
||||
expect(issue).to have_label(jira_issue_label_1)
|
||||
expect(issue).to have_label(jira_issue_label_2)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ function resolveCompilationTargets(filter) {
|
|||
ignore: [
|
||||
SCSS_PARTIAL_GLOB,
|
||||
'**/bootstrap_migration*', // TODO: Prefix file name with _ (and/or move to framework)
|
||||
'**/utilities.scss', // TODO: Prefix file name with _
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ spec/frontend/members/components/table/members_pagination_spec.js
|
|||
spec/frontend/members/components/table/members_table_spec.js
|
||||
spec/frontend/milestones/components/milestone_combobox_spec.js
|
||||
spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
|
||||
spec/frontend/ml/model_registry/components/candidate_detail_spec.js
|
||||
spec/frontend/ml/model_registry/components/model_edit_spec.js
|
||||
spec/frontend/ml/model_registry/components/model_version_create_spec.js
|
||||
spec/frontend/notes/components/discussion_notes_spec.js
|
||||
|
|
|
|||
|
|
@ -133,15 +133,6 @@ FactoryBot.define do
|
|||
target_branch { "master" }
|
||||
end
|
||||
|
||||
trait :merge_when_pipeline_succeeds do
|
||||
auto_merge_enabled { true }
|
||||
auto_merge_strategy { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
|
||||
merge_user { author }
|
||||
merge_params do
|
||||
{ 'auto_merge_strategy' => AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS, sha: diff_head_sha }
|
||||
end
|
||||
end
|
||||
|
||||
trait :merge_when_checks_pass do
|
||||
auto_merge_enabled { true }
|
||||
auto_merge_strategy { AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS }
|
||||
|
|
|
|||
|
|
@ -111,18 +111,27 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Cancel all builds' do
|
||||
it 'cancels commit', :js, :sidekiq_might_not_need_inline do
|
||||
visit pipeline_path(pipeline)
|
||||
click_on 'Cancel pipeline'
|
||||
expect(page).to have_content 'Canceled'
|
||||
describe 'Cancel jobs' do
|
||||
let!(:pipeline) do
|
||||
create(
|
||||
:ci_pipeline,
|
||||
project: project,
|
||||
user: creator,
|
||||
ref: project.default_branch,
|
||||
sha: project.commit.sha,
|
||||
status: :running,
|
||||
created_at: 5.months.ago
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Cancel build' do
|
||||
it 'cancels build', :js, :sidekiq_might_not_need_inline do
|
||||
before do
|
||||
visit pipeline_path(pipeline)
|
||||
find_by_testid('cancel-pipeline').click
|
||||
wait_for_requests
|
||||
click_on 'Cancel pipeline'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'cancels pipeline and jobs', :js, :sidekiq_might_not_need_inline do
|
||||
expect(page).to have_content 'Canceled'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -325,6 +325,12 @@ describe('outputChronicDuration', () => {
|
|||
).toBe('6 months 1 day 1 hour');
|
||||
});
|
||||
|
||||
it('returns hours only if hoursOnly option specified, rounded to nearest hour', () => {
|
||||
expect(outputChronicDuration(60 * 60 * 24 * 3 + 60 * 60 * 3, { hoursOnly: true })).toBe('75h');
|
||||
expect(outputChronicDuration(4 * 3600 + 60 + 1, { hoursOnly: true })).toBe('4h');
|
||||
expect(outputChronicDuration(5 * 3600, { hoursOnly: true })).toBe('5h');
|
||||
});
|
||||
|
||||
describe('when the format is not specified', () => {
|
||||
it('uses the default format', () => {
|
||||
expect(outputChronicDuration(2 * 3600 + 20 * 60)).toBe('2 hrs 20 mins');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon, GlPopover } from '@gitlab/ui';
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -36,7 +36,7 @@ describe('Linked pipeline', () => {
|
|||
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
|
||||
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
|
||||
const findCardPopover = () => wrapper.findComponent(GlPopover);
|
||||
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title-content');
|
||||
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
|
||||
const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' });
|
||||
|
|
@ -104,15 +104,6 @@ describe('Linked pipeline', () => {
|
|||
expect(wrapper.text()).toContain(`#${props.pipeline.id}`);
|
||||
});
|
||||
|
||||
it('adds the card tooltip text to the DOM', () => {
|
||||
expect(findCardTooltip().exists()).toBe(true);
|
||||
|
||||
expect(findCardTooltip().text()).toContain(mockPipeline.project.name);
|
||||
expect(findCardTooltip().text()).toContain(mockPipeline.status.label);
|
||||
expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name);
|
||||
expect(findCardTooltip().text()).toContain(mockPipeline.id.toString());
|
||||
});
|
||||
|
||||
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
|
||||
expect(findPipelineLabel().text()).toBe('Multi-project');
|
||||
});
|
||||
|
|
@ -165,6 +156,35 @@ describe('Linked pipeline', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('card title/popover', () => {
|
||||
it.each`
|
||||
name | pipelineProps | expectedTitle | shouldContain | shouldNotContain
|
||||
${'with pipeline name'} | ${{ name: 'Pipeline name' }} | ${'Pipeline name'} | ${['Pipeline name', mockPipeline.status.label, mockPipeline.sourceJob.name]} | ${[]}
|
||||
${'multi-project'} | ${{ multiproject: true }} | ${`${mockPipeline.sourceJob.name}: ${mockPipeline.project.name}`} | ${[mockPipeline.project.name, mockPipeline.sourceJob.name, mockPipeline.status.label]} | ${[]}
|
||||
${'in-project child'} | ${{ multiproject: false }} | ${mockPipeline.sourceJob.name} | ${[mockPipeline.sourceJob.name, mockPipeline.status.label]} | ${[mockPipeline.project.name]}
|
||||
`(
|
||||
'$name: renders correct card title and popover content',
|
||||
({ pipelineProps, expectedTitle, shouldContain, shouldNotContain }) => {
|
||||
const pipelineConfig = {
|
||||
...downstreamProps,
|
||||
pipeline: { ...mockPipeline, ...pipelineProps },
|
||||
};
|
||||
createComponent({ propsData: pipelineConfig });
|
||||
|
||||
expect(findDownstreamPipelineTitle().text()).toBe(expectedTitle);
|
||||
|
||||
const popoverText = findCardPopover().text();
|
||||
shouldContain.forEach((content) => {
|
||||
expect(popoverText).toContain(content);
|
||||
});
|
||||
|
||||
shouldNotContain.forEach((content) => {
|
||||
expect(popoverText).not.toContain(content);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('action button', () => {
|
||||
describe('with permissions', () => {
|
||||
describe('on an upstream', () => {
|
||||
|
|
@ -206,11 +226,11 @@ describe('Linked pipeline', () => {
|
|||
${findRetryButton} | ${'retry button'}
|
||||
${findExpandButton} | ${'expand button'}
|
||||
`('hides the card tooltip when $name is hovered', async ({ findElement }) => {
|
||||
expect(findCardTooltip().exists()).toBe(true);
|
||||
expect(findCardPopover().exists()).toBe(true);
|
||||
|
||||
await findElement().trigger('mouseover');
|
||||
|
||||
expect(findCardTooltip().exists()).toBe(false);
|
||||
expect(findCardPopover().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('and the retry button is clicked', () => {
|
||||
|
|
@ -273,11 +293,11 @@ describe('Linked pipeline', () => {
|
|||
${findCancelButton} | ${'cancel button'}
|
||||
${findExpandButton} | ${'expand button'}
|
||||
`('hides the card tooltip when $name is hovered', async ({ findElement }) => {
|
||||
expect(findCardTooltip().exists()).toBe(true);
|
||||
expect(findCardPopover().exists()).toBe(true);
|
||||
|
||||
await findElement().trigger('mouseover');
|
||||
|
||||
expect(findCardTooltip().exists()).toBe(false);
|
||||
expect(findCardPopover().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('and the cancel button is clicked', () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export default {
|
|||
},
|
||||
sourceJob: {
|
||||
__typename: 'CiJob',
|
||||
id: '1',
|
||||
name: 'test_c',
|
||||
},
|
||||
project: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { createMockSubscription } from 'mock-apollo-client';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
|
@ -13,6 +14,8 @@ import retryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry
|
|||
import HeaderActions from '~/ci/pipeline_details/header/components/header_actions.vue';
|
||||
import HeaderBadges from '~/ci/pipeline_details/header/components/header_badges.vue';
|
||||
import getPipelineDetailsQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
import pipelineCiStatusUpdatedSubscription from '~/graphql_shared/subscriptions/pipeline_ci_status_updated.subscription.graphql';
|
||||
|
||||
import {
|
||||
pipelineHeaderSuccess,
|
||||
pipelineHeaderRunning,
|
||||
|
|
@ -24,12 +27,15 @@ import {
|
|||
pipelineRetryMutationResponseFailed,
|
||||
pipelineCancelMutationResponseFailed,
|
||||
pipelineDeleteMutationResponseFailed,
|
||||
mockPipelineStatusUpdatedResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Pipeline header', () => {
|
||||
let wrapper;
|
||||
let mockedSubscription;
|
||||
let apolloProvider;
|
||||
|
||||
const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
|
||||
const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning);
|
||||
|
|
@ -84,10 +90,14 @@ describe('Pipeline header', () => {
|
|||
identityVerificationRequired: false,
|
||||
identityVerificationPath: '#',
|
||||
pipelineIid: 1,
|
||||
pipelineId: 100,
|
||||
paths: {
|
||||
pipelinesPath: '/namespace/my-project/-/pipelines',
|
||||
fullProject: '/namespace/my-project',
|
||||
},
|
||||
glFeatures: {
|
||||
ciPipelineStatusRealtime: true,
|
||||
},
|
||||
};
|
||||
|
||||
const createMockApolloProvider = (handlers) => {
|
||||
|
|
@ -95,10 +105,18 @@ describe('Pipeline header', () => {
|
|||
};
|
||||
|
||||
const createComponent = (handlers = defaultHandlers) => {
|
||||
mockedSubscription = createMockSubscription();
|
||||
apolloProvider = createMockApolloProvider(handlers);
|
||||
|
||||
apolloProvider.defaultClient.setRequestHandler(
|
||||
pipelineCiStatusUpdatedSubscription,
|
||||
() => mockedSubscription,
|
||||
);
|
||||
|
||||
wrapper = shallowMountExtended(PipelineHeader, {
|
||||
provide: defaultProvideOptions,
|
||||
stubs: { GlSprintf },
|
||||
apolloProvider: createMockApolloProvider(handlers),
|
||||
apolloProvider,
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
|
|
@ -403,5 +421,36 @@ describe('Pipeline header', () => {
|
|||
expect(findHeaderActions().props('isDeleting')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscription', () => {
|
||||
it('updates pipeline status when subscription updates', async () => {
|
||||
await createComponent([
|
||||
[getPipelineDetailsQuery, runningHandler],
|
||||
[cancelPipelineMutation, cancelMutationHandlerFailed],
|
||||
]);
|
||||
|
||||
const {
|
||||
data: {
|
||||
project: {
|
||||
pipeline: { detailedStatus },
|
||||
},
|
||||
},
|
||||
} = pipelineHeaderRunning;
|
||||
|
||||
expect(findStatus().props('status')).toStrictEqual(detailedStatus);
|
||||
|
||||
mockedSubscription.next(mockPipelineStatusUpdatedResponse);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findStatus().props('status')).toStrictEqual({
|
||||
__typename: 'DetailedStatus',
|
||||
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
|
||||
icon: 'status_success',
|
||||
id: 'success-1255-1255',
|
||||
text: 'Passed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1205,3 +1205,22 @@ export const failedRetryMutationResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPipelineStatusUpdatedResponse = {
|
||||
data: {
|
||||
ciPipelineStatusUpdated: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/1257',
|
||||
retryable: false,
|
||||
cancelable: false,
|
||||
__typename: 'Pipeline',
|
||||
detailedStatus: {
|
||||
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
|
||||
icon: 'status_success',
|
||||
id: 'success-1255-1255',
|
||||
label: 'passed',
|
||||
text: 'Passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ describe('CE IssuesListApp component', () => {
|
|||
signInPath: 'sign/in/path',
|
||||
groupId: '',
|
||||
commentTemplatePaths: [],
|
||||
timeTrackingLimitToHours: false,
|
||||
};
|
||||
|
||||
let defaultQueryResponse = getIssuesQueryResponse;
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
<div
|
||||
class="gl-flex gl-h-7 gl-items-center gl-ml-5"
|
||||
>
|
||||
<pipeline-ci-status-stub
|
||||
<ci-icon-stub
|
||||
class="gl-mr-2"
|
||||
pipelineid="gid://gitlab/Ci::Pipeline/167"
|
||||
projectfullpath="gitlab-org/gitlab-foss"
|
||||
showtooltip="true"
|
||||
status="[object Object]"
|
||||
uselink="true"
|
||||
/>
|
||||
</div>
|
||||
<gl-button-group-stub
|
||||
|
|
|
|||
|
|
@ -8,115 +8,54 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import LastCommit from '~/repository/components/last_commit.vue';
|
||||
import CommitInfo from '~/repository/components/commit_info.vue';
|
||||
import SignatureBadge from '~/commit/components/signature_badge.vue';
|
||||
import PipelineCiStatus from '~/vue_shared/components/ci_status/pipeline_ci_status.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import eventHub from '~/repository/event_hub';
|
||||
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
|
||||
import projectPathQuery from '~/repository/queries/project_path.query.graphql';
|
||||
import pipelineCiStatusUpdatedSubscription from '~/graphql_shared/subscriptions/pipeline_ci_status_updated.subscription.graphql';
|
||||
import { FORK_UPDATED_EVENT } from '~/repository/constants';
|
||||
import { mockPipelineStatusUpdatedResponse, createCommitData } from '../mock_data';
|
||||
|
||||
let wrapper;
|
||||
let commitData;
|
||||
let mockResolver;
|
||||
|
||||
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
|
||||
const findHistoryButton = () => wrapper.findByTestId('last-commit-history');
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findStatusBox = () => wrapper.findComponent(SignatureBadge);
|
||||
const findCommitInfo = () => wrapper.findComponent(CommitInfo);
|
||||
const findPipeline = () => wrapper.findComponent(PipelineCiStatus);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const { bindInternalEventDocument } = useMockInternalEventsTracking();
|
||||
|
||||
const defaultPipelineEdges = [
|
||||
{
|
||||
__typename: 'PipelineEdge',
|
||||
node: {
|
||||
__typename: 'Pipeline',
|
||||
id: 'gid://gitlab/Ci::Pipeline/167',
|
||||
},
|
||||
},
|
||||
];
|
||||
describe('Repository last commit component', () => {
|
||||
let wrapper;
|
||||
let commitData;
|
||||
let mockResolver;
|
||||
let apolloProvider;
|
||||
|
||||
const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signature = null }) => {
|
||||
return {
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: 'gid://gitlab/Project/6',
|
||||
repository: {
|
||||
__typename: 'Repository',
|
||||
paginatedTree: {
|
||||
__typename: 'TreeConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'Tree',
|
||||
lastCommit: {
|
||||
__typename: 'Commit',
|
||||
id: 'gid://gitlab/CommitPresenter/123456789',
|
||||
sha: '123456789',
|
||||
title: 'Commit title',
|
||||
titleHtml: 'Commit title',
|
||||
descriptionHtml: '',
|
||||
message: '',
|
||||
webPath: '/commit/123',
|
||||
authoredDate: '2019-01-01',
|
||||
authorName: 'Test',
|
||||
authorGravatar: 'https://test.com',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Test',
|
||||
avatarUrl: 'https://test.com',
|
||||
webPath: '/test',
|
||||
},
|
||||
signature,
|
||||
pipelines: {
|
||||
__typename: 'PipelineConnection',
|
||||
edges: pipelineEdges,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
|
||||
const findHistoryButton = () => wrapper.findByTestId('last-commit-history');
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findStatusBox = () => wrapper.findComponent(SignatureBadge);
|
||||
const findCommitInfo = () => wrapper.findComponent(CommitInfo);
|
||||
const findPipelineStatus = () => wrapper.findComponent(CiIcon);
|
||||
|
||||
const subscriptionHandler = jest.fn().mockResolvedValue(mockPipelineStatusUpdatedResponse);
|
||||
|
||||
const createComponent = (data = {}, pipelineSubscriptionHandler = subscriptionHandler) => {
|
||||
const currentPath = 'path';
|
||||
|
||||
commitData = createCommitData(data);
|
||||
mockResolver = jest.fn().mockResolvedValue(commitData);
|
||||
|
||||
apolloProvider = createMockApollo([
|
||||
[pathLastCommitQuery, mockResolver],
|
||||
[pipelineCiStatusUpdatedSubscription, pipelineSubscriptionHandler],
|
||||
]);
|
||||
|
||||
wrapper = shallowMountExtended(LastCommit, {
|
||||
apolloProvider,
|
||||
propsData: { currentPath, historyUrl: '/history' },
|
||||
provide: {
|
||||
glFeatures: {
|
||||
ciPipelineStatusRealtime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const createComponent = (data = {}) => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const currentPath = 'path';
|
||||
|
||||
commitData = createCommitData(data);
|
||||
mockResolver = jest.fn().mockResolvedValue(commitData);
|
||||
|
||||
const apolloProvider = createMockApollo([[pathLastCommitQuery, mockResolver]]);
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: projectPathQuery,
|
||||
data: {
|
||||
projectPath: 'gitlab-org/gitlab-foss',
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(LastCommit, {
|
||||
apolloProvider,
|
||||
propsData: { currentPath, historyUrl: '/history' },
|
||||
provide: {
|
||||
glFeatures: {
|
||||
ciPipelineStatusRealtime: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mockResolver = null;
|
||||
});
|
||||
|
||||
describe('Repository last commit component', () => {
|
||||
it.each`
|
||||
loading | label
|
||||
${true} | ${'shows'}
|
||||
|
|
@ -182,7 +121,7 @@ describe('Repository last commit component', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findPipeline().exists()).toBe(false);
|
||||
expect(findPipelineStatus().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders pipeline components when pipeline exists', async () => {
|
||||
|
|
@ -190,7 +129,7 @@ describe('Repository last commit component', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findPipeline().exists()).toBe(true);
|
||||
expect(findPipelineStatus().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('created', () => {
|
||||
|
|
@ -232,4 +171,16 @@ describe('Repository last commit component', () => {
|
|||
|
||||
expect(findStatusBox().props()).toMatchObject({ signature: signatureResponse });
|
||||
});
|
||||
|
||||
describe('subscription', () => {
|
||||
it('calls subscription with correct variables', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(subscriptionHandler).toHaveBeenCalledWith({
|
||||
pipelineId: 'gid://gitlab/Ci::Pipeline/167',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -282,3 +282,86 @@ export const currentUserDataMock = {
|
|||
};
|
||||
|
||||
export const applicationInfoMock = { gitpodEnabled: true };
|
||||
|
||||
export const mockPipelineStatusUpdatedResponse = {
|
||||
data: {
|
||||
ciPipelineStatusUpdated: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/1257',
|
||||
retryable: false,
|
||||
cancelable: false,
|
||||
__typename: 'Pipeline',
|
||||
detailedStatus: {
|
||||
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
|
||||
icon: 'status_success',
|
||||
id: 'success-1255-1255',
|
||||
label: 'passed',
|
||||
text: 'Passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPipelineEdges = [
|
||||
{
|
||||
__typename: 'PipelineEdge',
|
||||
node: {
|
||||
__typename: 'Pipeline',
|
||||
id: 'gid://gitlab/Ci::Pipeline/167',
|
||||
detailedStatus: {
|
||||
__typename: 'DetailedStatus',
|
||||
id: 'id',
|
||||
detailsPath: 'https://test.com/pipeline',
|
||||
icon: 'status_running',
|
||||
text: 'failed',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signature = null }) => {
|
||||
return {
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: 'gid://gitlab/Project/6',
|
||||
repository: {
|
||||
__typename: 'Repository',
|
||||
paginatedTree: {
|
||||
__typename: 'TreeConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'Tree',
|
||||
lastCommit: {
|
||||
__typename: 'Commit',
|
||||
id: 'gid://gitlab/CommitPresenter/123456789',
|
||||
sha: '123456789',
|
||||
title: 'Commit title',
|
||||
titleHtml: 'Commit title',
|
||||
descriptionHtml: '',
|
||||
message: '',
|
||||
webPath: '/commit/123',
|
||||
authoredDate: '2019-01-01',
|
||||
authorName: 'Test',
|
||||
authorGravatar: 'https://test.com',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Test',
|
||||
avatarUrl: 'https://test.com',
|
||||
webPath: '/test',
|
||||
},
|
||||
signature,
|
||||
pipelines: {
|
||||
__typename: 'PipelineConnection',
|
||||
edges: pipelineEdges,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import axios from 'axios';
|
|||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import Reviewers from '~/sidebar/components/reviewers/reviewers.vue';
|
||||
import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
|
||||
import SidebarService from '~/sidebar/services/sidebar_service';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
|
|
@ -13,6 +15,7 @@ import Mock from '../../mock_data';
|
|||
|
||||
jest.mock('~/super_sidebar/user_counts_fetch');
|
||||
|
||||
const { bindInternalEventDocument } = useMockInternalEventsTracking();
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('sidebar reviewers', () => {
|
||||
|
|
@ -20,10 +23,12 @@ describe('sidebar reviewers', () => {
|
|||
let wrapper;
|
||||
let mediator;
|
||||
let axiosMock;
|
||||
let trackEventSpy;
|
||||
|
||||
const findAssignButton = () => wrapper.findByTestId('sidebar-reviewers-assign-button');
|
||||
const findReviewers = () => wrapper.findComponent(Reviewers);
|
||||
|
||||
const createComponent = (props) => {
|
||||
const createComponent = ({ props, stubs, data } = {}) => {
|
||||
wrapper = shallowMountExtended(SidebarReviewers, {
|
||||
apolloProvider: apolloMock,
|
||||
propsData: {
|
||||
|
|
@ -35,6 +40,11 @@ describe('sidebar reviewers', () => {
|
|||
changing: false,
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
projectPath: 'projectPath',
|
||||
issuableId: 1,
|
||||
|
|
@ -43,10 +53,13 @@ describe('sidebar reviewers', () => {
|
|||
},
|
||||
stubs: {
|
||||
ApprovalSummary: true,
|
||||
...stubs,
|
||||
},
|
||||
// Attaching to document is required because this component emits something from the parent element :/
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
({ trackEventSpy } = bindInternalEventDocument(wrapper.element));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -69,29 +82,9 @@ describe('sidebar reviewers', () => {
|
|||
${'shows'} | ${true} | ${true}
|
||||
${'does not show'} | ${false} | ${false}
|
||||
`('$copy Assign button when canUpdate is $canUpdate', ({ canUpdate, expected }) => {
|
||||
wrapper = shallowMountExtended(SidebarReviewers, {
|
||||
apolloProvider: apolloMock,
|
||||
propsData: {
|
||||
issuableIid: '1',
|
||||
issuableId: 1,
|
||||
mediator,
|
||||
field: '',
|
||||
projectPath: 'projectPath',
|
||||
changing: false,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
issuable: { userPermissions: { adminMergeRequest: canUpdate } },
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
projectPath: 'projectPath',
|
||||
issuableId: 1,
|
||||
issuableIid: 1,
|
||||
multipleApprovalRulesAvailable: false,
|
||||
},
|
||||
stubs: {
|
||||
ApprovalSummary: true,
|
||||
createComponent({
|
||||
data: {
|
||||
issuable: { userPermissions: { adminMergeRequest: canUpdate } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -119,15 +112,36 @@ describe('sidebar reviewers', () => {
|
|||
expect(fetchUserCounts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls the mediator when "reviewBySelf" method is called', () => {
|
||||
createComponent();
|
||||
describe('assign yourself', () => {
|
||||
it('tracks how many times the Reviewers component indicates the user is assigning themself', async () => {
|
||||
createComponent({
|
||||
data: {
|
||||
issuable: { userPermissions: { adminMergeRequest: true } },
|
||||
},
|
||||
stubs: {
|
||||
Reviewers,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mediator.addSelfReview).not.toHaveBeenCalled();
|
||||
expect(mediator.store.reviewers.length).toBe(0);
|
||||
// Wait for Apollo to finish so the sidebar is enabled
|
||||
await nextTick();
|
||||
|
||||
wrapper.vm.reviewBySelf();
|
||||
const reviewers = findReviewers();
|
||||
reviewers.vm.assignSelf();
|
||||
|
||||
expect(mediator.addSelfReview).toHaveBeenCalled();
|
||||
expect(mediator.store.reviewers.length).toBe(1);
|
||||
expect(trackEventSpy).toHaveBeenCalledWith('assign_self_as_reviewer_in_mr', {}, undefined);
|
||||
});
|
||||
|
||||
it('calls the mediator when "reviewBySelf" method is called', () => {
|
||||
createComponent();
|
||||
|
||||
expect(mediator.addSelfReview).not.toHaveBeenCalled();
|
||||
expect(mediator.store.reviewers.length).toBe(0);
|
||||
|
||||
wrapper.vm.reviewBySelf();
|
||||
|
||||
expect(mediator.addSelfReview).toHaveBeenCalled();
|
||||
expect(mediator.store.reviewers.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,6 +126,22 @@ describe('NamespaceStorageApp', () => {
|
|||
});
|
||||
|
||||
describe('Namespace project list', () => {
|
||||
describe('when customSortKey is set', () => {
|
||||
it('disables sorting', () => {
|
||||
createComponent({ provide: { customSortKey: 'custom' } });
|
||||
|
||||
expect(findProjectList().props('enableSortableFields')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when customSortKey is not set', () => {
|
||||
it('enables sorting', () => {
|
||||
createComponent({ provide: { customSortKey: undefined } });
|
||||
|
||||
expect(findProjectList().props('enableSortableFields')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the 2 projects', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
|
@ -187,6 +203,47 @@ describe('NamespaceStorageApp', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('sorting projects', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
sortBy | sortDesc | sortKey
|
||||
${'storage'} | ${true} | ${'STORAGE_SIZE_DESC'}
|
||||
${'storage'} | ${false} | ${'STORAGE_SIZE_ASC'}
|
||||
${'repository'} | ${true} | ${'REPOSITORY_SIZE_DESC'}
|
||||
${'repository'} | ${false} | ${'REPOSITORY_SIZE_ASC'}
|
||||
${'snippets'} | ${true} | ${'SNIPPETS_SIZE_DESC'}
|
||||
${'snippets'} | ${false} | ${'SNIPPETS_SIZE_ASC'}
|
||||
${'buildArtifacts'} | ${true} | ${'BUILD_ARTIFACTS_SIZE_DESC'}
|
||||
${'buildArtifacts'} | ${false} | ${'BUILD_ARTIFACTS_SIZE_ASC'}
|
||||
${'lfsObjects'} | ${true} | ${'LFS_OBJECTS_SIZE_DESC'}
|
||||
${'lfsObjects'} | ${false} | ${'LFS_OBJECTS_SIZE_ASC'}
|
||||
${'packages'} | ${true} | ${'PACKAGES_SIZE_DESC'}
|
||||
${'packages'} | ${false} | ${'PACKAGES_SIZE_ASC'}
|
||||
${'wiki'} | ${true} | ${'WIKI_SIZE_DESC'}
|
||||
${'wiki'} | ${false} | ${'WIKI_SIZE_ASC'}
|
||||
${'snippets'} | ${true} | ${'SNIPPETS_SIZE_DESC'}
|
||||
${'snippets'} | ${false} | ${'SNIPPETS_SIZE_ASC'}
|
||||
${'containerRegistry'} | ${true} | ${'CONTAINER_REGISTRY_SIZE_DESC'}
|
||||
${'containerRegistry'} | ${false} | ${'CONTAINER_REGISTRY_SIZE_ASC'}
|
||||
`(
|
||||
'when sorting changed to $sortBy and sortDesc is $sortDesc',
|
||||
({ sortBy, sortDesc, sortKey }) => {
|
||||
it('forms correct sorting order string', async () => {
|
||||
findProjectList().vm.$emit('sortChanged', { sortBy, sortDesc });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(getProjectListStorageHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sortKey }),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('projects table pagination component', () => {
|
||||
const projectsStorageWithPageInfo = cloneDeep(mockGetProjectListStorageGraphQLResponse);
|
||||
projectsStorageWithPageInfo.data.namespace.projects.pageInfo.hasNextPage = true;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const createComponent = ({ provide = {}, props = {} } = {}) => {
|
|||
helpLinks: storageTypeHelpPaths,
|
||||
isLoading: false,
|
||||
sortBy: 'storage',
|
||||
sortableFields: { storage: false },
|
||||
enableSortableFields: false,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
|
|
@ -61,18 +61,34 @@ describe('ProjectList', () => {
|
|||
it('will allow sorting for fields that have sorting enabled', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
sortableFields: { storage: true },
|
||||
enableSortableFields: true,
|
||||
},
|
||||
});
|
||||
expect(findTable().props('fields')).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ key: 'storage', sortable: true })]),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ key: 'storage', sortable: true }),
|
||||
expect.objectContaining({ key: 'repository', sortable: true }),
|
||||
expect.objectContaining({ key: 'buildArtifacts', sortable: true }),
|
||||
expect.objectContaining({ key: 'lfsObjects', sortable: true }),
|
||||
expect.objectContaining({ key: 'packages', sortable: true }),
|
||||
expect.objectContaining({ key: 'wiki', sortable: true }),
|
||||
expect.objectContaining({ key: 'containerRegistry', sortable: true }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('will disable sorting by storage field', () => {
|
||||
createComponent();
|
||||
expect(findTable().props('fields')).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ key: 'storage', sortable: false })]),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ key: 'storage', sortable: false }),
|
||||
expect.objectContaining({ key: 'repository', sortable: false }),
|
||||
expect.objectContaining({ key: 'buildArtifacts', sortable: false }),
|
||||
expect.objectContaining({ key: 'lfsObjects', sortable: false }),
|
||||
expect.objectContaining({ key: 'packages', sortable: false }),
|
||||
expect.objectContaining({ key: 'wiki', sortable: false }),
|
||||
expect.objectContaining({ key: 'containerRegistry', sortable: false }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
|
||||
import { MWPS_MERGE_STRATEGY, MWCP_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
|
||||
import { MWCP_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
|
||||
import eventHub from '~/vue_merge_request_widget/event_hub';
|
||||
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ const defaultMrProps = () => ({
|
|||
sha,
|
||||
targetBranchPath,
|
||||
targetBranch,
|
||||
autoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
autoMergeStrategy: MWCP_MERGE_STRATEGY,
|
||||
});
|
||||
|
||||
const getStatusText = () => wrapper.findByTestId('statusText').text();
|
||||
|
|
@ -99,10 +98,10 @@ describe('MRWidgetAutoMergeEnabled', () => {
|
|||
|
||||
describe('computed', () => {
|
||||
describe('cancelButtonText', () => {
|
||||
it('should return "Cancel" if MWPS is selected', () => {
|
||||
it('should return "Cancel" if MWCP is selected', () => {
|
||||
factory({
|
||||
...defaultMrProps(),
|
||||
autoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
autoMergeStrategy: MWCP_MERGE_STRATEGY,
|
||||
});
|
||||
|
||||
expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe('Cancel auto-merge');
|
||||
|
|
@ -148,15 +147,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
|
|||
expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
|
||||
factory({
|
||||
...defaultMrProps(),
|
||||
autoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
});
|
||||
|
||||
expect(getStatusText()).toContain('to be merged automatically when the pipeline succeeds');
|
||||
});
|
||||
|
||||
it('should render the status text as "to be merged automatically..." if MWCP is selected', () => {
|
||||
factory({
|
||||
...defaultMrProps(),
|
||||
|
|
@ -165,16 +155,5 @@ describe('MRWidgetAutoMergeEnabled', () => {
|
|||
|
||||
expect(getStatusText()).toContain('to be merged automatically when all merge checks pass');
|
||||
});
|
||||
|
||||
it('should render the cancel button as "Cancel" if MWPS is selected', () => {
|
||||
factory({
|
||||
...defaultMrProps(),
|
||||
autoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
});
|
||||
|
||||
const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
|
||||
|
||||
expect(cancelButtonText).toBe('Cancel auto-merge');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/
|
|||
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
|
||||
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
|
||||
import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue';
|
||||
import { MWPS_MERGE_STRATEGY, MWCP_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
|
||||
import { MWCP_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
|
||||
import eventHub from '~/vue_merge_request_widget/event_hub';
|
||||
import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
|
@ -55,8 +55,8 @@ const createTestMr = (customConfig) => {
|
|||
shouldRemoveSourceBranch: true,
|
||||
canRemoveSourceBranch: false,
|
||||
targetBranch: 'main',
|
||||
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
|
||||
preferredAutoMergeStrategy: MWCP_MERGE_STRATEGY,
|
||||
availableAutoMergeStrategies: [MWCP_MERGE_STRATEGY],
|
||||
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
|
||||
transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
|
||||
translateStateToMachine: () => this.transitionStateMachine(),
|
||||
|
|
@ -216,31 +216,24 @@ describe('ReadyToMerge', () => {
|
|||
expect(findMergeButton().text()).toBe('Merge');
|
||||
});
|
||||
|
||||
it('should return Set to auto-merge in the button and Merge when pipeline succeeds in the helper text', () => {
|
||||
createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
|
||||
|
||||
expect(findMergeButton().text()).toBe('Set to auto-merge');
|
||||
expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
|
||||
});
|
||||
|
||||
it('should return Set to auto-merge in the button and Merge when checks pass in the helper text', () => {
|
||||
createComponent({ mr: { preferredAutoMergeStrategy: MWCP_MERGE_STRATEGY } });
|
||||
|
||||
expect(findMergeButton().text()).toBe('Set to auto-merge');
|
||||
expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
|
||||
expect(findMergeHelperText().text()).toBe('Merge when all merge checks pass');
|
||||
});
|
||||
|
||||
it('should show merge help text when pipeline has failed and has an auto merge strategy', () => {
|
||||
createComponent({
|
||||
mr: {
|
||||
pipeline: { status: 'FAILED' },
|
||||
availableAutoMergeStrategies: MWPS_MERGE_STRATEGY,
|
||||
availableAutoMergeStrategies: MWCP_MERGE_STRATEGY,
|
||||
hasCI: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findMergeButton().text()).toBe('Set to auto-merge');
|
||||
expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
|
||||
expect(findMergeHelperText().text()).toBe('Merge when all merge checks pass');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -248,7 +241,7 @@ describe('ReadyToMerge', () => {
|
|||
it('dropdown should be visible if auto merge is available', () => {
|
||||
createComponent({
|
||||
mr: {
|
||||
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
|
||||
availableAutoMergeStrategies: [MWCP_MERGE_STRATEGY],
|
||||
mergeable: true,
|
||||
headPipeline: { active: false },
|
||||
onlyAllowMergeIfPipelineSucceeds: false,
|
||||
|
|
@ -274,7 +267,7 @@ describe('ReadyToMerge', () => {
|
|||
it('dropdown should be hidden if the MR is not mergeable', () => {
|
||||
createComponent({
|
||||
mr: {
|
||||
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
|
||||
availableAutoMergeStrategies: [MWCP_MERGE_STRATEGY],
|
||||
mergeable: false,
|
||||
headPipeline: { active: true },
|
||||
onlyAllowMergeIfPipelineSucceeds: false,
|
||||
|
|
@ -389,11 +382,11 @@ describe('ReadyToMerge', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should handle merge when pipeline succeeds', async () => {
|
||||
it('should handle auto merge', async () => {
|
||||
createComponent({ mr: { shouldRemoveSourceBranch: false } }, true);
|
||||
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
|
||||
jest.spyOn(service, 'merge').mockResolvedValue(response(MWCP_MERGE_STRATEGY));
|
||||
|
||||
findMergeButton().vm.$emit('click');
|
||||
|
||||
|
|
@ -410,7 +403,7 @@ describe('ReadyToMerge', () => {
|
|||
expect.objectContaining({
|
||||
sha: '12345678',
|
||||
should_remove_source_branch: false,
|
||||
auto_merge_strategy: 'merge_when_pipeline_succeeds',
|
||||
auto_merge_strategy: 'merge_when_checks_pass',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
export const mockPipelineStatusResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/20',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/1257',
|
||||
detailedStatus: {
|
||||
label: 'running',
|
||||
id: 'running-1257-1257',
|
||||
icon: 'status_running',
|
||||
text: 'Running',
|
||||
detailsPath: '/root/ci-project/-/pipelines/1257',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPipelineStatusUpdatedResponse = {
|
||||
data: {
|
||||
ciPipelineStatusUpdated: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/1257',
|
||||
__typename: 'Pipeline',
|
||||
detailedStatus: {
|
||||
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
|
||||
icon: 'status_success',
|
||||
id: 'success-1255-1255',
|
||||
label: 'passed',
|
||||
text: 'Passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createAlert } from '~/alert';
|
||||
import { captureException } from '~/sentry/sentry_browser_wrapper';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import PipelineCiStatus from '~/vue_shared/components/ci_status/pipeline_ci_status.vue';
|
||||
import pipelineCiStatusQuery from '~/vue_shared/components/ci_status/graphql/pipeline_ci_status.query.graphql';
|
||||
import pipelineCiStatusUpdatedSubscription from '~/vue_shared/components/ci_status/graphql/pipeline_ci_status_updated.subscription.graphql';
|
||||
import { mockPipelineStatusResponse, mockPipelineStatusUpdatedResponse } from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/alert');
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
|
||||
describe('Pipeline CI Status', () => {
|
||||
let wrapper;
|
||||
let mockedSubscription;
|
||||
let apolloProvider;
|
||||
|
||||
const defaultProps = {
|
||||
pipelineId: 'gid://gitlab/Ci::Pipeline/1255',
|
||||
projectFullPath: 'gitlab-org/gitlab',
|
||||
canSubscribe: true,
|
||||
};
|
||||
|
||||
const error = new Error('GraphQL error');
|
||||
|
||||
const successResolver = jest.fn().mockResolvedValue(mockPipelineStatusResponse);
|
||||
const errorResolver = jest.fn().mockRejectedValue(error);
|
||||
|
||||
const createMockApolloProvider = (queryResolver) => {
|
||||
const requestHandlers = [[pipelineCiStatusQuery, queryResolver]];
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
};
|
||||
|
||||
const createComponent = (props = {}, queryResolver = successResolver) => {
|
||||
mockedSubscription = createMockApolloSubscription();
|
||||
apolloProvider = createMockApolloProvider(queryResolver);
|
||||
|
||||
apolloProvider.defaultClient.setRequestHandler(
|
||||
pipelineCiStatusUpdatedSubscription,
|
||||
() => mockedSubscription,
|
||||
);
|
||||
|
||||
wrapper = shallowMount(PipelineCiStatus, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
};
|
||||
|
||||
const findIcon = () => wrapper.findComponent(CiIcon);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
||||
describe('loading', () => {
|
||||
it('displays loading icon while fetching pipeline status', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('does not display loading icon', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('displays pipeline status', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls pipeline status query correctly', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(successResolver).toHaveBeenCalledWith({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
pipelineId: 'gid://gitlab/Ci::Pipeline/1255',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates status when subscription updates', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findIcon().props('status')).toStrictEqual({
|
||||
__typename: 'DetailedStatus',
|
||||
detailsPath: '/root/ci-project/-/pipelines/1257',
|
||||
icon: 'status_running',
|
||||
id: 'running-1257-1257',
|
||||
label: 'running',
|
||||
text: 'Running',
|
||||
});
|
||||
|
||||
mockedSubscription.next(mockPipelineStatusUpdatedResponse);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findIcon().props('status')).toStrictEqual({
|
||||
__typename: 'DetailedStatus',
|
||||
detailsPath: '/root/simple-ci-project/-/pipelines/1257',
|
||||
icon: 'status_success',
|
||||
id: 'success-1255-1255',
|
||||
label: 'passed',
|
||||
text: 'Passed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({}, errorResolver);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows an error', () => {
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred fetching the pipeline status.',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports an error', () => {
|
||||
expect(captureException).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,12 @@ describe('WorkItemTimeTracking component', () => {
|
|||
const findTimeTrackingBody = () => wrapper.findByTestId('time-tracking-body');
|
||||
const getProgressBarTooltip = () => getBinding(findProgressBar().element, 'gl-tooltip');
|
||||
|
||||
const createComponent = ({ canUpdate = true, timeEstimate = 0, totalTimeSpent = 0 } = {}) => {
|
||||
const createComponent = ({
|
||||
canUpdate = true,
|
||||
timeEstimate = 0,
|
||||
totalTimeSpent = 0,
|
||||
provide = {},
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemTimeTracking, {
|
||||
directives: {
|
||||
GlModal: createMockDirective('gl-modal'),
|
||||
|
|
@ -33,6 +38,8 @@ describe('WorkItemTimeTracking component', () => {
|
|||
},
|
||||
provide: {
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
timeTrackingLimitToHours: false,
|
||||
...provide,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
|
|
@ -186,6 +193,21 @@ describe('WorkItemTimeTracking component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when global time tracking hours only preference is turned on', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
timeEstimate: 0,
|
||||
totalTimeSpent: 60 * 60 * 24 * 3 + 60 * 60 * 3,
|
||||
provide: { timeTrackingLimitToHours: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows time spent in hours', () => {
|
||||
expect(findTimeTrackingBody().text()).toMatchInterpolatedText('Spent 75h Add estimate');
|
||||
expect(findProgressBar().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has no permissions', () => {
|
||||
it('does not show "Add time entry" button', () => {
|
||||
createComponent({ canUpdate: false });
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ describeSkipVue3(skipReason, () => {
|
|||
isSignedIn: true,
|
||||
workItemType: null,
|
||||
hasIssueDateFilterFeature: false,
|
||||
timeTrackingLimitToHours: false,
|
||||
...provide,
|
||||
},
|
||||
propsData: {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CiInputsValueInputType'], feature_category: :pipeline_composition do
|
||||
specify { expect(described_class.graphql_name).to eq('CiInputsValueInputType') }
|
||||
RSpec.describe GitlabSchema.types['CiInputsValue'], feature_category: :pipeline_composition do
|
||||
specify { expect(described_class.graphql_name).to eq('CiInputsValue') }
|
||||
|
||||
describe '.coerce_input' do
|
||||
subject(:coerce_input) { described_class.coerce_isolated_input(value) }
|
||||
|
|
@ -240,7 +240,8 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
|
|||
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
|
||||
rss_path: '#',
|
||||
show_new_issue_link: 'true',
|
||||
sign_in_path: new_user_session_path
|
||||
sign_in_path: new_user_session_path,
|
||||
time_tracking_limit_to_hours: "false"
|
||||
}
|
||||
|
||||
expect(helper.project_issues_list_data(project, current_user)).to include(expected)
|
||||
|
|
@ -298,7 +299,8 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
|
|||
new_project_path: new_project_path(namespace_id: group.id),
|
||||
rss_path: '#',
|
||||
sign_in_path: new_user_session_path,
|
||||
group_id: group.id
|
||||
group_id: group.id,
|
||||
time_tracking_limit_to_hours: "false"
|
||||
}
|
||||
|
||||
expect(helper.group_issues_list_data(group, current_user)).to include(expected)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue