Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-22 09:18:00 +00:00
parent c123291db9
commit 932d504aaa
32 changed files with 468 additions and 125 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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';

View file

@ -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

View file

@ -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>

View file

@ -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,
},
},
],
})); }));
}); });
}, },

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.',

View file

@ -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

View file

@ -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)

View 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

View file

@ -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"
```

View file

@ -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.

View file

@ -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.

View file

@ -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
} }
``` ```

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,
},
);
});
}); });
}); });

View file

@ -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);
});
});

View file

@ -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();
}); });

View file

@ -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

View file

@ -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'