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:
|
||||
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
|
||||
|
|
|
@ -4,10 +4,6 @@ GraphQL/OrderedArguments:
|
|||
- app/graphql/resolvers/base_issues_resolver.rb
|
||||
- app/graphql/resolvers/design_management/designs_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_position_base_input_type.rb
|
||||
- app/graphql/types/notes/diff_position_input_type.rb
|
||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="title-container">
|
||||
<h2
|
||||
<h1
|
||||
v-safe-html="titleHtml"
|
||||
:class="{
|
||||
'issue-realtime-pre-pulse': preAnimation,
|
||||
|
@ -76,7 +76,7 @@ export default {
|
|||
}"
|
||||
class="title qa-title"
|
||||
dir="auto"
|
||||
></h2>
|
||||
></h1>
|
||||
<gl-button
|
||||
v-if="showInlineEditButton && canUpdate"
|
||||
v-gl-tooltip.bottom
|
||||
|
|
|
@ -6,6 +6,8 @@ import { __ } from '~/locale';
|
|||
import {
|
||||
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
||||
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
||||
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
|
||||
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||
} from '~/security_configuration/constants';
|
||||
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.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,
|
||||
};
|
||||
|
@ -172,7 +180,13 @@ export default {
|
|||
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
|
||||
<p>
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
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_PROVIDER_LEARN_MORE_CLICK_ACTION = 'click_link';
|
||||
export const TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL = 'security_training_provider';
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import {
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlLink,
|
||||
GlBadge,
|
||||
GlSafeHtmlDirective,
|
||||
GlTooltipDirective,
|
||||
GlIntersectionObserver,
|
||||
|
@ -17,6 +15,7 @@ import Poll from '~/lib/utils/poll';
|
|||
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
||||
import StatusIcon from './status_icon.vue';
|
||||
import Actions from './actions.vue';
|
||||
import ChildContent from './child_content.vue';
|
||||
import { generateText } from './utils';
|
||||
|
||||
export const LOADING_STATES = {
|
||||
|
@ -30,12 +29,11 @@ export default {
|
|||
components: {
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlLink,
|
||||
GlBadge,
|
||||
GlIntersectionObserver,
|
||||
SmartVirtualList,
|
||||
StatusIcon,
|
||||
Actions,
|
||||
ChildContent,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
|
@ -196,9 +194,6 @@ export default {
|
|||
Sentry.captureException(e);
|
||||
});
|
||||
},
|
||||
isArray(arr) {
|
||||
return Array.isArray(arr);
|
||||
},
|
||||
appear(index) {
|
||||
if (index === this.fullData.length - 1) {
|
||||
this.showFade = false;
|
||||
|
@ -299,60 +294,14 @@ export default {
|
|||
class="gl-py-3 gl-pl-7"
|
||||
data-testid="extension-list-item"
|
||||
>
|
||||
<div 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"
|
||||
/>
|
||||
<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>
|
||||
<gl-intersection-observer
|
||||
:options="{ rootMargin: '100px', thresholds: 0.1 }"
|
||||
class="gl-w-full"
|
||||
@appear="appear(index)"
|
||||
@disappear="disappear(index)"
|
||||
>
|
||||
<child-content :data="data" :widget-label="widgetLabel" :level="2" />
|
||||
</gl-intersection-observer>
|
||||
</li>
|
||||
</smart-virtual-list>
|
||||
<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
|
||||
// },
|
||||
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>
|
||||
<div>
|
||||
<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
|
||||
v-if="enableEdit"
|
||||
v-gl-tooltip.bottom
|
||||
|
|
|
@ -4,17 +4,17 @@ module Types
|
|||
class CommitActionType < BaseInputObject
|
||||
argument :action, type: Types::CommitActionModeEnum, required: true,
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
module Types
|
||||
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,
|
||||
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
|
||||
|
|
|
@ -5,6 +5,15 @@ module Types
|
|||
class NegatedIssueFilterInputType < BaseInputObject
|
||||
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],
|
||||
required: false,
|
||||
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
|
||||
|
@ -14,24 +23,15 @@ module Types
|
|||
argument :milestone_title, [GraphQL::Types::String],
|
||||
required: false,
|
||||
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,
|
||||
required: false,
|
||||
description: 'Filter by negated milestone wildcard values.'
|
||||
argument :my_reaction_emoji, GraphQL::Types::String,
|
||||
required: false,
|
||||
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],
|
||||
as: :issue_types,
|
||||
description: 'Filters out issues by the given issue types.',
|
||||
|
|
|
@ -4,13 +4,13 @@ module Types
|
|||
class JiraUsersMappingInputType < BaseInputObject
|
||||
graphql_name 'JiraUsersMappingInputType'
|
||||
|
||||
argument :jira_account_id,
|
||||
GraphQL::Types::String,
|
||||
required: true,
|
||||
description: 'Jira account ID of the user.'
|
||||
argument :gitlab_id,
|
||||
GraphQL::Types::Int,
|
||||
required: false,
|
||||
description: 'ID of the GitLab user.'
|
||||
argument :jira_account_id,
|
||||
GraphQL::Types::String,
|
||||
required: true,
|
||||
description: 'Jira account ID of the user.'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
.detail-page-description.content-block
|
||||
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
|
||||
.title-container
|
||||
%h2.title= markdown_field(issuable, :title)
|
||||
%h1.title= markdown_field(issuable, :title)
|
||||
- if issuable.description.present?
|
||||
.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>" \
|
||||
"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. Regressions and bugs are covered with tests that reduce the risk of the issue happening
|
||||
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. [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.
|
||||
|
|
|
@ -530,8 +530,9 @@ Feature.remove(:feature_flag_name)
|
|||
## Feature flags in tests
|
||||
|
||||
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**
|
||||
to ensure the feature works properly.
|
||||
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. 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.
|
||||
|
||||
|
|
|
@ -128,6 +128,7 @@ mentioned below:
|
|||
variant: '', // Optional: GitLab UI badge variant, defaults to info
|
||||
},
|
||||
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))
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Incident details', :js do
|
|||
context 'when a developer+ displays the incident' do
|
||||
it 'shows the incident' do
|
||||
page.within('.issuable-details') do
|
||||
expect(find('h2')).to have_content(incident.title)
|
||||
expect(find('h1')).to have_content(incident.title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,7 +33,7 @@ RSpec.describe 'Incident details', :js do
|
|||
page.within('.issuable-details') do
|
||||
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(incident.description)
|
||||
end
|
||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe 'Incident Detail', :js do
|
|||
incident_tabs = find('[data-testid="incident-tabs"]')
|
||||
|
||||
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(incident.description)
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe 'Issue Detail', :js do
|
|||
|
||||
it 'shows the issue' 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
|
||||
|
@ -85,7 +85,7 @@ RSpec.describe 'Issue Detail', :js do
|
|||
|
||||
it 'shows the issue' 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
|
||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe 'New issue', :js do
|
|||
it 'allows issue creation' do
|
||||
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')
|
||||
end
|
||||
|
||||
|
@ -111,7 +111,7 @@ RSpec.describe 'New issue', :js do
|
|||
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
@ -126,7 +126,7 @@ RSpec.describe 'New issue', :js do
|
|||
click_button 'Create issue'
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
|
@ -152,7 +152,7 @@ RSpec.describe 'New issue', :js do
|
|||
click_button 'Create issue'
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
|
@ -181,7 +181,7 @@ RSpec.describe 'New issue', :js do
|
|||
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -160,7 +160,7 @@ RSpec.describe 'Labels Hierarchy', :js do
|
|||
|
||||
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: parent_group_label.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 {
|
||||
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
|
||||
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
|
||||
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
|
||||
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
|
||||
} from '~/security_configuration/constants';
|
||||
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
||||
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();
|
||||
const titleEl = wrapperWithTitle.find('h2');
|
||||
const titleEl = wrapperWithTitle.find('[data-testid="title"]');
|
||||
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -255,4 +255,43 @@ RSpec.describe API::Topics do
|
|||
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
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
|
||||
<script>
|
||||
import { ResizeObserver } from 'vue-resize'
|
||||
import 'vue-resize/dist/vue-resize.css'
|
||||
import { ObserveVisibility } from 'vue-observe-visibility'
|
||||
import ScrollParent from 'scrollparent'
|
||||
import config from '../config'
|
||||
|
|
Loading…
Reference in a new issue