Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c123291db9
commit
932d504aaa
32 changed files with 468 additions and 125 deletions
|
@ -1,9 +1,4 @@
|
||||||
---
|
---
|
||||||
GraphQL/FieldDefinitions:
|
GraphQL/FieldDefinitions:
|
||||||
Exclude:
|
Exclude:
|
||||||
- ee/app/graphql/types/ci/code_quality_degradation_type.rb
|
|
||||||
- ee/app/graphql/types/epic_type.rb
|
|
||||||
- ee/app/graphql/types/group_release_stats_type.rb
|
|
||||||
- ee/app/graphql/types/iteration_type.rb
|
|
||||||
- ee/app/graphql/types/requirements_management/requirement_type.rb
|
|
||||||
- ee/app/graphql/types/vulnerability_type.rb
|
- ee/app/graphql/types/vulnerability_type.rb
|
||||||
|
|
|
@ -4,10 +4,6 @@ GraphQL/OrderedArguments:
|
||||||
- app/graphql/resolvers/base_issues_resolver.rb
|
- app/graphql/resolvers/base_issues_resolver.rb
|
||||||
- app/graphql/resolvers/design_management/designs_resolver.rb
|
- app/graphql/resolvers/design_management/designs_resolver.rb
|
||||||
- app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
|
- app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
|
||||||
- app/graphql/types/commit_action_type.rb
|
|
||||||
- app/graphql/types/diff_paths_input_type.rb
|
|
||||||
- app/graphql/types/issues/negated_issue_filter_input_type.rb
|
|
||||||
- app/graphql/types/jira_users_mapping_input_type.rb
|
|
||||||
- app/graphql/types/notes/diff_image_position_input_type.rb
|
- app/graphql/types/notes/diff_image_position_input_type.rb
|
||||||
- app/graphql/types/notes/diff_position_base_input_type.rb
|
- app/graphql/types/notes/diff_position_base_input_type.rb
|
||||||
- app/graphql/types/notes/diff_position_input_type.rb
|
- app/graphql/types/notes/diff_position_input_type.rb
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<h2
|
<h1
|
||||||
v-safe-html="titleHtml"
|
v-safe-html="titleHtml"
|
||||||
:class="{
|
:class="{
|
||||||
'issue-realtime-pre-pulse': preAnimation,
|
'issue-realtime-pre-pulse': preAnimation,
|
||||||
|
@ -76,7 +76,7 @@ export default {
|
||||||
}"
|
}"
|
||||||
class="title qa-title"
|
class="title qa-title"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
></h2>
|
></h1>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="showInlineEditButton && canUpdate"
|
v-if="showInlineEditButton && canUpdate"
|
||||||
v-gl-tooltip.bottom
|
v-gl-tooltip.bottom
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { __ } from '~/locale';
|
||||||
import {
|
import {
|
||||||
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
||||||
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
||||||
|
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
|
||||||
|
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||||
} from '~/security_configuration/constants';
|
} from '~/security_configuration/constants';
|
||||||
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
|
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
|
||||||
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
|
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
|
||||||
|
@ -137,6 +139,12 @@ export default {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
trackProviderLearnMoreClick(providerId) {
|
||||||
|
this.track(TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, {
|
||||||
|
label: TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||||
|
property: providerId,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
i18n,
|
i18n,
|
||||||
};
|
};
|
||||||
|
@ -172,7 +180,13 @@ export default {
|
||||||
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
|
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
|
||||||
<p>
|
<p>
|
||||||
{{ provider.description }}
|
{{ provider.description }}
|
||||||
<gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
<gl-link
|
||||||
|
:href="provider.url"
|
||||||
|
target="_blank"
|
||||||
|
@click="trackProviderLearnMoreClick(provider.id)"
|
||||||
|
>
|
||||||
|
{{ __('Learn more.') }}
|
||||||
|
</gl-link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
export const TRACK_TOGGLE_TRAINING_PROVIDER_ACTION = 'toggle_security_training_provider';
|
export const TRACK_TOGGLE_TRAINING_PROVIDER_ACTION = 'toggle_security_training_provider';
|
||||||
export const TRACK_TOGGLE_TRAINING_PROVIDER_LABEL = 'update_security_training_provider';
|
export const TRACK_TOGGLE_TRAINING_PROVIDER_LABEL = 'update_security_training_provider';
|
||||||
|
|
||||||
|
export const TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION = 'click_link';
|
||||||
|
export const TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL = 'security_training_provider';
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
import {
|
import {
|
||||||
GlButton,
|
GlButton,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlLink,
|
|
||||||
GlBadge,
|
|
||||||
GlSafeHtmlDirective,
|
GlSafeHtmlDirective,
|
||||||
GlTooltipDirective,
|
GlTooltipDirective,
|
||||||
GlIntersectionObserver,
|
GlIntersectionObserver,
|
||||||
|
@ -17,6 +15,7 @@ import Poll from '~/lib/utils/poll';
|
||||||
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
||||||
import StatusIcon from './status_icon.vue';
|
import StatusIcon from './status_icon.vue';
|
||||||
import Actions from './actions.vue';
|
import Actions from './actions.vue';
|
||||||
|
import ChildContent from './child_content.vue';
|
||||||
import { generateText } from './utils';
|
import { generateText } from './utils';
|
||||||
|
|
||||||
export const LOADING_STATES = {
|
export const LOADING_STATES = {
|
||||||
|
@ -30,12 +29,11 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
GlButton,
|
GlButton,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlLink,
|
|
||||||
GlBadge,
|
|
||||||
GlIntersectionObserver,
|
GlIntersectionObserver,
|
||||||
SmartVirtualList,
|
SmartVirtualList,
|
||||||
StatusIcon,
|
StatusIcon,
|
||||||
Actions,
|
Actions,
|
||||||
|
ChildContent,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
SafeHtml: GlSafeHtmlDirective,
|
SafeHtml: GlSafeHtmlDirective,
|
||||||
|
@ -196,9 +194,6 @@ export default {
|
||||||
Sentry.captureException(e);
|
Sentry.captureException(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isArray(arr) {
|
|
||||||
return Array.isArray(arr);
|
|
||||||
},
|
|
||||||
appear(index) {
|
appear(index) {
|
||||||
if (index === this.fullData.length - 1) {
|
if (index === this.fullData.length - 1) {
|
||||||
this.showFade = false;
|
this.showFade = false;
|
||||||
|
@ -299,60 +294,14 @@ export default {
|
||||||
class="gl-py-3 gl-pl-7"
|
class="gl-py-3 gl-pl-7"
|
||||||
data-testid="extension-list-item"
|
data-testid="extension-list-item"
|
||||||
>
|
>
|
||||||
<div class="gl-w-full">
|
<gl-intersection-observer
|
||||||
<div v-if="data.header" class="gl-mb-2">
|
:options="{ rootMargin: '100px', thresholds: 0.1 }"
|
||||||
<template v-if="isArray(data.header)">
|
class="gl-w-full"
|
||||||
<component
|
@appear="appear(index)"
|
||||||
:is="headerI === 0 ? 'strong' : 'span'"
|
@disappear="disappear(index)"
|
||||||
v-for="(header, headerI) in data.header"
|
>
|
||||||
:key="headerI"
|
<child-content :data="data" :widget-label="widgetLabel" :level="2" />
|
||||||
v-safe-html="generateText(header)"
|
</gl-intersection-observer>
|
||||||
class="gl-display-block"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<strong v-else v-safe-html="generateText(data.header)"></strong>
|
|
||||||
</div>
|
|
||||||
<div class="gl-display-flex">
|
|
||||||
<status-icon
|
|
||||||
v-if="data.icon"
|
|
||||||
:icon-name="data.icon.name"
|
|
||||||
:size="12"
|
|
||||||
class="gl-pl-0"
|
|
||||||
/>
|
|
||||||
<gl-intersection-observer
|
|
||||||
:options="{ rootMargin: '100px', thresholds: 0.1 }"
|
|
||||||
class="gl-w-full"
|
|
||||||
@appear="appear(index)"
|
|
||||||
@disappear="disappear(index)"
|
|
||||||
>
|
|
||||||
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
|
||||||
<div class="gl-mr-4 gl-display-flex gl-align-items-center">
|
|
||||||
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
|
|
||||||
</div>
|
|
||||||
<div v-if="data.link">
|
|
||||||
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
|
||||||
</div>
|
|
||||||
<div v-if="data.supportingText">
|
|
||||||
<p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p>
|
|
||||||
</div>
|
|
||||||
<gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
|
|
||||||
{{ data.badge.text }}
|
|
||||||
</gl-badge>
|
|
||||||
|
|
||||||
<actions
|
|
||||||
:widget="$options.label || $options.name"
|
|
||||||
:tertiary-buttons="data.actions"
|
|
||||||
class="gl-ml-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="data.subtext"
|
|
||||||
v-safe-html="generateText(data.subtext)"
|
|
||||||
class="gl-m-0 gl-font-sm"
|
|
||||||
></p>
|
|
||||||
</gl-intersection-observer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</smart-virtual-list>
|
</smart-virtual-list>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script>
|
||||||
|
import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||||
|
import StatusIcon from './status_icon.vue';
|
||||||
|
import Actions from './actions.vue';
|
||||||
|
import { generateText } from './utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChildContent',
|
||||||
|
components: {
|
||||||
|
GlBadge,
|
||||||
|
GlLink,
|
||||||
|
StatusIcon,
|
||||||
|
Actions,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
SafeHtml: GlSafeHtmlDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
widgetLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isArray(arr) {
|
||||||
|
return Array.isArray(arr);
|
||||||
|
},
|
||||||
|
generateText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{ 'gl-pl-6': level === 3 }" class="gl-w-full">
|
||||||
|
<div v-if="data.header" class="gl-mb-2">
|
||||||
|
<template v-if="isArray(data.header)">
|
||||||
|
<component
|
||||||
|
:is="headerI === 0 ? 'strong' : 'span'"
|
||||||
|
v-for="(header, headerI) in data.header"
|
||||||
|
:key="headerI"
|
||||||
|
v-safe-html="generateText(header)"
|
||||||
|
class="gl-display-block"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<strong v-else v-safe-html="generateText(data.header)"></strong>
|
||||||
|
</div>
|
||||||
|
<div class="gl-display-flex">
|
||||||
|
<status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
|
||||||
|
<div class="gl-w-full">
|
||||||
|
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
||||||
|
<div class="gl-mr-4 gl-display-flex gl-align-items-center">
|
||||||
|
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.link">
|
||||||
|
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.supportingText">
|
||||||
|
<p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p>
|
||||||
|
</div>
|
||||||
|
<gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
|
||||||
|
{{ data.badge.text }}
|
||||||
|
</gl-badge>
|
||||||
|
<actions :widget="widgetLabel" :tertiary-buttons="data.actions" class="gl-ml-auto" />
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="data.subtext"
|
||||||
|
v-safe-html="generateText(data.subtext)"
|
||||||
|
class="gl-m-0 gl-font-sm"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="data.children && level === 2">
|
||||||
|
<ul class="gl-m-0 gl-p-0 gl-list-style-none">
|
||||||
|
<li>
|
||||||
|
<child-content
|
||||||
|
v-for="childData in data.children"
|
||||||
|
:key="childData.id"
|
||||||
|
:data="childData"
|
||||||
|
:widget-label="widgetLabel"
|
||||||
|
:level="3"
|
||||||
|
data-testid="child-content"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -88,6 +88,16 @@ export default {
|
||||||
// text: 'Link text', // Required: Text to be used inside the link
|
// text: 'Link text', // Required: Text to be used inside the link
|
||||||
// },
|
// },
|
||||||
actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
|
actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: `child-${issue.id}`,
|
||||||
|
header: 'New',
|
||||||
|
text: '%{critical_start}1 Critical%{critical_end}',
|
||||||
|
icon: {
|
||||||
|
name: EXTENSION_ICONS.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,7 +58,12 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<h2 v-safe-html="issuable.titleHtml || issuable.title" class="title qa-title" dir="auto"></h2>
|
<h1
|
||||||
|
v-safe-html="issuable.titleHtml || issuable.title"
|
||||||
|
class="title qa-title"
|
||||||
|
dir="auto"
|
||||||
|
data-testid="title"
|
||||||
|
></h1>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="enableEdit"
|
v-if="enableEdit"
|
||||||
v-gl-tooltip.bottom
|
v-gl-tooltip.bottom
|
||||||
|
|
|
@ -4,17 +4,17 @@ module Types
|
||||||
class CommitActionType < BaseInputObject
|
class CommitActionType < BaseInputObject
|
||||||
argument :action, type: Types::CommitActionModeEnum, required: true,
|
argument :action, type: Types::CommitActionModeEnum, required: true,
|
||||||
description: 'Action to perform: create, delete, move, update, or chmod.'
|
description: 'Action to perform: create, delete, move, update, or chmod.'
|
||||||
argument :file_path, type: GraphQL::Types::String, required: true,
|
|
||||||
description: 'Full path to the file.'
|
|
||||||
argument :content, type: GraphQL::Types::String, required: false,
|
argument :content, type: GraphQL::Types::String, required: false,
|
||||||
description: 'Content of the file.'
|
description: 'Content of the file.'
|
||||||
argument :previous_path, type: GraphQL::Types::String, required: false,
|
|
||||||
description: 'Original full path to the file being moved.'
|
|
||||||
argument :last_commit_id, type: GraphQL::Types::String, required: false,
|
|
||||||
description: 'Last known file commit ID.'
|
|
||||||
argument :execute_filemode, type: GraphQL::Types::Boolean, required: false,
|
|
||||||
description: 'Enables/disables the execute flag on the file.'
|
|
||||||
argument :encoding, type: Types::CommitEncodingEnum, required: false,
|
argument :encoding, type: Types::CommitEncodingEnum, required: false,
|
||||||
description: 'Encoding of the file. Default is text.'
|
description: 'Encoding of the file. Default is text.'
|
||||||
|
argument :execute_filemode, type: GraphQL::Types::Boolean, required: false,
|
||||||
|
description: 'Enables/disables the execute flag on the file.'
|
||||||
|
argument :file_path, type: GraphQL::Types::String, required: true,
|
||||||
|
description: 'Full path to the file.'
|
||||||
|
argument :last_commit_id, type: GraphQL::Types::String, required: false,
|
||||||
|
description: 'Last known file commit ID.'
|
||||||
|
argument :previous_path, type: GraphQL::Types::String, required: false,
|
||||||
|
description: 'Original full path to the file being moved.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
module Types
|
module Types
|
||||||
class DiffPathsInputType < BaseInputObject
|
class DiffPathsInputType < BaseInputObject
|
||||||
argument :old_path, GraphQL::Types::String, required: false,
|
|
||||||
description: 'Path of the file on the start SHA.'
|
|
||||||
argument :new_path, GraphQL::Types::String, required: false,
|
argument :new_path, GraphQL::Types::String, required: false,
|
||||||
description: 'Path of the file on the HEAD SHA.'
|
description: 'Path of the file on the HEAD SHA.'
|
||||||
|
argument :old_path, GraphQL::Types::String, required: false,
|
||||||
|
description: 'Path of the file on the start SHA.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,15 @@ module Types
|
||||||
class NegatedIssueFilterInputType < BaseInputObject
|
class NegatedIssueFilterInputType < BaseInputObject
|
||||||
graphql_name 'NegatedIssueFilterInput'
|
graphql_name 'NegatedIssueFilterInput'
|
||||||
|
|
||||||
|
argument :assignee_id, GraphQL::Types::String,
|
||||||
|
required: false,
|
||||||
|
description: 'ID of a user not assigned to the issues.'
|
||||||
|
argument :assignee_usernames, [GraphQL::Types::String],
|
||||||
|
required: false,
|
||||||
|
description: 'Usernames of users not assigned to the issue.'
|
||||||
|
argument :author_username, GraphQL::Types::String,
|
||||||
|
required: false,
|
||||||
|
description: "Username of a user who didn't author the issue."
|
||||||
argument :iids, [GraphQL::Types::String],
|
argument :iids, [GraphQL::Types::String],
|
||||||
required: false,
|
required: false,
|
||||||
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
|
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
|
||||||
|
@ -14,24 +23,15 @@ module Types
|
||||||
argument :milestone_title, [GraphQL::Types::String],
|
argument :milestone_title, [GraphQL::Types::String],
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Milestone not applied to this issue.'
|
description: 'Milestone not applied to this issue.'
|
||||||
argument :release_tag, [GraphQL::Types::String],
|
|
||||||
required: false,
|
|
||||||
description: "Release tag not associated with the issue's milestone. Ignored when parent is a group."
|
|
||||||
argument :author_username, GraphQL::Types::String,
|
|
||||||
required: false,
|
|
||||||
description: "Username of a user who didn't author the issue."
|
|
||||||
argument :assignee_usernames, [GraphQL::Types::String],
|
|
||||||
required: false,
|
|
||||||
description: 'Usernames of users not assigned to the issue.'
|
|
||||||
argument :assignee_id, GraphQL::Types::String,
|
|
||||||
required: false,
|
|
||||||
description: 'ID of a user not assigned to the issues.'
|
|
||||||
argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum,
|
argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Filter by negated milestone wildcard values.'
|
description: 'Filter by negated milestone wildcard values.'
|
||||||
argument :my_reaction_emoji, GraphQL::Types::String,
|
argument :my_reaction_emoji, GraphQL::Types::String,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Filter by reaction emoji applied by the current user.'
|
description: 'Filter by reaction emoji applied by the current user.'
|
||||||
|
argument :release_tag, [GraphQL::Types::String],
|
||||||
|
required: false,
|
||||||
|
description: "Release tag not associated with the issue's milestone. Ignored when parent is a group."
|
||||||
argument :types, [Types::IssueTypeEnum],
|
argument :types, [Types::IssueTypeEnum],
|
||||||
as: :issue_types,
|
as: :issue_types,
|
||||||
description: 'Filters out issues by the given issue types.',
|
description: 'Filters out issues by the given issue types.',
|
||||||
|
|
|
@ -4,13 +4,13 @@ module Types
|
||||||
class JiraUsersMappingInputType < BaseInputObject
|
class JiraUsersMappingInputType < BaseInputObject
|
||||||
graphql_name 'JiraUsersMappingInputType'
|
graphql_name 'JiraUsersMappingInputType'
|
||||||
|
|
||||||
argument :jira_account_id,
|
|
||||||
GraphQL::Types::String,
|
|
||||||
required: true,
|
|
||||||
description: 'Jira account ID of the user.'
|
|
||||||
argument :gitlab_id,
|
argument :gitlab_id,
|
||||||
GraphQL::Types::Int,
|
GraphQL::Types::Int,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'ID of the GitLab user.'
|
description: 'ID of the GitLab user.'
|
||||||
|
argument :jira_account_id,
|
||||||
|
GraphQL::Types::String,
|
||||||
|
required: true,
|
||||||
|
description: 'Jira account ID of the user.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
.detail-page-description.content-block
|
.detail-page-description.content-block
|
||||||
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
|
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
|
||||||
.title-container
|
.title-container
|
||||||
%h2.title= markdown_field(issuable, :title)
|
%h1.title= markdown_field(issuable, :title)
|
||||||
- if issuable.description.present?
|
- if issuable.description.present?
|
||||||
.description
|
.description
|
||||||
.md= markdown_field(issuable, :description)
|
.md= markdown_field(issuable, :description)
|
||||||
|
|
139
db/fixtures/development/33_triage_ops.rb
Normal file
139
db/fixtures/development/33_triage_ops.rb
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require './spec/support/sidekiq_middleware'
|
||||||
|
require './spec/support/helpers/test_env'
|
||||||
|
|
||||||
|
class Gitlab::Seeder::TriageOps
|
||||||
|
WEBHOOK_URL = 'http://0.0.0.0:8080'
|
||||||
|
WEBHOOK_TOKEN = "triage-ops-webhook-token"
|
||||||
|
|
||||||
|
def seed!
|
||||||
|
puts "Updating settings to allow web hooks to localhost"
|
||||||
|
ApplicationSetting.current_without_cache.update!(allow_local_requests_from_web_hooks_and_services: true)
|
||||||
|
|
||||||
|
Sidekiq::Testing.inline! do
|
||||||
|
puts "Ensuring required groups"
|
||||||
|
ensure_group('gitlab-com')
|
||||||
|
ensure_group('gitlab-jh/jh-team')
|
||||||
|
ensure_group('gitlab-org')
|
||||||
|
ensure_group('gitlab-org/gitlab-core-team/community-members')
|
||||||
|
ensure_group('gitlab-org/security')
|
||||||
|
puts "Ensuring required projects"
|
||||||
|
ensure_project('gitlab-org/gitlab')
|
||||||
|
ensure_project('gitlab-org/security/gitlab')
|
||||||
|
puts "Ensuring required bot user"
|
||||||
|
ensure_bot_user
|
||||||
|
puts "Setting up webhooks for #{WEBHOOK_URL}"
|
||||||
|
ensure_webhook_for('gitlab-com')
|
||||||
|
ensure_webhook_for('gitlab-org')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_bot_user
|
||||||
|
bot = User.find_by_username('triagebot')
|
||||||
|
bot ||= User.create!(
|
||||||
|
username: 'triagebot',
|
||||||
|
name: 'Triage Bot',
|
||||||
|
email: 'triagebot@example.com',
|
||||||
|
confirmed_at: DateTime.now,
|
||||||
|
password: SecureRandom.hex.slice(0, 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
ensure_group('gitlab-org').add_maintainer(bot)
|
||||||
|
ensure_group('gitlab-com').add_maintainer(bot)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
scopes: ['api'],
|
||||||
|
name: "API Token #{Time.zone.now}"
|
||||||
|
}
|
||||||
|
response = PersonalAccessTokens::CreateService.new(current_user: bot, target_user: bot, params: params).execute
|
||||||
|
|
||||||
|
unless response.success?
|
||||||
|
raise "Can't create Triage Bot access token: #{response.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Bot with API_TOKEN=#{response[:personal_access_token].token} is present now."
|
||||||
|
|
||||||
|
bot
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_webhook_for(group_path)
|
||||||
|
group = Group.find_by_full_path(group_path)
|
||||||
|
|
||||||
|
hook_params = {
|
||||||
|
enable_ssl_verification: false,
|
||||||
|
token: WEBHOOK_TOKEN,
|
||||||
|
url: WEBHOOK_URL
|
||||||
|
}
|
||||||
|
# Subscribe the hook to all possible events.
|
||||||
|
all_group_hook_events = GroupHook.triggers.values
|
||||||
|
all_group_hook_events.each { |value| hook_params[value] = true }
|
||||||
|
|
||||||
|
group.hooks.delete_all
|
||||||
|
|
||||||
|
hook = group.hooks.new(hook_params)
|
||||||
|
hook.save!
|
||||||
|
|
||||||
|
puts "Hook token '#{hook.token}' for '#{group_path}' group is present now."
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_group(full_path)
|
||||||
|
group = Group.find_by_full_path(full_path)
|
||||||
|
|
||||||
|
return group if group
|
||||||
|
|
||||||
|
parent_path = full_path.split('/')[0..-2].join('/')
|
||||||
|
parent = ensure_group(parent_path) if parent_path.present?
|
||||||
|
|
||||||
|
group_path = full_path.split('/').last
|
||||||
|
|
||||||
|
group = Group.new(
|
||||||
|
name: group_path.titleize,
|
||||||
|
path: group_path,
|
||||||
|
parent_id: parent&.id
|
||||||
|
)
|
||||||
|
group.description = FFaker::Lorem.sentence
|
||||||
|
group.save!
|
||||||
|
|
||||||
|
group.add_owner(User.first)
|
||||||
|
group.create_namespace_settings
|
||||||
|
|
||||||
|
group
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_project(project_fullpath)
|
||||||
|
project = Project.find_by_full_path(project_fullpath)
|
||||||
|
|
||||||
|
return project if project
|
||||||
|
|
||||||
|
group_path = project_fullpath.split('/')[0..-2].join('/')
|
||||||
|
project_path = project_fullpath.split('/').last
|
||||||
|
|
||||||
|
group = ensure_group(group_path)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
namespace_id: group.id,
|
||||||
|
name: project_path.titleize,
|
||||||
|
path: project_path,
|
||||||
|
description: FFaker::Lorem.sentence,
|
||||||
|
visibility_level: Gitlab::VisibilityLevel::PRIVATE,
|
||||||
|
skip_disk_validation: true
|
||||||
|
}
|
||||||
|
|
||||||
|
project = ::Projects::CreateService.new(User.first, params).execute
|
||||||
|
|
||||||
|
raise "Can't create project '#{project_fullpath}' : #{project.errors.full_messages}" unless project.persisted?
|
||||||
|
|
||||||
|
project
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if ENV['SEED_TRIAGE_OPS']
|
||||||
|
Gitlab::Seeder.quiet do
|
||||||
|
Gitlab::Seeder::TriageOps.new.seed!
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts "Skipped. Use the `SEED_TRIAGE_OPS` environment variable to enable seeding data for triage ops project."
|
||||||
|
end
|
|
@ -203,3 +203,28 @@ curl --request PUT \
|
||||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||||
"https://gitlab.example.com/api/v4/topics/1"
|
"https://gitlab.example.com/api/v4/topics/1"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Delete a project topic
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80725) in GitLab 14.9.
|
||||||
|
|
||||||
|
You must be an administrator to delete a project.
|
||||||
|
When you delete a project topic, you also delete the topic assignment for projects.
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
DELETE /topics/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported attributes:
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
| ------------- | ------- | ---------------------- | ------------------- |
|
||||||
|
| `id` | integer | **{check-circle}** Yes | ID of project topic |
|
||||||
|
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl --request DELETE \
|
||||||
|
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||||
|
"https://gitlab.example.com/api/v4/topics/1"
|
||||||
|
```
|
||||||
|
|
|
@ -264,6 +264,8 @@ requirements.
|
||||||
1. Peer member testing is optional but recommended when the risk of a change is high. This includes when the changes are [far-reaching](https://about.gitlab.com/handbook/engineering/development/#reducing-the-impact-of-far-reaching-work) or are for [components critical for security](../code_review.md#security).
|
1. Peer member testing is optional but recommended when the risk of a change is high. This includes when the changes are [far-reaching](https://about.gitlab.com/handbook/engineering/development/#reducing-the-impact-of-far-reaching-work) or are for [components critical for security](../code_review.md#security).
|
||||||
1. Regressions and bugs are covered with tests that reduce the risk of the issue happening
|
1. Regressions and bugs are covered with tests that reduce the risk of the issue happening
|
||||||
again.
|
again.
|
||||||
|
1. Code affected by a feature flag is covered by [automated tests with the feature flag enabled and disabled](../feature_flags/index.md#feature-flags-in-tests), or both
|
||||||
|
states are tested as part of peer member testing or as part of the rollout plan.
|
||||||
1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed.
|
1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed.
|
||||||
1. [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) have been followed.
|
1. [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) have been followed.
|
||||||
1. [Application and rate limit guidelines](../merge_request_application_and_rate_limit_guidelines.md) have been followed.
|
1. [Application and rate limit guidelines](../merge_request_application_and_rate_limit_guidelines.md) have been followed.
|
||||||
|
|
|
@ -530,8 +530,9 @@ Feature.remove(:feature_flag_name)
|
||||||
## Feature flags in tests
|
## Feature flags in tests
|
||||||
|
|
||||||
Introducing a feature flag into the codebase creates an additional code path that should be tested.
|
Introducing a feature flag into the codebase creates an additional code path that should be tested.
|
||||||
It is strongly advised to test all code affected by a feature flag, both when **enabled** and **disabled**
|
It is strongly advised to include automated tests for all code affected by a feature flag, both when **enabled** and **disabled**
|
||||||
to ensure the feature works properly.
|
to ensure the feature works properly. If automated tests are not included for both states, the functionality associated
|
||||||
|
with the untested code path should be manually tested before deployment to production.
|
||||||
|
|
||||||
When using the testing environment, all feature flags are enabled by default.
|
When using the testing environment, all feature flags are enabled by default.
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,7 @@ mentioned below:
|
||||||
variant: '', // Optional: GitLab UI badge variant, defaults to info
|
variant: '', // Optional: GitLab UI badge variant, defaults to info
|
||||||
},
|
},
|
||||||
actions: [], // Optional: Action button for row
|
actions: [], // Optional: Action button for row
|
||||||
|
children: [], // Optional: Child content to render, structure matches the same structure
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -328,14 +328,6 @@ as part of your normal job definition.
|
||||||
A new configuration variable ([`SECRET_DETECTION_HISTORIC_SCAN`](#available-cicd-variables))
|
A new configuration variable ([`SECRET_DETECTION_HISTORIC_SCAN`](#available-cicd-variables))
|
||||||
can be set to change the behavior of the GitLab Secret Detection scan to run on the entire Git history of a repository.
|
can be set to change the behavior of the GitLab Secret Detection scan to run on the entire Git history of a repository.
|
||||||
|
|
||||||
We have created a [short video walkthrough](https://youtu.be/wDtc_K00Y0A) showcasing how you can perform a full history secret detection scan.
|
|
||||||
<div class="video-fallback">
|
|
||||||
See the video: <a href="https://www.youtube.com/watch?v=wDtc_K00Y0A">Walkthrough of historical secret detection scan</a>.
|
|
||||||
</div>
|
|
||||||
<figure class="video-container">
|
|
||||||
<iframe src="https://www.youtube.com/embed/wDtc_K00Y0A" frameborder="0" allowfullscreen="true"> </iframe>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
## Running Secret Detection in an offline environment
|
## Running Secret Detection in an offline environment
|
||||||
|
|
||||||
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access
|
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access
|
||||||
|
|
|
@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
> - Moved to GitLab Free.
|
> - Moved to GitLab Free.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
Free tier namespaces on GitLab SaaS have a 5GB storage limit. To learn more, visit our [pricing page](https://about.gitlab.com/pricing/).
|
Free tier namespaces on GitLab SaaS have a 5GB storage limit. This limit is not visible on the storage quota page nor currently enforced for users who exceed the limit. To learn more, visit our [pricing page](https://about.gitlab.com/pricing/).
|
||||||
|
|
||||||
A project's repository has a free storage quota of 10 GB. When a project's repository reaches
|
A project's repository has a free storage quota of 10 GB. When a project's repository reaches
|
||||||
the quota it is locked. You cannot push changes to a locked project. To monitor the size of each
|
the quota it is locked. You cannot push changes to a locked project. To monitor the size of each
|
||||||
|
|
|
@ -77,5 +77,19 @@ module API
|
||||||
render_validation_error!(topic)
|
render_validation_error!(topic)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Delete a topic' do
|
||||||
|
detail 'This feature was introduced in GitLab 14.9.'
|
||||||
|
end
|
||||||
|
params do
|
||||||
|
requires :id, type: Integer, desc: 'ID of project topic'
|
||||||
|
end
|
||||||
|
delete 'topics/:id' do
|
||||||
|
authenticated_as_admin!
|
||||||
|
|
||||||
|
topic = ::Projects::Topic.find(params[:id])
|
||||||
|
|
||||||
|
destroy_conditionally!(topic)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Incident details', :js do
|
||||||
context 'when a developer+ displays the incident' do
|
context 'when a developer+ displays the incident' do
|
||||||
it 'shows the incident' do
|
it 'shows the incident' do
|
||||||
page.within('.issuable-details') do
|
page.within('.issuable-details') do
|
||||||
expect(find('h2')).to have_content(incident.title)
|
expect(find('h1')).to have_content(incident.title)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ RSpec.describe 'Incident details', :js do
|
||||||
page.within('.issuable-details') do
|
page.within('.issuable-details') do
|
||||||
incident_tabs = find('[data-testid="incident-tabs"]')
|
incident_tabs = find('[data-testid="incident-tabs"]')
|
||||||
|
|
||||||
expect(find('h2')).to have_content(incident.title)
|
expect(find('h1')).to have_content(incident.title)
|
||||||
expect(incident_tabs).to have_content('Summary')
|
expect(incident_tabs).to have_content('Summary')
|
||||||
expect(incident_tabs).to have_content(incident.description)
|
expect(incident_tabs).to have_content(incident.description)
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe 'Incident Detail', :js do
|
||||||
incident_tabs = find('[data-testid="incident-tabs"]')
|
incident_tabs = find('[data-testid="incident-tabs"]')
|
||||||
|
|
||||||
aggregate_failures 'shows title and Summary tab' do
|
aggregate_failures 'shows title and Summary tab' do
|
||||||
expect(find('h2')).to have_content(incident.title)
|
expect(find('h1')).to have_content(incident.title)
|
||||||
expect(incident_tabs).to have_content('Summary')
|
expect(incident_tabs).to have_content('Summary')
|
||||||
expect(incident_tabs).to have_content(incident.description)
|
expect(incident_tabs).to have_content(incident.description)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe 'Issue Detail', :js do
|
||||||
|
|
||||||
it 'shows the issue' do
|
it 'shows the issue' do
|
||||||
page.within('.issuable-details') do
|
page.within('.issuable-details') do
|
||||||
expect(find('h2')).to have_content(issue.title)
|
expect(find('h1')).to have_content(issue.title)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -85,7 +85,7 @@ RSpec.describe 'Issue Detail', :js do
|
||||||
|
|
||||||
it 'shows the issue' do
|
it 'shows the issue' do
|
||||||
page.within('.issuable-details') do
|
page.within('.issuable-details') do
|
||||||
expect(find('h2')).to have_content(issue.reload.title)
|
expect(find('h1')).to have_content(issue.reload.title)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe 'New issue', :js do
|
||||||
it 'allows issue creation' do
|
it 'allows issue creation' do
|
||||||
click_button 'Create issue'
|
click_button 'Create issue'
|
||||||
|
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('issue title')
|
expect(page.find('.issue-details h1.title')).to have_content('issue title')
|
||||||
expect(page.find('.issue-details .description')).to have_content('issue description')
|
expect(page.find('.issue-details .description')).to have_content('issue description')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ RSpec.describe 'New issue', :js do
|
||||||
|
|
||||||
click_button 'Create issue'
|
click_button 'Create issue'
|
||||||
|
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('issue title')
|
expect(page.find('.issue-details h1.title')).to have_content('issue title')
|
||||||
expect(page.find('.issue-details .description')).to have_content('issue description')
|
expect(page.find('.issue-details .description')).to have_content('issue description')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -126,7 +126,7 @@ RSpec.describe 'New issue', :js do
|
||||||
click_button 'Create issue'
|
click_button 'Create issue'
|
||||||
|
|
||||||
expect(page).not_to have_css('.recaptcha')
|
expect(page).not_to have_css('.recaptcha')
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('issue title')
|
expect(page.find('.issue-details h1.title')).to have_content('issue title')
|
||||||
expect(page.find('.issue-details .description')).to have_content('issue description')
|
expect(page.find('.issue-details .description')).to have_content('issue description')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ RSpec.describe 'New issue', :js do
|
||||||
click_button 'Create issue'
|
click_button 'Create issue'
|
||||||
|
|
||||||
expect(page).not_to have_css('.recaptcha')
|
expect(page).not_to have_css('.recaptcha')
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('issue title')
|
expect(page.find('.issue-details h1.title')).to have_content('issue title')
|
||||||
expect(page.find('.issue-details .description')).to have_content('issue description')
|
expect(page.find('.issue-details .description')).to have_content('issue description')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ RSpec.describe 'New issue', :js do
|
||||||
|
|
||||||
click_button 'Create issue'
|
click_button 'Create issue'
|
||||||
|
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('issue title')
|
expect(page.find('.issue-details h1.title')).to have_content('issue title')
|
||||||
expect(page.find('.issue-details .description')).to have_content('issue description')
|
expect(page.find('.issue-details .description')).to have_content('issue description')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -160,7 +160,7 @@ RSpec.describe 'Labels Hierarchy', :js do
|
||||||
|
|
||||||
find('.btn-confirm').click
|
find('.btn-confirm').click
|
||||||
|
|
||||||
expect(page.find('.issue-details h2.title')).to have_content('new created issue')
|
expect(page.find('.issue-details h1.title')).to have_content('new created issue')
|
||||||
expect(page).to have_selector('span.gl-label-text', text: grandparent_group_label.title)
|
expect(page).to have_selector('span.gl-label-text', text: grandparent_group_label.title)
|
||||||
expect(page).to have_selector('span.gl-label-text', text: parent_group_label.title)
|
expect(page).to have_selector('span.gl-label-text', text: parent_group_label.title)
|
||||||
expect(page).to have_selector('span.gl-label-text', text: project_label_1.title)
|
expect(page).to have_selector('span.gl-label-text', text: project_label_1.title)
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||||
import {
|
import {
|
||||||
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
||||||
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
||||||
|
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
|
||||||
|
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||||
} from '~/security_configuration/constants';
|
} from '~/security_configuration/constants';
|
||||||
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
||||||
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
|
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
|
||||||
|
@ -244,6 +246,24 @@ describe('TrainingProviderList component', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`tracks when a provider's "Learn more" link is clicked`, () => {
|
||||||
|
const firstProviderLink = findLinks().at(0);
|
||||||
|
const [{ id: firstProviderId }] = securityTrainingProviders;
|
||||||
|
|
||||||
|
expect(trackingSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
firstProviderLink.vm.$emit('click');
|
||||||
|
|
||||||
|
expect(trackingSpy).toHaveBeenCalledWith(
|
||||||
|
undefined,
|
||||||
|
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
|
||||||
|
{
|
||||||
|
label: TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||||
|
property: firstProviderId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import ChildContent from '~/vue_merge_request_widget/components/extensions/child_content.vue';
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
const mockData = () => ({
|
||||||
|
header: 'Test header',
|
||||||
|
text: 'Test content',
|
||||||
|
icon: {
|
||||||
|
name: 'error',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function factory(propsData) {
|
||||||
|
wrapper = shallowMount(ChildContent, {
|
||||||
|
propsData: {
|
||||||
|
...propsData,
|
||||||
|
widgetLabel: 'Test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MR widget extension child content', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders child components', () => {
|
||||||
|
factory({
|
||||||
|
data: {
|
||||||
|
...mockData(),
|
||||||
|
children: [mockData()],
|
||||||
|
},
|
||||||
|
level: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="child-content"]').exists()).toBe(true);
|
||||||
|
expect(wrapper.find('[data-testid="child-content"]').props('level')).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
|
@ -66,10 +66,12 @@ describe('IssuableTitle', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const titleEl = wrapperWithTitle.find('h2');
|
const titleEl = wrapperWithTitle.find('[data-testid="title"]');
|
||||||
|
|
||||||
expect(titleEl.exists()).toBe(true);
|
expect(titleEl.exists()).toBe(true);
|
||||||
expect(titleEl.html()).toBe('<h2 dir="auto" class="title qa-title"><b>Sample</b> title</h2>');
|
expect(titleEl.html()).toBe(
|
||||||
|
'<h1 dir="auto" data-testid="title" class="title qa-title"><b>Sample</b> title</h1>',
|
||||||
|
);
|
||||||
|
|
||||||
wrapperWithTitle.destroy();
|
wrapperWithTitle.destroy();
|
||||||
});
|
});
|
||||||
|
|
|
@ -255,4 +255,43 @@ RSpec.describe API::Topics do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /topics', :aggregate_failures do
|
||||||
|
context 'as administrator' do
|
||||||
|
it 'deletes a topic' do
|
||||||
|
delete api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 for non existing id' do
|
||||||
|
delete api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 400 for invalid `id` parameter' do
|
||||||
|
delete api('/topics/invalid', admin), params: { name: 'my-topic' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['error']).to eql('id is invalid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as normal user' do
|
||||||
|
it 'returns 403 Forbidden' do
|
||||||
|
delete api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as anonymous' do
|
||||||
|
it 'returns 401 Unauthorized' do
|
||||||
|
delete api("/topics/#{topic_3.id}"), params: { name: 'my-topic' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ResizeObserver } from 'vue-resize'
|
import { ResizeObserver } from 'vue-resize'
|
||||||
|
import 'vue-resize/dist/vue-resize.css'
|
||||||
import { ObserveVisibility } from 'vue-observe-visibility'
|
import { ObserveVisibility } from 'vue-observe-visibility'
|
||||||
import ScrollParent from 'scrollparent'
|
import ScrollParent from 'scrollparent'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
Loading…
Reference in a new issue