diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-22 12:18:00 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-22 12:18:00 +0300 |
commit | 932d504aaadc03b978eccad962a12be93f84be47 (patch) | |
tree | da6fba1cb0b34d8a5c8ecd57f85b23fc3ee9ed63 | |
parent | c123291db91a8c54865779ee29ddebaad159d147 (diff) |
Add latest changes from gitlab-org/gitlab@master
32 files changed, 468 insertions, 125 deletions
diff --git a/.rubocop_todo/graphql/field_definitions.yml b/.rubocop_todo/graphql/field_definitions.yml index e1245a53409..0e2399ba243 100644 --- a/.rubocop_todo/graphql/field_definitions.yml +++ b/.rubocop_todo/graphql/field_definitions.yml @@ -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 diff --git a/.rubocop_todo/graphql/ordered_arguments.yml b/.rubocop_todo/graphql/ordered_arguments.yml index def1b0085e4..4b9cb0bdebd 100644 --- a/.rubocop_todo/graphql/ordered_arguments.yml +++ b/.rubocop_todo/graphql/ordered_arguments.yml @@ -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 diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue index 5e92211685a..1982147e454 100644 --- a/app/assets/javascripts/issues/show/components/title.vue +++ b/app/assets/javascripts/issues/show/components/title.vue @@ -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 diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index 539e2bff17c..dd470562f2d 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -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> diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js index dc76436e91d..86cc3a9c2f9 100644 --- a/app/assets/javascripts/security_configuration/constants.js +++ b/app/assets/javascripts/security_configuration/constants.js @@ -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'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index a25b4ab54e5..4f27a3baca9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -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 diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue new file mode 100644 index 00000000000..5f42c6c7acb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue @@ -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> diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js index 4aeebf095c4..e52f2c2c666 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -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, + }, + }, + ], })); }); }, diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue index b96ce0c43f7..45941174a62 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue @@ -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 diff --git a/app/graphql/types/commit_action_type.rb b/app/graphql/types/commit_action_type.rb index 6f6d6a418dc..1aa3a4e7ee1 100644 --- a/app/graphql/types/commit_action_type.rb +++ b/app/graphql/types/commit_action_type.rb @@ -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 diff --git a/app/graphql/types/diff_paths_input_type.rb b/app/graphql/types/diff_paths_input_type.rb index cdcff1a7e34..c5c75105fda 100644 --- a/app/graphql/types/diff_paths_input_type.rb +++ b/app/graphql/types/diff_paths_input_type.rb @@ -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 diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb index 73e090a4802..fc39efd2493 100644 --- a/app/graphql/types/issues/negated_issue_filter_input_type.rb +++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb @@ -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.', diff --git a/app/graphql/types/jira_users_mapping_input_type.rb b/app/graphql/types/jira_users_mapping_input_type.rb index 37fd05370c0..4df2e27b45a 100644 --- a/app/graphql/types/jira_users_mapping_input_type.rb +++ b/app/graphql/types/jira_users_mapping_input_type.rb @@ -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 diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml index e5197acf06f..1babc6885c2 100644 --- a/app/views/shared/issue_type/_details_content.html.haml +++ b/app/views/shared/issue_type/_details_content.html.haml @@ -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) diff --git a/db/fixtures/development/33_triage_ops.rb b/db/fixtures/development/33_triage_ops.rb new file mode 100644 index 00000000000..38e0d4a6371 --- /dev/null +++ b/db/fixtures/development/33_triage_ops.rb @@ -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 diff --git a/doc/api/topics.md b/doc/api/topics.md index 538b9af9374..1246e36f2cb 100644 --- a/doc/api/topics.md +++ b/doc/api/topics.md @@ -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" +``` diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 47c89f41656..6798d02f046 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -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. diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index af402713f6e..4b417b26381 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -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. diff --git a/doc/development/new_fe_guide/modules/widget_extensions.md b/doc/development/new_fe_guide/modules/widget_extensions.md index 37712cb2cec..d3cd839464d 100644 --- a/doc/development/new_fe_guide/modules/widget_extensions.md +++ b/doc/development/new_fe_guide/modules/widget_extensions.md @@ -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 } ``` diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md index 2ce2d59898f..336d1027656 100644 --- a/doc/user/application_security/secret_detection/index.md +++ b/doc/user/application_security/secret_detection/index.md @@ -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 diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 7fe889b9953..67f7f7f3db5 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -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 diff --git a/lib/api/topics.rb b/lib/api/topics.rb index b9c2bcc2da8..e4a1fa2367e 100644 --- a/lib/api/topics.rb +++ b/lib/api/topics.rb @@ -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 diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb index b704a0515c8..dbcd524cad0 100644 --- a/spec/features/incidents/incident_details_spec.rb +++ b/spec/features/incidents/incident_details_spec.rb @@ -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 diff --git a/spec/features/issues/incident_issue_spec.rb b/spec/features/issues/incident_issue_spec.rb index 3033a138551..2956ddede2e 100644 --- a/spec/features/issues/incident_issue_spec.rb +++ b/spec/features/issues/incident_issue_spec.rb @@ -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 diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index b37c8e9d1cf..88709d66887 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -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 diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index 70d7deadec3..de9888295b9 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -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 diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index a064eef5cc8..479199b72b7 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -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) diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 18c9ada6bde..4d92f0cca22 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -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, + }, + ); + }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js new file mode 100644 index 00000000000..198a4c2823a --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js @@ -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); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js index 93de6dbe306..11e3302d409 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js @@ -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(); }); diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index fd9f306128f..5c17ca9581e 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -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 diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue index 3ce35999d8a..d18c5f3ee97 100644 --- a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue @@ -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' |