diff options
103 files changed, 1211 insertions, 732 deletions
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 022f1c17a93..e48159d67cb 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -39,17 +39,6 @@ review-docs-cleanup: script: - ./scripts/trigger-build.rb docs cleanup -docs-lint markdown: - extends: - - .default-retry - - .docs:rules:docs-lint - # When updating the image version here, update it in /scripts/lint-doc.sh too. - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.16-vale-2.20.1-markdownlint-0.32.2 - stage: lint - needs: [] - script: - - scripts/lint-doc.sh - docs-lint links: extends: - .docs:rules:docs-lint @@ -67,6 +56,33 @@ docs-lint links: # Check the internal links and anchors (in parallel) - "parallel time bundle exec nanoc check ::: internal_links internal_anchors" +.docs-markdown-lint-image: + # When updating the image version here, update it in /scripts/lint-doc.sh too. + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.16-vale-2.20.1-markdownlint-0.32.2 + +docs-lint markdown: + extends: + - .default-retry + - .docs:rules:docs-lint + - .docs-markdown-lint-image + stage: lint + needs: [] + script: + - scripts/lint-doc.sh + +docs-code-quality: + extends: + - .docs:rules:docs-code-quality + - .docs-markdown-lint-image + stage: lint + needs: [] + script: + - vale --output=doc/.vale/vale-json.tmpl --minAlertLevel warning doc/**/*.md > gl-code-quality-report-docs.json || exit_code=$? + artifacts: + reports: + codequality: gl-code-quality-report-docs.json + expire_in: 1 week + ui-docs-links lint: extends: - .docs:rules:docs-lint diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 972412cc94a..1cb12363168 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -7,7 +7,7 @@ include: - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common - ref: 1.5.0 + ref: 1.7.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index f567f896734..0745efff5b2 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -1,6 +1,6 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 1.5.0 + ref: 1.7.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 41c61483bc0..ab98b2a0591 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -213,6 +213,9 @@ - "scripts/lint-doc.sh" - ".gitlab/ci/docs.gitlab-ci.yml" +.docs-code-quality-patterns: &docs-code-quality-patterns + - "doc/**/*.md" + .docs-deprecations-and-removals-patterns: &docs-deprecations-and-removals-patterns - "doc/update/deprecations.md" - "doc/update/removals.md" @@ -791,6 +794,12 @@ when: manual allow_failure: true +.docs:rules:docs-code-quality: + rules: + - <<: *if-default-branch-refs + - <<: *if-default-refs + changes: *docs-code-quality-patterns + .docs:rules:docs-lint: rules: - <<: *if-default-refs diff --git a/.rubocop_todo/gitlab/json.yml b/.rubocop_todo/gitlab/json.yml index 78c54dc157d..5b893710725 100644 --- a/.rubocop_todo/gitlab/json.yml +++ b/.rubocop_todo/gitlab/json.yml @@ -1,7 +1,6 @@ --- # Cop supports --autocorrect. Gitlab/Json: - Details: grace period Exclude: - 'app/controllers/admin/application_settings_controller.rb' - 'app/controllers/concerns/authenticates_with_two_factor.rb' diff --git a/.rubocop_todo/gitlab/no_code_coverage_comment.yml b/.rubocop_todo/gitlab/no_code_coverage_comment.yml index 0dcee5f32ee..195dfe5a81d 100644 --- a/.rubocop_todo/gitlab/no_code_coverage_comment.yml +++ b/.rubocop_todo/gitlab/no_code_coverage_comment.yml @@ -1,6 +1,5 @@ --- Gitlab/NoCodeCoverageComment: - Details: grace period Exclude: - 'app/models/integration.rb' - 'app/services/ci/job_artifacts/destroy_batch_service.rb' diff --git a/.rubocop_todo/gitlab/rspec/avoid_setup.yml b/.rubocop_todo/gitlab/rspec/avoid_setup.yml deleted file mode 100644 index 5f75cda7a21..00000000000 --- a/.rubocop_todo/gitlab/rspec/avoid_setup.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -Gitlab/RSpec/AvoidSetup: - Details: grace period diff --git a/.rubocop_todo/gitlab/service_response.yml b/.rubocop_todo/gitlab/service_response.yml index ccf934e09b3..03b73d6491d 100644 --- a/.rubocop_todo/gitlab/service_response.yml +++ b/.rubocop_todo/gitlab/service_response.yml @@ -1,6 +1,5 @@ --- Gitlab/ServiceResponse: - Details: grace period Exclude: - 'app/services/alert_management/metric_images/upload_service.rb' - 'app/services/analytics/cycle_analytics/stages/base_service.rb' diff --git a/.rubocop_todo/layout/first_array_element_indentation.yml b/.rubocop_todo/layout/first_array_element_indentation.yml index cbe806fc16f..d4a3d2f5524 100644 --- a/.rubocop_todo/layout/first_array_element_indentation.yml +++ b/.rubocop_todo/layout/first_array_element_indentation.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Layout/FirstArrayElementIndentation: - Details: grace period Exclude: - 'spec/lib/gitlab/github_import/importer/issues_importer_spec.rb' - 'spec/lib/gitlab/search/found_blob_spec.rb' diff --git a/.rubocop_todo/layout/trailing_whitespace.yml b/.rubocop_todo/layout/trailing_whitespace.yml index d9c88c989e0..8e3e0795c03 100644 --- a/.rubocop_todo/layout/trailing_whitespace.yml +++ b/.rubocop_todo/layout/trailing_whitespace.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Layout/TrailingWhitespace: - Details: grace period Exclude: - 'app/models/concerns/analytics/cycle_analytics/stage_event_model.rb' - 'db/migrate/20210611100359_rebuild_index_for_cadence_iterations_automation.rb' diff --git a/.rubocop_todo/rails/active_record_callbacks_order.yml b/.rubocop_todo/rails/active_record_callbacks_order.yml index 11ffff36e8d..baeba86c4b9 100644 --- a/.rubocop_todo/rails/active_record_callbacks_order.yml +++ b/.rubocop_todo/rails/active_record_callbacks_order.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Rails/ActiveRecordCallbacksOrder: - Details: grace period Exclude: - 'app/models/award_emoji.rb' - 'app/models/gpg_key.rb' diff --git a/.rubocop_todo/rails/file_path.yml b/.rubocop_todo/rails/file_path.yml index 24a08fa5ee2..898d303bd3d 100644 --- a/.rubocop_todo/rails/file_path.yml +++ b/.rubocop_todo/rails/file_path.yml @@ -1,6 +1,5 @@ --- Rails/FilePath: - Details: grace period Exclude: - 'app/controllers/help_controller.rb' - 'app/helpers/startupjs_helper.rb' diff --git a/.rubocop_todo/rails/helper_instance_variable.yml b/.rubocop_todo/rails/helper_instance_variable.yml index 8f9197c9223..53e376730fd 100644 --- a/.rubocop_todo/rails/helper_instance_variable.yml +++ b/.rubocop_todo/rails/helper_instance_variable.yml @@ -1,6 +1,5 @@ --- Rails/HelperInstanceVariable: - Details: grace period Exclude: - 'app/helpers/admin/user_actions_helper.rb' - 'app/helpers/application_helper.rb' diff --git a/.rubocop_todo/rails/index_with.yml b/.rubocop_todo/rails/index_with.yml index 91a75e198f5..b7bc2a26959 100644 --- a/.rubocop_todo/rails/index_with.yml +++ b/.rubocop_todo/rails/index_with.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Rails/IndexWith: - Details: grace period Exclude: - 'app/helpers/ci/jobs_helper.rb' - 'app/models/ci/build_trace_chunk.rb' diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml index 262804739bd..2ad8d6204c8 100644 --- a/.rubocop_todo/rails/inverse_of.yml +++ b/.rubocop_todo/rails/inverse_of.yml @@ -1,6 +1,5 @@ --- Rails/InverseOf: - Details: grace period Exclude: - 'app/models/alert_management/alert.rb' - 'app/models/alert_management/alert_assignee.rb' diff --git a/.rubocop_todo/rails/redundant_foreign_key.yml b/.rubocop_todo/rails/redundant_foreign_key.yml index 22af6131b33..0d23c51caae 100644 --- a/.rubocop_todo/rails/redundant_foreign_key.yml +++ b/.rubocop_todo/rails/redundant_foreign_key.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Rails/RedundantForeignKey: - Details: grace period Exclude: - 'app/models/alert_management/metric_image.rb' - 'app/models/ci/build.rb' diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index a2d5ba5d89b..90582e45009 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -1,6 +1,5 @@ --- RSpec/FactoryBot/AvoidCreate: - Details: grace period Exclude: - 'ee/spec/presenters/approval_rule_presenter_spec.rb' - 'ee/spec/presenters/audit_event_presenter_spec.rb' diff --git a/.rubocop_todo/rspec/file_path.yml b/.rubocop_todo/rspec/file_path.yml index ebeb967841c..8930b709bfd 100644 --- a/.rubocop_todo/rspec/file_path.yml +++ b/.rubocop_todo/rspec/file_path.yml @@ -1,6 +1,5 @@ --- RSpec/FilePath: - Details: grace period Exclude: - 'ee/spec/frontend/fixtures/analytics/charts.rb' - 'ee/spec/frontend/fixtures/analytics/devops_reports/devops_adoption/enabled_namespaces.rb' diff --git a/.rubocop_todo/rspec/multiple_memoized_helpers.yml b/.rubocop_todo/rspec/multiple_memoized_helpers.yml index 218ec3bb478..e939abd2886 100644 --- a/.rubocop_todo/rspec/multiple_memoized_helpers.yml +++ b/.rubocop_todo/rspec/multiple_memoized_helpers.yml @@ -1,6 +1,5 @@ --- RSpec/MultipleMemoizedHelpers: - Details: grace period Exclude: - 'ee/spec/features/boards/swimlanes/epics_swimlanes_filtering_spec.rb' - 'ee/spec/finders/epics_finder_spec.rb' diff --git a/.rubocop_todo/rspec/scattered_let.yml b/.rubocop_todo/rspec/scattered_let.yml index 61f1a09589f..9a272ec31cc 100644 --- a/.rubocop_todo/rspec/scattered_let.yml +++ b/.rubocop_todo/rspec/scattered_let.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. RSpec/ScatteredLet: - Details: grace period Exclude: - 'ee/spec/features/boards/user_visits_board_spec.rb' - 'ee/spec/features/groups/group_roadmap_spec.rb' diff --git a/.rubocop_todo/style/accessor_grouping.yml b/.rubocop_todo/style/accessor_grouping.yml index a2ba217dbcc..a4fae856953 100644 --- a/.rubocop_todo/style/accessor_grouping.yml +++ b/.rubocop_todo/style/accessor_grouping.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/AccessorGrouping: - Details: grace period Exclude: - 'app/finders/template_finder.rb' - 'app/models/commit.rb' diff --git a/.rubocop_todo/style/bare_percent_literals.yml b/.rubocop_todo/style/bare_percent_literals.yml index 1a155e3cca0..cb40669ca02 100644 --- a/.rubocop_todo/style/bare_percent_literals.yml +++ b/.rubocop_todo/style/bare_percent_literals.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/BarePercentLiterals: - Details: grace period Exclude: - 'app/models/commit.rb' - 'app/models/concerns/storage/legacy_namespace.rb' diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml index 7fbec98ead5..300d8678719 100644 --- a/.rubocop_todo/style/empty_method.yml +++ b/.rubocop_todo/style/empty_method.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/EmptyMethod: - Details: grace period Exclude: - 'app/controllers/admin/application_settings/appearances_controller.rb' - 'app/controllers/admin/applications_controller.rb' diff --git a/.rubocop_todo/style/explicit_block_argument.yml b/.rubocop_todo/style/explicit_block_argument.yml index 346be201322..20e8c976fb7 100644 --- a/.rubocop_todo/style/explicit_block_argument.yml +++ b/.rubocop_todo/style/explicit_block_argument.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/ExplicitBlockArgument: - Details: grace period Exclude: - 'app/controllers/admin/background_migrations_controller.rb' - 'app/controllers/admin/batched_jobs_controller.rb' diff --git a/.rubocop_todo/style/keyword_parameters_order.yml b/.rubocop_todo/style/keyword_parameters_order.yml index 7c283fed44a..ca6cb416b93 100644 --- a/.rubocop_todo/style/keyword_parameters_order.yml +++ b/.rubocop_todo/style/keyword_parameters_order.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/KeywordParametersOrder: - Details: grace period Exclude: - 'app/controllers/concerns/product_analytics_tracking.rb' - 'app/finders/group_descendants_finder.rb' diff --git a/.rubocop_todo/style/numeric_literal_prefix.yml b/.rubocop_todo/style/numeric_literal_prefix.yml index 5de15180438..4e8b608e424 100644 --- a/.rubocop_todo/style/numeric_literal_prefix.yml +++ b/.rubocop_todo/style/numeric_literal_prefix.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/NumericLiteralPrefix: - Details: grace period Exclude: - 'app/models/container_repository.rb' - 'app/services/packages/debian/generate_distribution_key_service.rb' diff --git a/.rubocop_todo/style/redundant_begin.yml b/.rubocop_todo/style/redundant_begin.yml index e96cdb26b08..d2851de201b 100644 --- a/.rubocop_todo/style/redundant_begin.yml +++ b/.rubocop_todo/style/redundant_begin.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/RedundantBegin: - Details: grace period Exclude: - 'app/controllers/concerns/membership_actions.rb' - 'app/controllers/concerns/metrics_dashboard.rb' diff --git a/.rubocop_todo/style/single_argument_dig.yml b/.rubocop_todo/style/single_argument_dig.yml index 3ffd27d26ae..a85039a45f5 100644 --- a/.rubocop_todo/style/single_argument_dig.yml +++ b/.rubocop_todo/style/single_argument_dig.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/SingleArgumentDig: - Details: grace period Exclude: - 'app/graphql/resolvers/namespace_projects_resolver.rb' - 'app/models/ci/build.rb' diff --git a/.rubocop_todo/style/sole_nested_conditional.yml b/.rubocop_todo/style/sole_nested_conditional.yml index 535b8d20765..3c663b5f89a 100644 --- a/.rubocop_todo/style/sole_nested_conditional.yml +++ b/.rubocop_todo/style/sole_nested_conditional.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/SoleNestedConditional: - Details: grace period Exclude: - 'app/controllers/admin/application_settings_controller.rb' - 'app/controllers/ldap/omniauth_callbacks_controller.rb' diff --git a/.rubocop_todo/style/special_global_vars.yml b/.rubocop_todo/style/special_global_vars.yml deleted file mode 100644 index df688872d71..00000000000 --- a/.rubocop_todo/style/special_global_vars.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# Cop supports --auto-correct. -Style/SpecialGlobalVars: - Details: grace period diff --git a/.rubocop_todo/style/string_literals_in_interpolation.yml b/.rubocop_todo/style/string_literals_in_interpolation.yml index fc90e1ce6a6..29e94e77345 100644 --- a/.rubocop_todo/style/string_literals_in_interpolation.yml +++ b/.rubocop_todo/style/string_literals_in_interpolation.yml @@ -1,7 +1,6 @@ --- # Cop supports --auto-correct. Style/StringLiteralsInInterpolation: - Details: grace period Exclude: - 'app/graphql/mutations/base_mutation.rb' - 'app/helpers/colors_helper.rb' diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 44c16324950..f3307977be9 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -109,6 +109,8 @@ export default { :update-filters="true" :index="index" :show-work-item-type-icon="showWorkItemTypeIcon" - /> + > + <slot></slot> + </board-card-inner> </li> </template> diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 3a2b11a649d..1c67a2d9f7f 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -15,7 +15,6 @@ import { updateHistory } from '~/lib/utils/url_utility'; import { sprintf, __, n__ } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue'; import { ListType } from '../constants'; @@ -36,7 +35,6 @@ export default { IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssuableBlockedIcon, GlSprintf, - BoardCardMoveToPosition, WorkItemTypeIcon, IssueHealthStatus: () => import('ee_component/related_items_tree/components/issue_health_status.vue'), @@ -250,8 +248,7 @@ export default { >{{ item.title }}</a > </h4> - <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved --> - <board-card-move-to-position v-if="!isEpicBoard" :item="item" :list="list" :index="index" /> + <slot></slot> </div> <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"> <template v-for="label in orderedLabels"> diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue index ff938219475..706b453e868 100644 --- a/app/assets/javascripts/boards/components/board_card_move_to_position.vue +++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue @@ -1,6 +1,6 @@ <script> import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; @@ -31,10 +31,13 @@ export default { type: Number, required: true, }, + listItemsLength: { + type: Number, + required: true, + }, }, computed: { ...mapState(['pageInfoByListId']), - ...mapGetters(['getBoardItemsByList']), tracking() { return { category: 'boards:list', @@ -42,15 +45,9 @@ export default { property: `type_card`, }; }, - listItems() { - return this.getBoardItemsByList(this.list.id); - }, listHasNextPage() { return this.pageInfoByListId[this.list.id]?.hasNextPage; }, - lengthOfListItemsInBoard() { - return this.listItems?.length; - }, itemIdentifier() { return `${this.item.id}-${this.item.iid}-${this.index}`; }, @@ -58,7 +55,7 @@ export default { return this.index === 0; }, isLastItemInList() { - return this.index === this.lengthOfListItemsInBoard - 1; + return this.index === this.listItemsLength - 1; }, }, methods: { diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index edf1a5ee7e6..ebf19c32e5e 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -7,6 +7,7 @@ import { defaultSortableOptions } from '~/sortable/constants'; import { sortableStart, sortableEnd } from '~/sortable/utils'; import Tracking from '~/tracking'; import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import { toggleFormEventPrefix, DraggableItemTypes } from '../constants'; import eventHub from '../eventhub'; import BoardCard from './board_card.vue'; @@ -27,6 +28,7 @@ export default { BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'), GlLoadingIcon, GlIntersectionObserver, + BoardCardMoveToPosition, }, mixins: [Tracking.mixin()], props: { @@ -309,7 +311,16 @@ export default { :data-draggable-item-type="$options.draggableItemTypes.card" :disabled="disabled" :show-work-item-type-icon="!isEpicBoard" - /> + > + <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved --> + <board-card-move-to-position + v-if="!isEpicBoard" + :item="item" + :index="index" + :list="list" + :list-items-length="boardItems.length" + /> + </board-card> <gl-intersection-observer @appear="onReachingListBottom"> <li v-if="showCount" diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index 789efc8f09d..927ae6f6b81 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -1,48 +1,27 @@ <script> -import { GlButton, GlLink } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; -import ConfidentialityFilter from './confidentiality_filter.vue'; -import StatusFilter from './status_filter.vue'; +import { mapState } from 'vuex'; +import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue'; +import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS } from '../constants'; +import ResultsFilters from './results_filters.vue'; export default { name: 'GlobalSearchSidebar', components: { - GlButton, - GlLink, - StatusFilter, - ConfidentialityFilter, + ResultsFilters, + ScopeNavigation, }, computed: { - ...mapState(['urlQuery', 'sidebarDirty']), - showReset() { - return this.urlQuery.state || this.urlQuery.confidential; + ...mapState(['urlQuery']), + showFilters() { + return this.urlQuery.scope === SCOPE_ISSUES || this.urlQuery.scope === SCOPE_MERGE_REQUESTS; }, - showSidebar() { - return this.urlQuery.scope === 'issues' || this.urlQuery.scope === 'merge_requests'; - }, - }, - methods: { - ...mapActions(['applyQuery', 'resetQuery']), }, }; </script> <template> - <form - class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5" - @submit.prevent="applyQuery" - > - <template v-if="showSidebar"> - <status-filter /> - <confidentiality-filter /> - <div class="gl-display-flex gl-align-items-center gl-mt-3"> - <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> - {{ __('Apply') }} - </gl-button> - <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{ - __('Reset filters') - }}</gl-link> - </div> - </template> - </form> + <section class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"> + <scope-navigation /> + <results-filters v-if="showFilters" /> + </section> </template> diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue new file mode 100644 index 00000000000..5b53f94bb53 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue @@ -0,0 +1,49 @@ +<script> +import { GlButton, GlLink } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ConfidentialityFilter from './confidentiality_filter.vue'; +import StatusFilter from './status_filter.vue'; + +export default { + name: 'ResultsFilters', + components: { + GlButton, + GlLink, + StatusFilter, + ConfidentialityFilter, + }, + mixins: [glFeatureFlagsMixin()], + computed: { + ...mapState(['urlQuery', 'sidebarDirty']), + showReset() { + return this.urlQuery.state || this.urlQuery.confidential; + }, + searchPageVerticalNavFeatureFlag() { + return this.glFeatures.searchPageVerticalNav; + }, + }, + methods: { + ...mapActions(['applyQuery', 'resetQuery']), + }, +}; +</script> + +<template> + <form + :class="searchPageVerticalNavFeatureFlag ? 'gl-px-5' : 'gl-px-0'" + @submit.prevent="applyQuery" + > + <hr v-if="searchPageVerticalNavFeatureFlag" class="gl-my-5 gl-border-gray-100" /> + <status-filter /> + <confidentiality-filter /> + <div class="gl-display-flex gl-align-items-center gl-mt-4"> + <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> + {{ __('Apply') }} + </gl-button> + <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{ + __('Reset filters') + }}</gl-link> + </div> + </form> +</template> diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue new file mode 100644 index 00000000000..37138955415 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue @@ -0,0 +1,66 @@ +<script> +import { GlNav, GlNavItem } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import { formatNumber } from '~/locale'; +import Tracking from '~/tracking'; +import { NAV_LINK_DEFAULT_CLASSES, NUMBER_FORMATING_OPTIONS } from '../constants'; + +export default { + name: 'ScopeNavigation', + components: { + GlNav, + GlNavItem, + }, + mixins: [Tracking.mixin()], + computed: { + ...mapState(['navigation', 'urlQuery']), + }, + created() { + this.fetchSidebarCount(); + }, + methods: { + ...mapActions(['fetchSidebarCount']), + activeClasses(currentScope) { + return currentScope === this.urlQuery.scope ? 'gl-font-weight-bold' : ''; + }, + showFormatedCount(count) { + if (!count) { + return '0'; + } + const countNumber = parseInt(count.replace(/,/g, ''), 10); + return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS); + }, + handleClick(scope) { + this.track('click_menu_item', { label: `vertical_navigation_${scope}` }); + }, + linkClasses(scope) { + return [ + { 'gl-font-weight-bold': scope === this.urlQuery.scope }, + ...this.$options.NAV_LINK_DEFAULT_CLASSES, + ]; + }, + }, + NAV_LINK_DEFAULT_CLASSES, +}; +</script> + +<template> + <nav> + <gl-nav vertical pills> + <gl-nav-item + v-for="(item, scope, index) in navigation" + :key="scope" + :link-classes="linkClasses(scope)" + class="gl-mb-1" + :href="item.link" + :active="urlQuery.scope ? urlQuery.scope === scope : index === 0" + @click="handleClick(scope)" + ><span>{{ item.label }}</span + ><span v-if="item.count" class="gl-font-sm gl-font-weight-normal"> + {{ showFormatedCount(item.count) }} + </span> + </gl-nav-item> + </gl-nav> + <hr class="gl-mt-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" /> + </nav> +</template> diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js new file mode 100644 index 00000000000..3621138afe4 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/constants/index.js @@ -0,0 +1,11 @@ +export const SCOPE_ISSUES = 'issues'; +export const SCOPE_MERGE_REQUESTS = 'merge_requests'; + +export const NUMBER_FORMATING_OPTIONS = { notation: 'compact', compactDisplay: 'short' }; +export const NAV_LINK_DEFAULT_CLASSES = [ + 'gl-display-flex', + 'gl-flex-direction-row', + 'gl-flex-wrap-nowrap', + 'gl-justify-content-space-between', + 'gl-text-gray-900', +]; diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index be5742e5949..2a1b744561d 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -1,6 +1,8 @@ import Api from '~/api'; import { createAlert } from '~/flash'; +import axios from '~/lib/utils/axios_utils'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; +import { logError } from '~/lib/logger'; import { __ } from '~/locale'; import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants'; import * as types from './mutation_types'; @@ -99,3 +101,19 @@ export const applyQuery = ({ state }) => { export const resetQuery = ({ state }) => { visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null })); }; + +export const fetchSidebarCount = ({ commit, state }) => { + const promises = Object.keys(state.navigation).map((scope) => { + // active nav item has count already so we skip it + if (scope !== state.urlQuery.scope) { + return axios + .get(state.navigation[scope].count_link) + .then(({ data: { count } }) => { + commit(types.RECEIVE_NAVIGATION_COUNT, { key: scope, count }); + }) + .catch((e) => logError(e)); + } + return Promise.resolve(); + }); + return Promise.all(promises); +}; diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js index 4fa88822722..e20a43808cf 100644 --- a/app/assets/javascripts/search/store/index.js +++ b/app/assets/javascripts/search/store/index.js @@ -7,11 +7,11 @@ import createState from './state'; Vue.use(Vuex); -export const getStoreConfig = ({ query }) => ({ +export const getStoreConfig = ({ query, navigation }) => ({ actions, getters, mutations, - state: createState({ query }), + state: createState({ query, navigation }), }); const createStore = (config) => new Vuex.Store(getStoreConfig(config)); diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js index bf1e3e79cba..511b93cad2b 100644 --- a/app/assets/javascripts/search/store/mutation_types.js +++ b/app/assets/javascripts/search/store/mutation_types.js @@ -10,3 +10,4 @@ export const SET_QUERY = 'SET_QUERY'; export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY'; export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS'; +export const RECEIVE_NAVIGATION_COUNT = 'RECEIVE_NAVIGATION_COUNT'; diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js index 5d154fe3aa0..c1339845272 100644 --- a/app/assets/javascripts/search/store/mutations.js +++ b/app/assets/javascripts/search/store/mutations.js @@ -32,4 +32,8 @@ export default { [types.LOAD_FREQUENT_ITEMS](state, { key, data }) { state.frequentItems[key] = data; }, + [types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) { + const item = { ...state.navigation[key], count }; + state.navigation = { ...state.navigation, [key]: item }; + }, }; diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js index d4005697f35..b64231a8688 100644 --- a/app/assets/javascripts/search/store/state.js +++ b/app/assets/javascripts/search/store/state.js @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants'; -const createState = ({ query }) => ({ +const createState = ({ query, navigation }) => ({ urlQuery: cloneDeep(query), query, groups: [], @@ -13,5 +13,6 @@ const createState = ({ query }) => ({ [PROJECTS_LOCAL_STORAGE_KEY]: [], }, sidebarDirty: false, + navigation, }); export default createState; diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index d644d8acc98..f37b426cd91 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -219,6 +219,16 @@ } } + .search-sidebar { + .nav-link { + &.active, + &:hover { + background-color: rgba($gray-50, 0.8); + color: $gray-900; + } + } + } + // Sidebar .nav-sidebar li.active > a { color: $gray-900; diff --git a/app/controllers/groups/settings/repository_controller.rb b/app/controllers/groups/settings/repository_controller.rb index cb62ea2a543..ecd5d814fb6 100644 --- a/app/controllers/groups/settings/repository_controller.rb +++ b/app/controllers/groups/settings/repository_controller.rb @@ -8,9 +8,6 @@ module Groups before_action :authorize_create_deploy_token!, only: :create_deploy_token before_action :authorize_access!, only: :show before_action :define_deploy_token_variables, if: -> { can?(current_user, :create_deploy_token, @group) } - before_action do - push_frontend_feature_flag(:ajax_new_deploy_token, @group) - end feature_category :continuous_delivery urgency :low diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 40e89a06b46..c1449dfd3d3 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -26,12 +26,6 @@ class Projects::ArtifactsController < Projects::ApplicationController # It should be removed only after resolving the underlying performance # issues: https://gitlab.com/gitlab-org/gitlab/issues/32281 return head :no_content unless Feature.enabled?(:artifacts_management_page, @project) - - finder = Ci::JobArtifactsFinder.new(@project, artifacts_params) - all_artifacts = finder.execute - - @artifacts = all_artifacts.page(params[:page]).per(MAX_PER_PAGE) - @total_size = all_artifacts.total_size end def destroy diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 6ad85d8a47c..bf231bf012d 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -12,7 +12,6 @@ module Projects before_action :check_builds_available! before_action :define_variables before_action do - push_frontend_feature_flag(:ajax_new_deploy_token, @project) push_frontend_feature_flag(:ci_variable_settings_graphql, @project) end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index 0bf9b05b3b5..90988645d3a 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -6,9 +6,6 @@ module Projects layout 'project_settings' before_action :authorize_admin_project! before_action :define_variables, only: [:create_deploy_token] - before_action do - push_frontend_feature_flag(:ajax_new_deploy_token, @project) - end feature_category :source_code_management, [:show, :cleanup, :update] feature_category :continuous_delivery, [:create_deploy_token] diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index b3f82e89dfa..2f3aba8d09d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -417,7 +417,11 @@ module SearchHelper active_scope = @scope == scope_name result = { label: label, scope: scope_name, data: data, link: search_path(search_params), active: active_scope } - result[:count] = @search_results.formatted_count(scope_name) if active_scope && !@timeout + + if active_scope + result[:count] = !@timeout ? @search_results.formatted_count(scope_name) : 0 + end + result[:count_link] = search_count_path(search_params) unless active_scope result @@ -426,21 +430,24 @@ module SearchHelper # search page scope navigation def search_navigation { - projects: { label: _("Projects"), data: { qa_selector: 'projects_tab' }, condition: @project.nil? }, - blobs: { label: _("Code"), data: { qa_selector: 'code_tab' }, condition: project_search_tabs?(:blobs) || search_service.show_elasticsearch_tabs? || feature_flag_tab_enabled?(:global_search_code_tab) }, - issues: { label: _("Issues"), condition: project_search_tabs?(:issues) || feature_flag_tab_enabled?(:global_search_issues_tab) }, - merge_requests: { label: _("Merge requests"), condition: project_search_tabs?(:merge_requests) || feature_flag_tab_enabled?(:global_search_merge_requests_tab) }, - wiki_blobs: { label: _("Wiki"), condition: project_search_tabs?(:wiki) || search_service.show_elasticsearch_tabs? }, - commits: { label: _("Commits"), condition: project_search_tabs?(:commits) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)) }, - notes: { label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? }, - milestones: { label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? }, - users: { label: _("Users"), condition: show_user_search_tab? }, - snippet_titles: { label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? } + projects: { sort: 1, label: _("Projects"), data: { qa_selector: 'projects_tab' }, condition: @project.nil? }, + blobs: { sort: 2, label: _("Code"), data: { qa_selector: 'code_tab' }, condition: project_search_tabs?(:blobs) || search_service.show_elasticsearch_tabs? || feature_flag_tab_enabled?(:global_search_code_tab) }, + # sort: 3 is reserved for EE items + issues: { sort: 4, label: _("Issues"), condition: project_search_tabs?(:issues) || feature_flag_tab_enabled?(:global_search_issues_tab) }, + merge_requests: { sort: 5, label: _("Merge requests"), condition: project_search_tabs?(:merge_requests) || feature_flag_tab_enabled?(:global_search_merge_requests_tab) }, + wiki_blobs: { sort: 6, label: _("Wiki"), condition: project_search_tabs?(:wiki) || search_service.show_elasticsearch_tabs? }, + commits: { sort: 7, label: _("Commits"), condition: project_search_tabs?(:commits) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)) }, + notes: { sort: 8, label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? }, + milestones: { sort: 9, label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? }, + users: { sort: 10, label: _("Users"), condition: show_user_search_tab? }, + snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? } } end def search_navigation_json - Gitlab::Json.dump(search_navigation.each_with_object({}) do |(key, value), hash| + sorted_navigation = search_navigation.sort_by { |_, h| h[:sort] } + + Gitlab::Json.dump(sorted_navigation.each_with_object({}) do |(key, value), hash| hash[key] = search_filter_link_json(key, value[:label], value[:data], value[:search]) if value[:condition] end) end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index b2bdbda9da9..e1fbf7e3944 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -58,11 +58,7 @@ module Ci end def retryable? - return false unless Feature.enabled?(:ci_recreate_downstream_pipeline, project) - - return false if failed? && (pipeline_loop_detected? || reached_max_descendant_pipelines_depth?) - - super + false end def self.with_preloads diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 8262c3c90e1..ea2ea92dfce 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,7 +1,7 @@ - search_bar_classes = 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4' = render_if_exists 'shared/promotions/promote_advanced_search' -- if Feature.enabled?(:search_page_vertical_nav, current_user) && %w[issues merge_requests].include?(@scope) +- if Feature.enabled?(:search_page_vertical_nav, current_user) .results.gl-md-display-flex.gl-mt-0 #js-search-sidebar{ class: search_bar_classes, data: { navigation: search_navigation_json } } .gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml index 1b48843eb10..0f290f34a95 100644 --- a/app/views/shared/deploy_tokens/_form.html.haml +++ b/app/views/shared/deploy_tokens/_form.html.haml @@ -3,7 +3,7 @@ - group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url } = s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe } -= gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f| += gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: true do |f| .form-group = f.label :name, class: 'label-bold' diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml index faec379e42b..e5f1fd99125 100644 --- a/app/views/shared/deploy_tokens/_index.html.haml +++ b/app/views/shared/deploy_tokens/_index.html.haml @@ -8,20 +8,13 @@ %p = description .settings-content - - if Feature.enabled?(:ajax_new_deploy_token, group_or_project) - #js-new-deploy-token{ data: { - container_registry_enabled: container_registry_enabled?(group_or_project), - packages_registry_enabled: packages_registry_enabled?(group_or_project), - create_new_token_path: create_deploy_token_path(group_or_project), - token_type: group_or_project.is_a?(Group) ? 'group' : 'project', - deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index.md') - } + #js-new-deploy-token{ data: { + container_registry_enabled: container_registry_enabled?(group_or_project), + packages_registry_enabled: packages_registry_enabled?(group_or_project), + create_new_token_path: create_deploy_token_path(group_or_project), + token_type: group_or_project.is_a?(Group) ? 'group' : 'project', + deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index.md') } - - else - - if @created_deploy_token - = render 'shared/deploy_tokens/new_deploy_token', deploy_token: @created_deploy_token - %h5.gl-mt-0 - = s_('DeployTokens|New deploy token') - = render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens + } %hr = render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens diff --git a/config/feature_flags/development/ci_recreate_downstream_pipeline.yml b/config/feature_flags/development/ai_assist_api.yml index 17b8a0965fc..9b7da480f62 100644 --- a/config/feature_flags/development/ci_recreate_downstream_pipeline.yml +++ b/config/feature_flags/development/ai_assist_api.yml @@ -1,8 +1,8 @@ --- -name: ci_recreate_downstream_pipeline -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83613 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358409 -milestone: '14.10' +name: ai_assist_api +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100500 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378470 +milestone: '15.6' type: development -group: group::pipeline authoring +group: group::incubation default_enabled: false diff --git a/config/feature_flags/development/ai_assist_flag.yml b/config/feature_flags/development/ai_assist_flag.yml new file mode 100644 index 00000000000..ce8bff8267c --- /dev/null +++ b/config/feature_flags/development/ai_assist_flag.yml @@ -0,0 +1,8 @@ +--- +name: ai_assist_flag +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100500 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378470 +milestone: '15.6' +type: development +group: group::incubation +default_enabled: false diff --git a/config/feature_flags/development/ajax_new_deploy_token.yml b/config/feature_flags/development/ajax_new_deploy_token.yml deleted file mode 100644 index 6b0d9697006..00000000000 --- a/config/feature_flags/development/ajax_new_deploy_token.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ajax_new_deploy_token -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27141 -rollout_issue_url: -milestone: '12.10' -type: development -group: group::release -default_enabled: false diff --git a/config/metrics/counts_28d/20220621085114_unique_active_users_monthly.yml b/config/metrics/counts_28d/20220621085114_unique_active_users_monthly.yml index 3282a4db173..06b1cbcc3cc 100644 --- a/config/metrics/counts_28d/20220621085114_unique_active_users_monthly.yml +++ b/config/metrics/counts_28d/20220621085114_unique_active_users_monthly.yml @@ -10,7 +10,7 @@ milestone: "15.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90468/ time_frame: 28d data_source: redis_hll -data_category: optional +data_category: operational instrumentation_class: RedisHLLMetric options: events: diff --git a/config/metrics/counts_all/20210216183000_package_events_i_package_pull_package_by_guest.yml b/config/metrics/counts_all/20210216183000_package_events_i_package_pull_package_by_guest.yml index 45dcd531484..1ad33d0d479 100644 --- a/config/metrics/counts_all/20210216183000_package_events_i_package_pull_package_by_guest.yml +++ b/config/metrics/counts_all/20210216183000_package_events_i_package_pull_package_by_guest.yml @@ -1,5 +1,5 @@ --- -data_category: optional +data_category: operational key_path: counts.package_events_i_package_pull_package_by_guest description: A count of packages that have been downloaded from the package registry by a guest diff --git a/config/metrics/counts_all/20210216183005_package_events_i_package_push_package_by_deploy_token.yml b/config/metrics/counts_all/20210216183005_package_events_i_package_push_package_by_deploy_token.yml index 252474155c1..051e99a67e4 100644 --- a/config/metrics/counts_all/20210216183005_package_events_i_package_push_package_by_deploy_token.yml +++ b/config/metrics/counts_all/20210216183005_package_events_i_package_push_package_by_deploy_token.yml @@ -1,5 +1,5 @@ --- -data_category: optional +data_category: operational key_path: counts.package_events_i_package_push_package_by_deploy_token description: A count of packages that have been published to the package registry using a deploy token diff --git a/config/open_api.yml b/config/open_api.yml index 785bf43509f..a73f4916ae5 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -41,6 +41,10 @@ metadata: description: Operations related to managing Flipper-based feature flags - name: freeze_periods description: Operations related to deploy freeze periods + - name: ci_lint + description: Operations related to linting a CI config file + - name: group_export + description: Operations related to export groups - name: merge_requests description: Operations related to merge requests - name: metadata diff --git a/doc/.vale/vale-json.tmpl b/doc/.vale/vale-json.tmpl index f76ca03964c..7969cb704a0 100644 --- a/doc/.vale/vale-json.tmpl +++ b/doc/.vale/vale-json.tmpl @@ -1,47 +1,28 @@ {{- /* Modify Vale's output https://docs.errata.ai/vale/cli#--output */ -}} -{{- /* Keep track of our various counts */ -}} +{{- $fileIndexes := len .Files -}} +{{- $fileIndexes = sub $fileIndexes 1 -}} -{{- $e := 0 -}} -{{- $w := 0 -}} -{{- $s := 0 -}} -{{- $f := 0 -}} - -{{- /* Range over the linted files */ -}} - -{{- range .Files}} - -{{- $f = add1 $f -}} -{{- $path := .Path -}} - -{{- /* Range over the file's alerts */ -}} [ - -{{- range $idx, $a := .Alerts -}} - -{{- $error := "" -}} -{{- if eq .Severity "error" -}} - {{- $error = "blocker" -}} - {{- $e = add1 $e -}} -{{- else if eq .Severity "warning" -}} - {{- $error = "major" -}} - {{- $w = add1 $w -}} -{{- else -}} - {{- $error ="info" -}} - {{- $s = add1 $s -}} -{{- end}} - -{{- /* Variables setup */ -}} - -{{- $path = $path -}} -{{- $loc := printf "%d" .Line -}} -{{- $check := printf "%s" .Check -}} -{{- $message := printf "%s" .Message -}} -{{- $link := printf "%s" .Link -}} -{{- if $idx -}},{{- end -}} - -{{- /* Output */ -}} - + {{- /* Range over the linted files */ -}} + {{- range $idx1, $a := .Files -}} + {{- $path := .Path -}} + + {{/* Range over the file's alerts */}} + {{- range $idx2, $b := .Alerts -}} + {{- $error := "info" -}} + {{- if eq .Severity "error" -}} + {{- $error = "blocker" -}} + {{- else if eq .Severity "warning" -}} + {{- $error = "major" -}} + {{- end}} + + {{- /* Variables setup */ -}} + {{- $loc := printf "%d" .Line -}} + {{- $message := printf "%s" .Message -}} + {{- if $idx2 -}},{{- end -}} + + {{/* Output */}} { "description": "{{ $message }}", "fingerprint": "{{ $path }}-{{ $loc }}", @@ -53,6 +34,6 @@ } } } -{{end -}} -{{end -}} + {{- end}}{{- if (lt $idx1 $fileIndexes) -}},{{- end -}} + {{- end}} ] diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index b40df01cbe9..9b70181cbf1 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -38,6 +38,10 @@ and see the [Development section](../../index.md) for the required guidelines. If you find an issue, please submit a merge request with a fix or improvement, if you can, and include tests. +NOTE: +Consider placing your code behind a feature flag if you think it might affect production availability. +Not sure? Read [When to use feature flags](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags). + If the change is non-trivial, we encourage you to start a discussion with [a product manager or a member of the team](https://about.gitlab.com/handbook/product/categories/). You can do diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index e63fdb740f1..61aa5c5fe02 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -492,20 +492,42 @@ Epics can contain multiple nested child epics, up to a total of 7 levels deep. The maximum number of direct child epics is 100. +### Child epics from other groups + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8502) in GitLab 15.6 [with a flag](../../../administration/feature_flags.md) named `child_epics_from_different_hierarchies`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `child_epics_from_different_hierarchies`. +On GitLab.com, this feature is not available. The feature is not ready for production use. + +You can add a child epic that belongs to a group that is different from the parent epic's group. + +Prerequisites: + +- You must have at least the Reporter role for both the child and parent epics' groups. +- Multi-level child epics must be available for both the child and parent epics' groups. + +To add a child epic from another group, paste the epic's URL when [adding an existing epic](#add-a-child-epic-to-an-epic). + ### Add a child epic to an epic Prerequisites: - You must have at least the Reporter role for the parent epic's group. -To add a child epic to an epic: +To add a new epic as child epic: -1. Select **Add**. -1. Select **Add a new epic**. +1. In an epic, in the **Child issues and epics** section, select **Add > Add a new epic**. +1. Select a group from the dropdown. The epic's group is selected by default. +1. Enter a title for the new epic. +1. Select **Create epic**. + +To add an existing epic as child epic: + +1. In an epic, in the **Child issues and epics** section, select **Add > Add an existing epic**. 1. Identify the epic to be added, using either of the following methods: - Paste the link of the epic. - - Search for the desired issue by entering part of the epic's title, then selecting the desired - match ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9126) in GitLab 12.5). + - Search for the desired issue by entering part of the epic's title, then selecting the desired match. This search is only available for epics within the same group hierarchy. If there are multiple epics to be added, press <kbd>Space</kbd> and repeat this step. 1. Select **Add**. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 222b6d44cfd..32a4f834908 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -469,7 +469,7 @@ project and should only have access to that project. External users: -- Cannot create project, groups, and snippets within their personal namespaces. +- Cannot create project, groups, and snippets in their personal namespaces. - Can only create projects (including forks), subgroups, and snippets within top-level groups to which they are explicitly granted access. - Can only access public projects and projects to which they are explicitly granted access, thus hiding all other internal or private ones from them (like being diff --git a/lib/api/api.rb b/lib/api/api.rb index 19764885769..ee1e6e05e26 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -181,6 +181,7 @@ module API mount ::API::Ci::Runners mount ::API::Clusters::AgentTokens mount ::API::Clusters::Agents + mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys mount ::API::DeployTokens @@ -190,9 +191,11 @@ module API mount ::API::FeatureFlagsUserLists mount ::API::Features mount ::API::FreezePeriods + mount ::API::GroupExport mount ::API::ImportBitbucketServer mount ::API::ImportGithub mount ::API::Keys + mount ::API::Lint mount ::API::MergeRequestDiffs mount ::API::Metadata mount ::API::PersonalAccessTokens::SelfInformation @@ -206,6 +209,7 @@ module API mount ::API::ProtectedTags mount ::API::Releases mount ::API::Release::Links + mount ::API::Repositories mount ::API::ResourceAccessTokens mount ::API::Snippets mount ::API::SnippetRepositoryStorageMoves @@ -239,7 +243,6 @@ module API mount ::API::Ci::Triggers mount ::API::Ci::Variables mount ::API::CommitStatuses - mount ::API::Commits mount ::API::ComposerPackages mount ::API::ConanInstancePackages mount ::API::ConanProjectPackages @@ -262,7 +265,6 @@ module API mount ::API::GroupClusters mount ::API::GroupContainerRepositories mount ::API::GroupDebianDistributions - mount ::API::GroupExport mount ::API::GroupImport mount ::API::GroupLabels mount ::API::GroupMilestones @@ -276,7 +278,6 @@ module API mount ::API::IssueLinks mount ::API::Issues mount ::API::Labels - mount ::API::Lint mount ::API::Markdown mount ::API::MavenPackages mount ::API::Members @@ -307,7 +308,6 @@ module API mount ::API::ProtectedTags mount ::API::PypiPackages mount ::API::RemoteMirrors - mount ::API::Repositories mount ::API::ResourceLabelEvents mount ::API::ResourceMilestoneEvents mount ::API::ResourceStateEvents diff --git a/lib/api/commits.rb b/lib/api/commits.rb index b141e3edc71..3b122fb23a2 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -31,13 +31,31 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, urgency: :low do desc 'Get a project repository commits' do - success Entities::Commit + success code: 200, model: Entities::Commit + tags %w[commits] + is_array true + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not found' } + ] end params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' - optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' - optional :path, type: String, desc: 'The file path' + optional :ref_name, + type: String, + desc: 'The name of a repository branch or tag, if not given the default branch is used', + documentation: { example: 'v1.1.0' } + optional :since, + type: DateTime, + desc: 'Only commits after or on this date will be returned', + documentation: { example: '2021-09-20T11:50:22.001' } + optional :until, + type: DateTime, + desc: 'Only commits before or on this date will be returned', + documentation: { example: '2021-09-20T11:50:22.001' } + optional :path, + type: String, + desc: 'The file path', + documentation: { example: 'README.md' } optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' @@ -81,40 +99,87 @@ module API end desc 'Commit multiple file changes as one commit' do - success Entities::CommitDetail + success code: 200, model: Entities::CommitDetail + tags %w[commits] + failure [ + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' } + ] detail 'This feature was introduced in GitLab 8.13' end params do - requires :branch, type: String, desc: 'Name of the branch to commit into. To create a new branch, also provide either `start_branch` or `start_sha`, and optionally `start_project`.', allow_blank: false - requires :commit_message, type: String, desc: 'Commit message' - requires :actions, type: Array, desc: 'Actions to perform in commit' do - requires :action, type: String, desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze, allow_blank: false - requires :file_path, type: String, desc: 'Full path to the file. Ex. `lib/class.rb`' + requires :branch, + type: String, + desc: 'Name of the branch to commit into. To create a new branch, also provide either `start_branch` or `start_sha`, and optionally `start_project`.', + allow_blank: false, + documentation: { example: 'master' } + requires :commit_message, + type: String, + desc: 'Commit message', + documentation: { example: 'initial commit' } + requires :actions, + type: Array, + desc: 'Actions to perform in commit' do + requires :action, + type: String, + desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze, + allow_blank: false + requires :file_path, + type: String, + desc: 'Full path to the file.', + documentation: { example: 'lib/class.rb' } given action: ->(action) { action == 'move' } do - requires :previous_path, type: String, desc: 'Original full path to the file being moved. Ex. `lib/class1.rb`' + requires :previous_path, + type: String, + desc: 'Original full path to the file being moved.', + documentation: { example: 'lib/class.rb' } end given action: ->(action) { %w[create move].include? action } do - optional :content, type: String, desc: 'File content' + optional :content, + type: String, + desc: 'File content', + documentation: { example: 'Some file content' } end given action: ->(action) { action == 'update' } do - requires :content, type: String, desc: 'File content' + requires :content, + type: String, + desc: 'File content', + documentation: { example: 'Some file content' } end optional :encoding, type: String, desc: '`text` or `base64`', default: 'text', values: %w[text base64] given action: ->(action) { %w[update move delete].include? action } do - optional :last_commit_id, type: String, desc: 'Last known file commit id' + optional :last_commit_id, + type: String, + desc: 'Last known file commit id', + documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' } end given action: ->(action) { action == 'chmod' } do requires :execute_filemode, type: Boolean, desc: 'When `true/false` enables/disables the execute flag on the file.' end end - optional :start_branch, type: String, desc: 'Name of the branch to start the new branch from' - optional :start_sha, type: String, desc: 'SHA of the commit to start the new branch from' + optional :start_branch, + type: String, + desc: 'Name of the branch to start the new branch from', + documentation: { example: 'staging' } + optional :start_sha, + type: String, + desc: 'SHA of the commit to start the new branch from', + documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' } mutually_exclusive :start_branch, :start_sha - optional :start_project, types: [Integer, String], desc: 'The ID or path of the project to start the new branch from' - optional :author_email, type: String, desc: 'Author email for commit' - optional :author_name, type: String, desc: 'Author name for commit' + optional :start_project, + types: [Integer, String], + desc: 'The ID or path of the project to start the new branch from', + documentation: { example: 1 } + optional :author_email, + type: String, + desc: 'Author email for commit', + documentation: { example: 'janedoe@example.com' } + optional :author_name, + type: String, + desc: 'Author name for commit', + documentation: { example: 'Jane Doe' } optional :stats, type: Boolean, default: true, desc: 'Include commit stats' optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch` or `start_sha`' end @@ -151,8 +216,11 @@ module API end desc 'Get a specific commit of a project' do - success Entities::CommitDetail - failure [[404, 'Commit Not Found']] + success code: 200, model: Entities::CommitDetail + tags %w[commits] + failure [ + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' @@ -167,7 +235,12 @@ module API end desc 'Get the diff for a specific commit of a project' do - failure [[404, 'Commit Not Found']] + success code: 200, model: Entities::Diff + tags %w[commits] + is_array true + failure [ + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' @@ -184,8 +257,12 @@ module API end desc "Get a commit's comments" do - success Entities::CommitNote - failure [[404, 'Commit Not Found']] + success code: 200, model: Entities::CommitNote + tags %w[commits] + is_array true + failure [ + { code: 404, message: 'Not found' } + ] end params do use :pagination @@ -202,13 +279,25 @@ module API desc 'Cherry pick commit into a branch' do detail 'This feature was introduced in GitLab 8.15' - success Entities::Commit + success code: 200, model: Entities::Commit + tags %w[commits] + failure [ + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked' - requires :branch, type: String, desc: 'The name of the branch', allow_blank: false + requires :branch, + type: String, + desc: 'The name of the branch', + allow_blank: false, + documentation: { example: 'master' } optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes" - optional :message, type: String, desc: 'A custom commit message to use for the picked commit' + optional :message, + type: String, + desc: 'A custom commit message to use for the picked commit', + documentation: { example: 'Initial commit' } end post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do authorize_push_to_branch!(params[:branch]) @@ -248,11 +337,20 @@ module API desc 'Revert a commit in a branch' do detail 'This feature was introduced in GitLab 11.5' - success Entities::Commit + success code: 200, model: Entities::Commit + tags %w[commits] + failure [ + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'Commit SHA to revert' - requires :branch, type: String, desc: 'Target branch name', allow_blank: false + requires :branch, + type: String, + desc: 'Target branch name', + allow_blank: false, + documentation: { example: 'master' } optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes" end post ':id/repository/commits/:sha/revert', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do @@ -292,7 +390,12 @@ module API desc 'Get all references a commit is pushed to' do detail 'This feature was introduced in GitLab 10.6' - success Entities::BasicRef + success code: 200, model: Entities::BasicRef + tags %w[commits] + is_array true + failure [ + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha' @@ -312,14 +415,28 @@ module API end desc 'Post comment to commit' do - success Entities::CommitNote + success code: 200, model: Entities::CommitNote + tags %w[commits] + failure [ + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to post a comment' - requires :note, type: String, desc: 'The text of the comment' - optional :path, type: String, desc: 'The file path' + requires :note, + type: String, + desc: 'The text of the comment', + documentation: { example: 'Nice code!' } + optional :path, + type: String, + desc: 'The file path', + documentation: { example: 'doc/update/5.4-to-6.0.md' } given :path do - requires :line, type: Integer, desc: 'The line number' + requires :line, + type: Integer, + desc: 'The line number', + documentation: { example: 11 } requires :line_type, type: String, values: %w[new old], default: 'new', desc: 'The type of the line' end end @@ -361,7 +478,12 @@ module API end desc 'Get Merge Requests associated with a commit' do - success Entities::MergeRequestBasic + success code: 200, model: Entities::MergeRequestBasic + tags %w[commits] + is_array true + failure [ + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to find Merge Requests' @@ -383,7 +505,11 @@ module API end desc "Get a commit's signature" do - success Entities::CommitSignature + success code: 200, model: Entities::CommitSignature + tags %w[commits] + failure [ + { code: 404, message: 'Not found' } + ] end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' diff --git a/lib/api/entities/basic_ref.rb b/lib/api/entities/basic_ref.rb index 79c15075d99..1b821a5b0ec 100644 --- a/lib/api/entities/basic_ref.rb +++ b/lib/api/entities/basic_ref.rb @@ -3,7 +3,8 @@ module API module Entities class BasicRef < Grape::Entity - expose :type, :name + expose :type, documentation: { type: 'string', example: 'tag' } + expose :name, documentation: { type: 'string', example: 'v1.1.0' } end end end diff --git a/lib/api/entities/ci/lint/result.rb b/lib/api/entities/ci/lint/result.rb index b44a6e13463..698b02d3b4a 100644 --- a/lib/api/entities/ci/lint/result.rb +++ b/lib/api/entities/ci/lint/result.rb @@ -5,12 +5,17 @@ module API module Ci module Lint class Result < Grape::Entity - expose :valid?, as: :valid - expose :errors - expose :warnings - expose :merged_yaml - expose :includes - expose :jobs, if: -> (result, options) { options[:include_jobs] } + expose :valid?, as: :valid, documentation: { type: 'boolean' } + expose :errors, documentation: { is_array: true, type: 'string', + example: 'variables config should be a hash of key value pairs' } + expose :warnings, documentation: { is_array: true, type: 'string', + example: 'jobs:job may allow multiple pipelines ...' } + expose :merged_yaml, documentation: { type: 'string', example: '---\n:another_test:\n :stage: test\n + :script: echo 2\n:test:\n :stage: test\n :script: echo 1\n' } + expose :includes, documentation: { is_array: true, type: 'object', + example: '{ "blob": "https://gitlab.com/root/example-project/-/blob/...' } + expose :jobs, if: -> (result, options) { options[:include_jobs] }, + documentation: { is_array: true, type: 'object', example: '{ "name": "test: .... }' } end end end diff --git a/lib/api/entities/commit.rb b/lib/api/entities/commit.rb index a247ea2110e..ab1f51289d7 100644 --- a/lib/api/entities/commit.rb +++ b/lib/api/entities/commit.rb @@ -21,7 +21,7 @@ module API expose :web_url, documentation: { type: 'string', - example: 'https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746' + example: 'https://gitlab.example.com/janedoe/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746' } do |commit, _options| c = commit c = c.__subject__ if c.is_a?(Gitlab::View::Presenter::Base) diff --git a/lib/api/entities/commit_note.rb b/lib/api/entities/commit_note.rb index fe91712b48d..0632dc467b8 100644 --- a/lib/api/entities/commit_note.rb +++ b/lib/api/entities/commit_note.rb @@ -3,12 +3,22 @@ module API module Entities class CommitNote < Grape::Entity - expose :note - expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? } - expose(:line) { |note| note.diff_line.try(:line) if note.diff_note? } - expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? } + expose :note, documentation: { type: 'string', example: 'this doc is really nice' } + + expose :path, documentation: { type: 'string', example: 'README.md' } do |note| + note.diff_file.try(:file_path) if note.diff_note? + end + + expose :line, documentation: { type: 'integer', example: 11 } do |note| + note.diff_line.try(:line) if note.diff_note? + end + + expose :line_type, documentation: { type: 'string', example: 'new' } do |note| + note.diff_line.try(:type) if note.diff_note? + end + expose :author, using: Entities::UserBasic - expose :created_at + expose :created_at, documentation: { type: 'dateTime', example: '2016-01-19T09:44:55.600Z' } end end end diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb index 0d8e977a9f5..9430dd5e2a2 100644 --- a/lib/api/entities/commit_signature.rb +++ b/lib/api/entities/commit_signature.rb @@ -3,7 +3,7 @@ module API module Entities class CommitSignature < Grape::Entity - expose :signature_type + expose :signature_type, documentation: { type: 'string', example: 'PGP' } expose :signature, merge: true do |commit, options| if commit.signature.is_a?(::CommitSignatures::GpgSignature) || commit.raw_commit_from_rugged? @@ -13,7 +13,7 @@ module API end end - expose :commit_source do |commit, _| + expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |commit, _| commit.raw_commit_from_rugged? ? "rugged" : "gitaly" end diff --git a/lib/api/entities/compare.rb b/lib/api/entities/compare.rb index 75a36d9bb01..92066868d3c 100644 --- a/lib/api/entities/compare.rb +++ b/lib/api/entities/compare.rb @@ -7,21 +7,24 @@ module API compare.commits.last end - expose :commits, using: Entities::Commit do |compare, _| + expose :commits, documentation: { is_array: true }, using: Entities::Commit do |compare, _| compare.commits end - expose :diffs, using: Entities::Diff do |compare, _| + expose :diffs, documentation: { is_array: true }, using: Entities::Diff do |compare, _| compare.diffs.diffs.to_a end - expose :compare_timeout do |compare, _| + expose :compare_timeout, documentation: { type: 'boolean' } do |compare, _| compare.diffs.diffs.overflow? end - expose :same, as: :compare_same_ref + expose :same, as: :compare_same_ref, documentation: { type: 'boolean' } - expose :web_url do |compare, _| + expose :web_url, + documentation: { + example: "https://gitlab.example.com/gitlab/gitlab-foss/-/compare/main...feature" + } do |compare, _| Gitlab::UrlBuilder.build(compare) end end diff --git a/lib/api/entities/contributor.rb b/lib/api/entities/contributor.rb index 8763822b674..4fab953f0f6 100644 --- a/lib/api/entities/contributor.rb +++ b/lib/api/entities/contributor.rb @@ -3,7 +3,11 @@ module API module Entities class Contributor < Grape::Entity - expose :name, :email, :commits, :additions, :deletions + expose :name, documentation: { example: 'John Doe' } + expose :email, documentation: { example: 'johndoe@example.com' } + expose :commits, documentation: { type: 'integer', example: 117 } + expose :additions, documentation: { type: 'integer', example: 3 } + expose :deletions, documentation: { type: 'integer', example: 5 } end end end diff --git a/lib/api/entities/diff.rb b/lib/api/entities/diff.rb index e92bc5d6b68..e9650f07f00 100644 --- a/lib/api/entities/diff.rb +++ b/lib/api/entities/diff.rb @@ -3,11 +3,17 @@ module API module Entities class Diff < Grape::Entity - expose :old_path, :new_path, :a_mode, :b_mode - expose :new_file?, as: :new_file - expose :renamed_file?, as: :renamed_file - expose :deleted_file?, as: :deleted_file - expose :json_safe_diff, as: :diff + expose :json_safe_diff, as: :diff, documentation: { + type: 'string', + example: '--- a/doc/update/5.4-to-6.0.md\n+++ b/doc/update/5.4-to-6.0.md\n@@ -71,6 +71,8 @@\n...' + } + expose :new_path, documentation: { type: 'string', example: 'doc/update/5.4-to-6.0.md' } + expose :old_path, documentation: { type: 'string', example: 'doc/update/5.4-to-6.0.md' } + expose :a_mode, documentation: { type: 'string', example: '100755' } + expose :b_mode, documentation: { type: 'string', example: '100644' } + expose :new_file?, as: :new_file, documentation: { type: 'boolean' } + expose :renamed_file?, as: :renamed_file, documentation: { type: 'boolean' } + expose :deleted_file?, as: :deleted_file, documentation: { type: 'boolean' } end end end diff --git a/lib/api/entities/tree_object.rb b/lib/api/entities/tree_object.rb index e4e840ebe43..1f542885169 100644 --- a/lib/api/entities/tree_object.rb +++ b/lib/api/entities/tree_object.rb @@ -3,9 +3,12 @@ module API module Entities class TreeObject < Grape::Entity - expose :id, :name, :type, :path + expose :id, documentation: { example: 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba' } + expose :name, documentation: { example: 'html' } + expose :type, documentation: { example: 'tree' } + expose :path, documentation: { example: 'files/html' } - expose :mode do |obj, options| + expose :mode, documentation: { example: '040000' } do |obj, options| filemode = obj.mode filemode = "0" + filemode if filemode.length < 6 filemode diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb index 9e21c5e12b6..eda72d2cfc6 100644 --- a/lib/api/entities/user_public.rb +++ b/lib/api/entities/user_public.rb @@ -17,7 +17,7 @@ module API expose :two_factor_enabled?, as: :two_factor_enabled, documentation: { type: 'boolean', example: true } - expose :external, documentation: { type: 'boolean', example: false } + expose :external expose :private_profile, documentation: { type: 'boolean', example: :null } expose :commit_email_or_default, as: :commit_email diff --git a/lib/api/entities/x509_certificate.rb b/lib/api/entities/x509_certificate.rb index aad11339148..95d4948906e 100644 --- a/lib/api/entities/x509_certificate.rb +++ b/lib/api/entities/x509_certificate.rb @@ -3,13 +3,16 @@ module API module Entities class X509Certificate < Grape::Entity - expose :id - expose :subject - expose :subject_key_identifier - expose :email - expose :serial_number - expose :certificate_status - expose :x509_issuer, using: 'API::Entities::X509Issuer' + expose :id, documentation: { type: 'integer', example: 1 } + expose :subject, documentation: { type: 'string', example: 'CN=gitlab@example.org,OU=Example,O=World' } + expose :subject_key_identifier, documentation: { + type: 'string', + example: 'BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC' + } + expose :email, documentation: { type: 'string', example: 'gitlab@example.org' } + expose :serial_number, documentation: { type: 'integer', example: 278969561018901340486471282831158785578 } + expose :certificate_status, documentation: { type: 'string', example: 'good' } + expose :x509_issuer, using: 'API::Entities::X509Issuer', documentation: { type: 'string', example: '100755' } end end end diff --git a/lib/api/entities/x509_issuer.rb b/lib/api/entities/x509_issuer.rb index b480bc107bc..22429560c72 100644 --- a/lib/api/entities/x509_issuer.rb +++ b/lib/api/entities/x509_issuer.rb @@ -3,10 +3,13 @@ module API module Entities class X509Issuer < Grape::Entity - expose :id - expose :subject - expose :subject_key_identifier - expose :crl_url + expose :id, documentation: { type: 'integer', example: 1 } + expose :subject, documentation: { type: 'string', example: 'CN=PKI,OU=Example,O=World' } + expose :subject_key_identifier, documentation: { + type: 'string', + example: 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB' + } + expose :crl_url, documentation: { type: 'string', example: 'http://example.com/pki.crl' } end end end diff --git a/lib/api/entities/x509_signature.rb b/lib/api/entities/x509_signature.rb index 909b630288c..c3f0cb3659d 100644 --- a/lib/api/entities/x509_signature.rb +++ b/lib/api/entities/x509_signature.rb @@ -3,7 +3,7 @@ module API module Entities class X509Signature < Grape::Entity - expose :verification_status + expose :verification_status, documentation: { type: 'string', example: 'unverified' } expose :x509_certificate, using: 'API::Entities::X509Certificate' end end diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index 2948960a9b4..eb0a01e0d3d 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -15,6 +15,16 @@ module API resource :groups, requirements: { id: %r{[^/]+} } do desc 'Download export' do detail 'This feature was introduced in GitLab 12.5.' + tags %w[group_export] + produces %w[application/octet-stream application/json] + success code: 200 + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end get ':id/export/download' do check_rate_limit! :group_download_export, scope: [current_user, user_group] @@ -32,6 +42,15 @@ module API desc 'Start export' do detail 'This feature was introduced in GitLab 12.5.' + tags %w[group_export] + success code: 202 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 429, message: 'Too many requests' }, + { code: 503, message: 'Service unavailable' } + ] end post ':id/export' do check_rate_limit! :group_export, scope: current_user @@ -47,6 +66,14 @@ module API desc 'Start relations export' do detail 'This feature was introduced in GitLab 13.12' + tags %w[group_export] + success code: 202 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end post ':id/export_relations' do response = ::BulkImports::ExportService.new(portable: user_group, user: current_user).execute @@ -60,6 +87,15 @@ module API desc 'Download relations export' do detail 'This feature was introduced in GitLab 13.12' + produces %w[application/octet-stream application/json] + tags %w[group_export] + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end params do requires :relation, type: String, desc: 'Group relation name' @@ -77,6 +113,15 @@ module API desc 'Relations export status' do detail 'This feature was introduced in GitLab 13.12' + is_array true + tags %w[group_export] + success code: 200, model: Entities::BulkImports::ExportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end get ':id/export_relations/status' do present user_group.bulk_import_exports, with: Entities::BulkImports::ExportStatus diff --git a/lib/api/lint.rb b/lib/api/lint.rb index f65ecf3b4a6..1d19d653d8b 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -15,12 +15,18 @@ module API end namespace :ci do - desc 'Validation of .gitlab-ci.yml content' + desc 'Validates the .gitlab-ci.yml content' do + detail 'Checks if CI/CD YAML configuration is valid' + success code: 200, model: Entities::Ci::Lint::Result + tags %w[ci_lint] + end params do - requires :content, type: String, desc: 'Content of .gitlab-ci.yml' - optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response' - optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response' + requires :content, type: String, desc: 'The CI/CD configuration content' + optional :include_merged_yaml, type: Boolean, desc: 'If the expanded CI/CD configuration should be included in the response' + optional :include_jobs, type: Boolean, desc: 'If the list of jobs should be included in the response. This is + false by default' end + post '/lint', urgency: :low do unauthorized! unless can_lint_ci? @@ -36,14 +42,19 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Validation of .gitlab-ci.yml content' do - detail 'This feature was introduced in GitLab 13.5.' + desc 'Validates a CI YAML configuration with a namespace' do + detail 'Checks if a project’s latest (HEAD of the project’s default branch) .gitlab-ci.yml configuration is + valid' + success Entities::Ci::Lint::Result + tags %w[ci_lint] end params do - optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' - optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response' + optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check. This is false by default' + optional :include_jobs, type: Boolean, desc: 'If the list of jobs that would exist in a static check or pipeline + simulation should be included in the response. This is false by default' optional :ref, type: String, desc: 'Branch or tag used to execute a dry run. Defaults to the default branch of the project. Only used when dry_run is true' end + get ':id/ci/lint', urgency: :low do authorize! :download_code, user_project @@ -60,15 +71,19 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Validation of .gitlab-ci.yml content' do - detail 'This feature was introduced in GitLab 13.6.' + desc 'Validate a CI YAML configuration with a namespace' do + detail 'Checks if CI/CD YAML configuration is valid. This endpoint has namespace specific context' + success code: 200, model: Entities::Ci::Lint::Result + tags %w[ci_lint] end params do requires :content, type: String, desc: 'Content of .gitlab-ci.yml' - optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' - optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response' - optional :ref, type: String, desc: 'Branch or tag used to execute a dry run. Defaults to the default branch of the project. Only used when dry_run is true' + optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check. This is false by default' + optional :include_jobs, type: Boolean, desc: 'If the list of jobs that would exist in a static check or pipeline + simulation should be included in the response. This is false by default' + optional :ref, type: String, desc: 'When dry_run is true, sets the branch or tag to use. Defaults to the project’s default branch when not set' end + post ':id/ci/lint', urgency: :low do authorize! :create_pipeline, user_project diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 640b23b402b..beba2842316 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -15,24 +15,29 @@ module API requires :version, type: String, regexp: Gitlab::Regex.unbounded_semver_regex, - desc: 'The version of the release, using the semantic versioning format' + desc: 'The version of the release, using the semantic versioning format', + documentation: { example: '1.0.0' } optional :from, type: String, - desc: 'The first commit in the range of commits to use for the changelog' + desc: 'The first commit in the range of commits to use for the changelog', + documentation: { example: 'ed899a2f4b50b4370feeea94676502b42383c746' } optional :to, type: String, - desc: 'The last commit in the range of commits to use for the changelog' + desc: 'The last commit in the range of commits to use for the changelog', + documentation: { example: '6104942438c14ec7bd21c6cd5bd995272b3faff6' } optional :date, type: DateTime, - desc: 'The date and time of the release' + desc: 'The date and time of the release', + documentation: { type: 'dateTime', example: '2021-09-20T11:50:22.001+00:00' } optional :trailer, type: String, desc: 'The Git trailer to use for determining if commits are to be included in the changelog', - default: ::Repositories::ChangelogService::DEFAULT_TRAILER + default: ::Repositories::ChangelogService::DEFAULT_TRAILER, + documentation: { example: 'Changelog' } end end @@ -41,7 +46,9 @@ module API feature_category :source_code_management params do - requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + requires :id, types: [String, Integer], + desc: 'The ID or URL-encoded path of the project', + documentation: { example: 1 } end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do @@ -94,15 +101,19 @@ module API success Entities::TreeObject end params do - optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :path, type: String, desc: 'The path of the tree' + optional :ref, type: String, + desc: 'The name of a repository branch or tag, if not given the default branch is used', + documentation: { example: 'main' } + optional :path, type: String, desc: 'The path of the tree', documentation: { example: 'files/html' } optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' use :pagination optional :pagination, type: String, values: %w(legacy keyset none), default: 'legacy', desc: 'Specify the pagination method ("none" is only valid if "recursive" is true)' given pagination: ->(value) { value == 'keyset' } do - optional :page_token, type: String, desc: 'Record from which to start the keyset pagination' + optional :page_token, type: String, + desc: 'Record from which to start the keyset pagination', + documentation: { example: 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba' } end given pagination: ->(value) { value == 'none' } do @@ -123,7 +134,8 @@ module API desc 'Get raw blob contents from the repository' params do - requires :sha, type: String, desc: 'The commit hash' + requires :sha, type: String, + desc: 'The commit hash', documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' } end get ':id/repository/blobs/:sha/raw' do # Load metadata enough to ask Workhorse to load the whole blob @@ -136,7 +148,8 @@ module API desc 'Get a blob from the repository' params do - requires :sha, type: String, desc: 'The commit hash' + requires :sha, type: String, + desc: 'The commit hash', documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' } end get ':id/repository/blobs/:sha' do assign_blob_vars!(limit: -1) @@ -151,9 +164,12 @@ module API desc 'Get an archive of the repository' params do - optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' - optional :format, type: String, desc: 'The archive format' - optional :path, type: String, desc: 'Subfolder of the repository to be downloaded' + optional :sha, type: String, + desc: 'The commit sha of the archive to be downloaded', + documentation: { example: '7d70e02340bac451f281cecf0a980907974bd8be' } + optional :format, type: String, desc: 'The archive format', documentation: { example: 'tar.gz' } + optional :path, type: String, + desc: 'Subfolder of the repository to be downloaded', documentation: { example: 'files/archives' } end get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do check_archive_rate_limit!(current_user, user_project) do @@ -171,9 +187,13 @@ module API success Entities::Compare end params do - requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' - requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' - optional :from_project_id, type: Integer, desc: 'The project to compare from' + requires :from, type: String, + desc: 'The commit, branch name, or tag name to start comparison', + documentation: { example: 'main' } + requires :to, type: String, + desc: 'The commit, branch name, or tag name to stop comparison', + documentation: { example: 'feature' } + optional :from_project_id, type: Integer, desc: 'The project to compare from', documentation: { example: 1 } optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false end get ':id/repository/compare', urgency: :low do @@ -215,7 +235,10 @@ module API success Entities::Commit end params do - requires :refs, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The refs to find the common ancestor of, multiple refs can be passed' + requires :refs, type: Array[String], + coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, + desc: 'The refs to find the common ancestor of, multiple refs can be passed', + documentation: { example: 'main' } end get ':id/repository/merge_base' do refs = params[:refs] @@ -241,12 +264,14 @@ module API desc 'Generates a changelog section for a release and returns it' do detail 'This feature was introduced in GitLab 14.6' + success Entities::Changelog end params do use :release_params optional :config_file, type: String, + documentation: { example: '.gitlab/changelog_config.yml' }, desc: "The file path to the configuration file as stored in the project's Git repository. Defaults to '.gitlab/changelog_config.yml'" end get ':id/repository/changelog' do @@ -264,26 +289,31 @@ module API desc 'Generates a changelog section for a release and commits it in a changelog file' do detail 'This feature was introduced in GitLab 13.9' + success code: 200 end params do use :release_params optional :branch, type: String, - desc: 'The branch to commit the changelog changes to' + desc: 'The branch to commit the changelog changes to', + documentation: { example: 'main' } optional :config_file, type: String, + documentation: { example: '.gitlab/changelog_config.yml' }, desc: "The file path to the configuration file as stored in the project's Git repository. Defaults to '.gitlab/changelog_config.yml'" optional :file, type: String, desc: 'The file to commit the changelog changes to', - default: ::Repositories::ChangelogService::DEFAULT_FILE + default: ::Repositories::ChangelogService::DEFAULT_FILE, + documentation: { example: 'CHANGELOG.md' } optional :message, type: String, - desc: 'The commit message to use when committing the changelog' + desc: 'The commit message to use when committing the changelog', + documentation: { example: 'Initial commit' } end post ':id/repository/changelog' do branch = params[:branch] || user_project.default_branch_or_main diff --git a/spec/controllers/groups/settings/repository_controller_spec.rb b/spec/controllers/groups/settings/repository_controller_spec.rb index cbf55218b94..73a205069f5 100644 --- a/spec/controllers/groups/settings/repository_controller_spec.rb +++ b/spec/controllers/groups/settings/repository_controller_spec.rb @@ -13,88 +13,73 @@ RSpec.describe Groups::Settings::RepositoryController do end describe 'POST create_deploy_token' do - context 'when ajax_new_deploy_token feature flag is disabled for the project' do - before do - stub_feature_flags(ajax_new_deploy_token: false) - entity.add_owner(user) - end + let(:good_deploy_token_params) do + { + name: 'name', + expires_at: 1.day.from_now.to_s, + username: 'deployer', + read_repository: '1', + deploy_token_type: DeployToken.deploy_token_types[:group_type] + } + end - it_behaves_like 'a created deploy token' do - let(:entity) { group } - let(:create_entity_params) { { group_id: group } } - let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] } - end + let(:request_params) do + { + group_id: group.to_param, + deploy_token: deploy_token_params + } end - context 'when ajax_new_deploy_token feature flag is enabled for the project' do - let(:good_deploy_token_params) do - { - name: 'name', - expires_at: 1.day.from_now.to_s, - username: 'deployer', - read_repository: '1', - deploy_token_type: DeployToken.deploy_token_types[:group_type] - } - end + before do + group.add_owner(user) + end + + subject { post :create_deploy_token, params: request_params, format: :json } - let(:request_params) do + context('a good request') do + let(:deploy_token_params) { good_deploy_token_params } + let(:expected_response) do { - group_id: group.to_param, - deploy_token: deploy_token_params + 'id' => be_a(Integer), + 'name' => deploy_token_params[:name], + 'username' => deploy_token_params[:username], + 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), + 'token' => be_a(String), + 'expired' => false, + 'revoked' => false, + 'scopes' => deploy_token_params.inject([]) do |scopes, kv| + key, value = kv + key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes + end } end - before do - group.add_owner(user) - end + it 'creates the deploy token' do + subject - subject { post :create_deploy_token, params: request_params, format: :json } - - context('a good request') do - let(:deploy_token_params) { good_deploy_token_params } - let(:expected_response) do - { - 'id' => be_a(Integer), - 'name' => deploy_token_params[:name], - 'username' => deploy_token_params[:username], - 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), - 'token' => be_a(String), - 'expired' => false, - 'revoked' => false, - 'scopes' => deploy_token_params.inject([]) do |scopes, kv| - key, value = kv - key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes - end - } - end - - it 'creates the deploy token' do - subject - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/deploy_token') - expect(json_response).to match(expected_response) - end + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/deploy_token') + expect(json_response).to match(expected_response) end + end - context('a bad request') do - let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } - let(:expected_response) { { 'message' => "Scopes can't be blank" } } + context('a bad request') do + let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } + let(:expected_response) { { 'message' => "Scopes can't be blank" } } - it 'does not create the deploy token' do - subject + it 'does not create the deploy token' do + subject - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to match(expected_response) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to match(expected_response) end + end - context('an invalid request') do - let(:deploy_token_params) { good_deploy_token_params.except(:name) } + context('an invalid request') do + let(:deploy_token_params) { good_deploy_token_params.except(:name) } - it 'raises a validation error' do - expect { subject }.to raise_error(ActiveRecord::StatementInvalid) - end + it 'raises a validation error' do + expect { subject }.to raise_error(ActiveRecord::StatementInvalid) end end end diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index 2d145b6ff1d..81c1d4acd36 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -30,28 +30,10 @@ RSpec.describe Projects::ArtifactsController do stub_feature_flags(artifacts_management_page: true) end - it 'sets the artifacts variable' do + it 'renders the page' do subject - expect(assigns(:artifacts)).to contain_exactly(*project.job_artifacts) - end - - it 'sets the total size variable' do - subject - - expect(assigns(:total_size)).to eq(project.job_artifacts.total_size) - end - - describe 'pagination' do - before do - stub_const("#{described_class}::MAX_PER_PAGE", 1) - end - - it 'paginates artifacts' do - subject - - expect(assigns(:artifacts)).to contain_exactly(project.reload.job_artifacts.last) - end + expect(response).to have_gitlab_http_status(:ok) end end @@ -65,18 +47,6 @@ RSpec.describe Projects::ArtifactsController do expect(response).to have_gitlab_http_status(:no_content) end - - it 'does not set the artifacts variable' do - subject - - expect(assigns(:artifacts)).to eq(nil) - end - - it 'does not set the total size variable' do - subject - - expect(assigns(:total_size)).to eq(nil) - end end end diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index 6e04e3991ab..ea50ff6caa0 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -51,78 +51,64 @@ RSpec.describe Projects::Settings::RepositoryController do end describe 'POST create_deploy_token' do - context 'when ajax_new_deploy_token feature flag is disabled for the project' do - before do - stub_feature_flags(ajax_new_deploy_token: false) - end - - it_behaves_like 'a created deploy token' do - let(:entity) { project } - let(:create_entity_params) { base_params } - let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] } - end + let(:good_deploy_token_params) do + { + name: 'name', + expires_at: 1.day.from_now.to_s, + username: 'deployer', + read_repository: '1', + deploy_token_type: DeployToken.deploy_token_types[:project_type] + } end - context 'when ajax_new_deploy_token feature flag is enabled for the project' do - let(:good_deploy_token_params) do + let(:request_params) { base_params.merge({ deploy_token: deploy_token_params }) } + + subject { post :create_deploy_token, params: request_params, format: :json } + + context('a good request') do + let(:deploy_token_params) { good_deploy_token_params } + let(:expected_response) do { - name: 'name', - expires_at: 1.day.from_now.to_s, - username: 'deployer', - read_repository: '1', - deploy_token_type: DeployToken.deploy_token_types[:project_type] + 'id' => be_a(Integer), + 'name' => deploy_token_params[:name], + 'username' => deploy_token_params[:username], + 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), + 'token' => be_a(String), + 'expired' => false, + 'revoked' => false, + 'scopes' => deploy_token_params.inject([]) do |scopes, kv| + key, value = kv + key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes + end } end - let(:request_params) { base_params.merge({ deploy_token: deploy_token_params }) } - - subject { post :create_deploy_token, params: request_params, format: :json } - - context('a good request') do - let(:deploy_token_params) { good_deploy_token_params } - let(:expected_response) do - { - 'id' => be_a(Integer), - 'name' => deploy_token_params[:name], - 'username' => deploy_token_params[:username], - 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), - 'token' => be_a(String), - 'expired' => false, - 'revoked' => false, - 'scopes' => deploy_token_params.inject([]) do |scopes, kv| - key, value = kv - key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes - end - } - end + it 'creates the deploy token' do + subject - it 'creates the deploy token' do - subject - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/deploy_token') - expect(json_response).to match(expected_response) - end + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/deploy_token') + expect(json_response).to match(expected_response) end + end - context('a bad request') do - let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } - let(:expected_response) { { 'message' => "Scopes can't be blank" } } + context('a bad request') do + let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) } + let(:expected_response) { { 'message' => "Scopes can't be blank" } } - it 'does not create the deploy token' do - subject + it 'does not create the deploy token' do + subject - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to match(expected_response) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to match(expected_response) end + end - context('an invalid request') do - let(:deploy_token_params) { good_deploy_token_params.except(:name) } + context('an invalid request') do + let(:deploy_token_params) { good_deploy_token_params.except(:name) } - it 'raises a validation error' do - expect { subject }.to raise_error(ActiveRecord::StatementInvalid) - end + it 'raises a validation error' do + expect { subject }.to raise_error(ActiveRecord::StatementInvalid) end end end diff --git a/spec/features/groups/settings/repository_spec.rb b/spec/features/groups/settings/repository_spec.rb index f6b8bbdd35f..cd7dcbdb28d 100644 --- a/spec/features/groups/settings/repository_spec.rb +++ b/spec/features/groups/settings/repository_spec.rb @@ -23,26 +23,9 @@ RSpec.describe 'Group Repository settings', :js do stub_container_registry_config(enabled: true) end - context 'when ajax deploy tokens is enabled' do - before do - stub_feature_flags(ajax_new_deploy_token: true) - end - - it_behaves_like 'a deploy token in settings' do - let(:entity_type) { 'group' } - let(:page_path) { group_settings_repository_path(group) } - end - end - - context 'when ajax deploy tokens is disabled' do - before do - stub_feature_flags(ajax_new_deploy_token: false) - end - - it_behaves_like 'a deploy token in settings' do - let(:entity_type) { 'group' } - let(:page_path) { group_settings_repository_path(group) } - end + it_behaves_like 'a deploy token in settings' do + let(:entity_type) { 'group' } + let(:page_path) { group_settings_repository_path(group) } end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index d9bdbf7aa1a..b25ae80b3c3 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -30,7 +30,6 @@ RSpec.describe 'Projects > Settings > Repository settings' do before do stub_container_registry_config(enabled: true) - stub_feature_flags(ajax_new_deploy_token: project) end it_behaves_like 'a deploy token in settings' do diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb index eed3494ef5b..47383be1ba1 100644 --- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb +++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb @@ -11,7 +11,6 @@ RSpec.describe 'Repository Settings > User sees revoke deploy token modal', :js before do project.add_role(user, role) sign_in(user) - stub_feature_flags(ajax_new_deploy_token: project) visit(project_settings_repository_path(project)) click_button('Revoke') end diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 3ebc51c4bcb..3b77c999a9e 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -7,7 +7,6 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; -import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; @@ -49,7 +48,6 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon'); - const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition); const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon); const performSearchMock = jest.fn(); @@ -143,10 +141,6 @@ describe('Board card component', () => { expect(findHiddenIssueIcon().exists()).toBe(false); }); - it('renders the move to position icon', () => { - expect(findMoveToPositionComponent().exists()).toBe(true); - }); - it('does not render the work type icon by default', () => { expect(findWorkItemIcon().exists()).toBe(false); }); @@ -595,10 +589,5 @@ describe('Board card component', () => { expect(findEpicCountablesTotalWeight().text()).toBe('15'); expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed'); }); - - it('does not render the move to position icon', () => { - createWrapper(); - expect(findMoveToPositionComponent().exists()).toBe(false); - }); }); }); diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 9b0c0b93ffb..3a2beb714e9 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import createComponent from 'jest/boards/board_list_helper'; import BoardCard from '~/boards/components/board_card.vue'; import eventHub from '~/boards/eventhub'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import { mockIssues } from './mock_data'; @@ -15,6 +16,7 @@ describe('Board list component', () => { const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]'); const findDraggable = () => wrapper.findComponent(Draggable); + const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition); const startDrag = ( params = { @@ -99,6 +101,10 @@ describe('Board list component', () => { await nextTick(); expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1'); }); + + it('renders the move to position icon', () => { + expect(findMoveToPositionComponent().exists()).toBe(true); + }); }); describe('load more issues', () => { diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js index 7254b9486ef..8dee3c77787 100644 --- a/spec/frontend/boards/components/board_card_move_to_position_spec.js +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -48,6 +48,7 @@ describe('Board Card Move to position', () => { propsData: { item: mockIssue2, list: mockList, + listItemsLength: 3, index: 0, ...propsData, }, diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js index 0542e96c77c..fa5ccfeb478 100644 --- a/spec/frontend/search/mock_data.js +++ b/spec/frontend/search/mock_data.js @@ -107,3 +107,87 @@ export const PROMISE_ALL_EXPECTED_MUTATIONS = { payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] }, }, }; + +export const MOCK_NAVIGATION = { + projects: { + label: 'Projects', + scope: 'projects', + link: '/search?scope=projects&search=et', + count_link: '/search/count?scope=projects&search=et', + }, + blobs: { + label: 'Code', + scope: 'blobs', + link: '/search?scope=blobs&search=et', + count_link: '/search/count?scope=blobs&search=et', + }, + issues: { + label: 'Issues', + scope: 'issues', + link: '/search?scope=issues&search=et', + active: true, + count: '2,430', + }, + merge_requests: { + label: 'Merge requests', + scope: 'merge_requests', + link: '/search?scope=merge_requests&search=et', + count_link: '/search/count?scope=merge_requests&search=et', + }, + wiki_blobs: { + label: 'Wiki', + scope: 'wiki_blobs', + link: '/search?scope=wiki_blobs&search=et', + count_link: '/search/count?scope=wiki_blobs&search=et', + }, + commits: { + label: 'Commits', + scope: 'commits', + link: '/search?scope=commits&search=et', + count_link: '/search/count?scope=commits&search=et', + }, + notes: { + label: 'Comments', + scope: 'notes', + link: '/search?scope=notes&search=et', + count_link: '/search/count?scope=notes&search=et', + }, + milestones: { + label: 'Milestones', + scope: 'milestones', + link: '/search?scope=milestones&search=et', + count_link: '/search/count?scope=milestones&search=et', + }, + users: { + label: 'Users', + scope: 'users', + link: '/search?scope=users&search=et', + count_link: '/search/count?scope=users&search=et', + }, +}; + +export const MOCK_NAVIGATION_DATA = { + projects: { + label: 'Projects', + scope: 'projects', + link: '/search?scope=projects&search=et', + count_link: '/search/count?scope=projects&search=et', + }, +}; + +export const MOCK_ENDPOINT_RESPONSE = { count: '13' }; + +export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = { + projects: { + count: '13', + label: 'Projects', + scope: 'projects', + link: '/search?scope=projects&search=et', + count_link: '/search/count?scope=projects&search=et', + }, +}; + +export const MOCK_NAVIGATION_ACTION_MUTATION = { + type: types.RECEIVE_NAVIGATION_COUNT, + payload: { key: 'projects', count: '13' }, +}; diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index 89959feec39..b4e3388f067 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -1,11 +1,9 @@ -import { GlButton, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import { MOCK_QUERY } from 'jest/search/mock_data'; import GlobalSearchSidebar from '~/search/sidebar/components/app.vue'; -import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue'; -import StatusFilter from '~/search/sidebar/components/status_filter.vue'; +import ResultsFilters from '~/search/sidebar/components/results_filters.vue'; Vue.use(Vuex); @@ -35,118 +33,49 @@ describe('GlobalSearchSidebar', () => { wrapper.destroy(); }); - const findSidebarForm = () => wrapper.find('form'); - const findStatusFilter = () => wrapper.findComponent(StatusFilter); - const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); - const findApplyButton = () => wrapper.findComponent(GlButton); - const findResetLinkButton = () => wrapper.findComponent(GlLink); + const findSidebarSection = () => wrapper.find('section'); + const findFilters = () => wrapper.findComponent(ResultsFilters); - describe('template', () => { + describe('renders properly', () => { describe('scope=projects', () => { beforeEach(() => { createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } }); }); - it("doesn't render StatusFilter", () => { - expect(findStatusFilter().exists()).toBe(false); + it('shows section', () => { + expect(findSidebarSection().exists()).toBe(true); }); - it("doesn't render ConfidentialityFilter", () => { - expect(findConfidentialityFilter().exists()).toBe(false); - }); - - it("doesn't render ApplyButton", () => { - expect(findApplyButton().exists()).toBe(false); - }); - }); - - describe('scope=issues', () => { - beforeEach(() => { - createComponent({ urlQuery: MOCK_QUERY }); - }); - it('renders StatusFilter', () => { - expect(findStatusFilter().exists()).toBe(true); - }); - - it('renders ConfidentialityFilter', () => { - expect(findConfidentialityFilter().exists()).toBe(true); - }); - - it('renders ApplyButton', () => { - expect(findApplyButton().exists()).toBe(true); - }); - }); - }); - - describe('ApplyButton', () => { - describe('when sidebarDirty is false', () => { - beforeEach(() => { - createComponent({ sidebarDirty: false }); - }); - - it('disables the button', () => { - expect(findApplyButton().attributes('disabled')).toBe('true'); + it("doesn't shows filters", () => { + expect(findFilters().exists()).toBe(false); }); }); - describe('when sidebarDirty is true', () => { + describe('scope=merge_requests', () => { beforeEach(() => { - createComponent({ sidebarDirty: true }); + createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } }); }); - it('enables the button', () => { - expect(findApplyButton().attributes('disabled')).toBe(undefined); - }); - }); - }); - - describe('ResetLinkButton', () => { - describe('with no filter selected', () => { - beforeEach(() => { - createComponent({ urlQuery: {} }); + it('shows section', () => { + expect(findSidebarSection().exists()).toBe(true); }); - it('does not render', () => { - expect(findResetLinkButton().exists()).toBe(false); + it('shows filters', () => { + expect(findFilters().exists()).toBe(true); }); }); - describe('with filter selected', () => { + describe('scope=issues', () => { beforeEach(() => { createComponent({ urlQuery: MOCK_QUERY }); }); - - it('does render', () => { - expect(findResetLinkButton().exists()).toBe(true); - }); - }); - - describe('with filter selected and user updated query back to default', () => { - beforeEach(() => { - createComponent({ urlQuery: MOCK_QUERY, query: {} }); + it('shows section', () => { + expect(findSidebarSection().exists()).toBe(true); }); - it('does render', () => { - expect(findResetLinkButton().exists()).toBe(true); + it('shows filters', () => { + expect(findFilters().exists()).toBe(true); }); }); }); - - describe('actions', () => { - beforeEach(() => { - createComponent({}); - }); - - it('clicking ApplyButton calls applyQuery', () => { - findSidebarForm().trigger('submit'); - - expect(actionSpies.applyQuery).toHaveBeenCalled(); - }); - - it('clicking ResetLinkButton calls resetQuery', () => { - findResetLinkButton().vm.$emit('click'); - - expect(actionSpies.resetQuery).toHaveBeenCalled(); - }); - }); }); diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js new file mode 100644 index 00000000000..4f217709297 --- /dev/null +++ b/spec/frontend/search/sidebar/components/filters_spec.js @@ -0,0 +1,132 @@ +import { GlButton, GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { MOCK_QUERY } from 'jest/search/mock_data'; +import ResultsFilters from '~/search/sidebar/components/results_filters.vue'; +import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue'; +import StatusFilter from '~/search/sidebar/components/status_filter.vue'; + +Vue.use(Vuex); + +describe('GlobalSearchSidebarFilters', () => { + let wrapper; + + const actionSpies = { + applyQuery: jest.fn(), + resetQuery: jest.fn(), + }; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + urlQuery: MOCK_QUERY, + ...initialState, + }, + actions: actionSpies, + }); + + wrapper = shallowMount(ResultsFilters, { + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findSidebarForm = () => wrapper.find('form'); + const findStatusFilter = () => wrapper.findComponent(StatusFilter); + const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); + const findApplyButton = () => wrapper.findComponent(GlButton); + const findResetLinkButton = () => wrapper.findComponent(GlLink); + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent({ urlQuery: MOCK_QUERY }); + }); + it('renders StatusFilter', () => { + expect(findStatusFilter().exists()).toBe(true); + }); + + it('renders ConfidentialityFilter', () => { + expect(findConfidentialityFilter().exists()).toBe(true); + }); + + it('renders ApplyButton', () => { + expect(findApplyButton().exists()).toBe(true); + }); + }); + + describe('ApplyButton', () => { + describe('when sidebarDirty is false', () => { + beforeEach(() => { + createComponent({ sidebarDirty: false }); + }); + + it('disables the button', () => { + expect(findApplyButton().attributes('disabled')).toBe('true'); + }); + }); + + describe('when sidebarDirty is true', () => { + beforeEach(() => { + createComponent({ sidebarDirty: true }); + }); + + it('enables the button', () => { + expect(findApplyButton().attributes('disabled')).toBe(undefined); + }); + }); + }); + + describe('ResetLinkButton', () => { + describe('with no filter selected', () => { + beforeEach(() => { + createComponent({ urlQuery: {} }); + }); + + it('does not render', () => { + expect(findResetLinkButton().exists()).toBe(false); + }); + }); + + describe('with filter selected', () => { + beforeEach(() => { + createComponent({ urlQuery: MOCK_QUERY }); + }); + + it('does render', () => { + expect(findResetLinkButton().exists()).toBe(true); + }); + }); + + describe('with filter selected and user updated query back to default', () => { + beforeEach(() => { + createComponent({ urlQuery: MOCK_QUERY, query: {} }); + }); + + it('does render', () => { + expect(findResetLinkButton().exists()).toBe(true); + }); + }); + }); + + describe('actions', () => { + beforeEach(() => { + createComponent({}); + }); + + it('clicking ApplyButton calls applyQuery', () => { + findSidebarForm().trigger('submit'); + + expect(actionSpies.applyQuery).toHaveBeenCalled(); + }); + + it('clicking ResetLinkButton calls resetQuery', () => { + findResetLinkButton().vm.$emit('click'); + + expect(actionSpies.resetQuery).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js new file mode 100644 index 00000000000..6262a52e01a --- /dev/null +++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js @@ -0,0 +1,80 @@ +import { GlNav, GlNavItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { MOCK_QUERY, MOCK_NAVIGATION } from 'jest/search/mock_data'; +import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue'; + +Vue.use(Vuex); + +describe('ScopeNavigation', () => { + let wrapper; + + const actionSpies = { + fetchSidebarCount: jest.fn(), + }; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + urlQuery: MOCK_QUERY, + navigation: MOCK_NAVIGATION, + ...initialState, + }, + actions: actionSpies, + }); + + wrapper = shallowMount(ScopeNavigation, { + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findNavElement = () => wrapper.find('nav'); + const findGlNav = () => wrapper.findComponent(GlNav); + const findGlNavItems = () => wrapper.findAllComponents(GlNavItem); + const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active')); + const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1); + + describe('scope navigation', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders section', () => { + expect(findNavElement().exists()).toBe(true); + }); + + it('renders nav component', () => { + expect(findGlNav().exists()).toBe(true); + }); + + it('renders all nav item components', () => { + expect(findGlNavItems()).toHaveLength(9); + }); + + it('nav items have proper links', () => { + const linkAtPosition = 3; + const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]]; + + expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link); + }); + }); + + describe('scope navigation sets proper state', () => { + beforeEach(() => { + createComponent(); + }); + + it('sets proper class to active item', () => { + expect(findGlNavItemActive()).toHaveLength(1); + }); + + it('active item', () => { + expect(findGlNavItemActiveCount().text()).toBe('2.4K'); + }); + }); +}); diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index c442ffa521d..3d19b27ff86 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; import { createAlert } from '~/flash'; +import * as logger from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import * as actions from '~/search/store/actions'; @@ -23,6 +24,9 @@ import { MOCK_FRESH_DATA_RES, PRELOAD_EXPECTED_MUTATIONS, PROMISE_ALL_EXPECTED_MUTATIONS, + MOCK_NAVIGATION_DATA, + MOCK_NAVIGATION_ACTION_MUTATION, + MOCK_ENDPOINT_RESPONSE, } from '../mock_data'; jest.mock('~/flash'); @@ -31,6 +35,9 @@ jest.mock('~/lib/utils/url_utility', () => ({ joinPaths: jest.fn().mockReturnValue(''), visitUrl: jest.fn(), })); +jest.mock('~/lib/logger', () => ({ + logError: jest.fn(), +})); describe('Global Search Store Actions', () => { let mock; @@ -260,4 +267,32 @@ describe('Global Search Store Actions', () => { ); }); }); + + describe.each` + action | axiosMock | type | scope | expectedMutations | errorLogs + ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0} + ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0} + ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${'issues'} | ${[]} | ${1} + `('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => { + describe(`on ${type}`, () => { + beforeEach(() => { + state.navigation = MOCK_NAVIGATION_DATA; + state.urlQuery = { + scope, + }; + + if (axiosMock.method) { + mock[axiosMock.method]().reply(axiosMock.code, MOCK_ENDPOINT_RESPONSE); + } + }); + + it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${ + expectedMutations.length === 0 ? '' : 'the correct ' + }mutations for ${scope}`, () => { + return testAction({ action, state, expectedMutations }).then(() => { + expect(logger.logError).toHaveBeenCalledTimes(errorLogs); + }); + }); + }); + }); }); diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js index 25f9b692955..a79ec8f70b0 100644 --- a/spec/frontend/search/store/mutations_spec.js +++ b/spec/frontend/search/store/mutations_spec.js @@ -1,13 +1,20 @@ import * as types from '~/search/store/mutation_types'; import mutations from '~/search/store/mutations'; import createState from '~/search/store/state'; -import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data'; +import { + MOCK_QUERY, + MOCK_GROUPS, + MOCK_PROJECTS, + MOCK_NAVIGATION_DATA, + MOCK_NAVIGATION_ACTION_MUTATION, + MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION, +} from '../mock_data'; describe('Global Search Store Mutations', () => { let state; beforeEach(() => { - state = createState({ query: MOCK_QUERY }); + state = createState({ query: MOCK_QUERY, navigation: MOCK_NAVIGATION_DATA }); }); describe('REQUEST_GROUPS', () => { @@ -90,4 +97,15 @@ describe('Global Search Store Mutations', () => { expect(state.frequentItems[payload.key]).toStrictEqual(payload.data); }); }); + + describe('RECEIVE_NAVIGATION_COUNT', () => { + it('sets frequentItems[key] to data', () => { + const { payload } = MOCK_NAVIGATION_ACTION_MUTATION; + mutations[types.RECEIVE_NAVIGATION_COUNT](state, payload); + + expect(state.navigation[payload.key]).toStrictEqual( + MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION[payload.key], + ); + }); + }); }); diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 4348a57d055..a844d65157d 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -1023,6 +1023,7 @@ RSpec.describe SearchHelper do describe '.search_navigation_json' do using RSpec::Parameterized::TableSyntax + context 'with data' do example_data_1 = { projects: { label: _("Projects"), condition: true }, @@ -1041,13 +1042,13 @@ RSpec.describe SearchHelper do } where(:data, :matcher) do - example_data_1 | -> { include("projects") } - example_data_2 | -> { eq("{}") } - example_data_3 | -> { include("projects", "blobs", "epics") } + example_data_1 | -> { include("projects") } + example_data_2 | -> { eq("{}") } + example_data_3 | -> { include("projects", "blobs", "epics") } end with_them do - it 'converts correctly' do + it 'renders data correctly' do allow(self).to receive(:search_navigation).with(no_args).and_return(data) expect(search_navigation_json).to instance_exec(&matcher) @@ -1056,6 +1057,23 @@ RSpec.describe SearchHelper do end end + describe '.search_navigation_json with .search_navigation' do + before do + allow(self).to receive(:current_user).and_return(build(:user)) + allow(self).to receive(:can?).and_return(true) + allow(self).to receive(:project_search_tabs?).and_return(true) + allow(self).to receive(:feature_flag_tab_enabled?).and_return(true) + allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true) + allow(self).to receive(:feature_flag_tab_enabled?).and_return(true) + @show_snippets = true + @project = nil + end + + it 'test search navigation item order for CE all options enabled' do + expect(Gitlab::Json.parse(search_navigation_json).keys).to eq(%w[projects blobs issues merge_requests wiki_blobs commits notes milestones users snippet_titles]) + end + end + describe '.search_filter_link_json' do using RSpec::Parameterized::TableSyntax diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 3df278a49dd..fce5a3bc7bb 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -37,18 +37,8 @@ RSpec.describe Ci::Bridge do describe '#retryable?' do let(:bridge) { create(:ci_bridge, :success) } - it 'returns true' do - expect(bridge.retryable?).to eq(true) - end - - context 'without ci_recreate_downstream_pipeline ff' do - before do - stub_feature_flags(ci_recreate_downstream_pipeline: false) - end - - it 'returns false' do - expect(bridge.retryable?).to eq(false) - end + it 'returns false' do + expect(bridge.retryable?).to eq(false) end end diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 261eba73424..535999f211d 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -286,12 +286,6 @@ RSpec.describe Ci::Processable do end end - context 'when the processable is a bridge' do - subject(:processable) { create(:ci_bridge, pipeline: pipeline) } - - it_behaves_like 'retryable processable' - end - context 'when the processable is a build' do subject(:processable) { create(:ci_build, pipeline: pipeline) } diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb index 5737303aaf8..540e700efa6 100644 --- a/spec/services/ci/retry_job_service_spec.rb +++ b/spec/services/ci/retry_job_service_spec.rb @@ -27,22 +27,6 @@ RSpec.describe Ci::RetryJobService do project.add_reporter(reporter) end - shared_context 'retryable bridge' do - let_it_be(:downstream_project) { create(:project, :repository) } - - let_it_be_with_refind(:job) do - create(:ci_bridge, :success, - pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage - ) - end - - let_it_be(:job_to_clone) { job } - - before do - job.update!(retried: false) - end - end - shared_context 'retryable build' do let_it_be_with_reload(:job) do create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) @@ -211,20 +195,6 @@ RSpec.describe Ci::RetryJobService do expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError) end - context 'when the job to be cloned is a bridge' do - include_context 'retryable bridge' - - it_behaves_like 'clones the job' - - context 'when given variables' do - let(:new_job) { service.clone!(job, variables: job_variables_attributes) } - - it 'does not give variables to the new bridge' do - expect { new_job }.not_to raise_error - end - end - end - context 'when the job to be cloned is a build' do include_context 'retryable build' @@ -331,20 +301,6 @@ RSpec.describe Ci::RetryJobService do subject { service.execute(job) } - context 'when the job to be retried is a bridge' do - include_context 'retryable bridge' - - it_behaves_like 'retries the job' - - context 'when given variables' do - let(:new_job) { service.clone!(job, variables: job_variables_attributes) } - - it 'does not give variables to the new bridge' do - expect { new_job }.not_to raise_error - end - end - end - context 'when the job to be retried is a build' do include_context 'retryable build' diff --git a/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb b/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb deleted file mode 100644 index 74ad0ccb77a..00000000000 --- a/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'shared/deploy_tokens/_form.html.haml' do - using RSpec::Parameterized::TableSyntax - - let_it_be(:user) { create(:user) } - let_it_be(:token) { build(:deploy_token) } - - RSpec.shared_examples "display deploy token settings" do |role, shows_package_registry_permissions| - before do - subject.add_member(user, role) - allow(view).to receive(:current_user).and_return(user) - stub_config(packages: { enabled: packages_enabled }) - end - - it "correctly renders the form" do - render 'shared/deploy_tokens/form', token: token, group_or_project: subject - - if shows_package_registry_permissions - expect(rendered).to have_content('Allows read-only access to the package registry.') - else - expect(rendered).not_to have_content('Allows read-only access to the package registry.') - end - end - end - - context "when the subject is a project" do - let_it_be(:subject, refind: true) { create(:project, :private) } - - where(:packages_enabled, :feature_enabled, :role, :shows_package_registry_permissions) do - true | true | :maintainer | true - false | true | :maintainer | false - true | false | :maintainer | false - false | false | :maintainer | false - end - - with_them do - before do - subject.update!(packages_enabled: feature_enabled) - end - - it_behaves_like 'display deploy token settings', params[:role], params[:shows_package_registry_permissions] - end - end - - context "when the subject is a group" do - let_it_be(:subject, refind: true) { create(:group, :private) } - - where(:packages_enabled, :role, :shows_package_registry_permissions) do - true | :owner | true - false | :owner | false - true | :maintainer | true - false | :maintainer | false - end - - with_them do - it_behaves_like 'display deploy token settings', params[:role], params[:shows_package_registry_permissions] - end - end -end |