summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-30 15:08:48 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-30 15:08:48 +0000
commit340f15b402eec795fca0e0f29709baef0ecf14a7 (patch)
tree6a7eac5d394f4002b9e5b0c9da12bc12e59ed59c
parent1e254d9f5a46a85c9bb6f24da8265a30fd388db4 (diff)
downloadgitlab-ce-340f15b402eec795fca0e0f29709baef0ecf14a7.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml308
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue41
-rw-r--r--app/assets/javascripts/alert_management/components/alert_sidebar.vue35
-rw-r--r--app/assets/javascripts/alert_management/details.js53
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql2
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql2
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql3
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql2
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql3
-rw-r--r--app/assets/javascripts/design_management_new/components/design_destroyer.vue9
-rw-r--r--app/assets/javascripts/design_management_new/components/design_notes/design_note.vue4
-rw-r--r--app/assets/javascripts/design_management_new/components/toolbar/index.vue20
-rw-r--r--app/assets/javascripts/design_management_new/graphql/queries/app_data.query.graphql4
-rw-r--r--app/assets/javascripts/design_management_new/index.js33
-rw-r--r--app/assets/javascripts/design_management_new/mixins/all_versions.js19
-rw-r--r--app/assets/javascripts/design_management_new/pages/design/index.vue11
-rw-r--r--app/assets/javascripts/design_management_new/pages/index.vue4
-rw-r--r--app/assets/javascripts/design_management_new/router/constants.js1
-rw-r--r--app/assets/javascripts/design_management_new/router/index.js5
-rw-r--r--app/assets/javascripts/design_management_new/router/routes.js46
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue9
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue25
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue72
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue89
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue137
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue74
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js2
-rw-r--r--app/controllers/projects/static_site_editor_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb28
-rw-r--r--app/graphql/types/alert_management/alert_type.rb6
-rw-r--r--app/models/ci/job_artifact.rb9
-rw-r--r--app/models/concerns/featurable.rb3
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/presenters/alert_management/alert_presenter.rb2
-rw-r--r--app/presenters/alert_management/prometheus_alert_presenter.rb4
-rw-r--r--app/presenters/projects/prometheus/alert_presenter.rb10
-rw-r--r--app/services/snippets/destroy_service.rb5
-rw-r--r--app/services/snippets/update_statistics_service.rb33
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml5
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--changelogs/unreleased/222964-collapse-button.yml5
-rw-r--r--changelogs/unreleased/35349-reorder-api.yml5
-rw-r--r--changelogs/unreleased/add_requirements_visibility_access_project_settings.yml5
-rw-r--r--changelogs/unreleased/dedup-merge-request-metrics.yml5
-rw-r--r--changelogs/unreleased/disable_ilm_on_ELK_yaml.yml5
-rw-r--r--changelogs/unreleased/fix-logrotate-su-parameter.yml5
-rw-r--r--changelogs/unreleased/fj-223701-update-snippets-statistics-after-post-receive.yml5
-rw-r--r--changelogs/unreleased/sy-metrics-embeds-in-alerts.yml5
-rw-r--r--db/migrate/20200615203153_add_requirements_access_level_to_project_features.rb19
-rw-r--r--db/post_migrate/20200526115436_dedup_mr_metrics.rb65
-rw-r--r--db/structure.sql7
-rw-r--r--doc/administration/high_availability/database.md19
-rw-r--r--doc/administration/postgresql/index.md36
-rw-r--r--doc/administration/postgresql/replication_and_failover.md21
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql14
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json36
-rw-r--r--doc/api/graphql/reference/index.md4
-rw-r--r--doc/api/issues.md19
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/development/testing_guide/review_apps.md13
-rw-r--r--doc/user/project/code_intelligence.md8
-rw-r--r--doc/user/project/settings/index.md1
-rw-r--r--lib/api/issues.rb24
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb5
-rw-r--r--lib/gitlab/metrics/methods.rb2
-rw-r--r--lib/support/logrotate/gitlab2
-rw-r--r--locale/gitlab.pot79
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/page/project/issue/index.rb20
-rw-r--r--qa/qa/page/project/issue/jira_import.rb25
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb75
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb2
-rw-r--r--scripts/review_apps/base-config.yaml10
-rwxr-xr-xscripts/review_apps/gcp_cleanup.sh2
-rwxr-xr-xscripts/review_apps/review-apps.sh60
-rw-r--r--spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb4
-rw-r--r--spec/features/projects/issues/design_management/user_paginates_designs_spec.rb57
-rw-r--r--spec/features/projects/issues/design_management/user_permissions_upload_spec.rb30
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb81
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_images_spec.rb1
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_spec.rb40
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_spec.rb80
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb2
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js18
-rw-r--r--spec/frontend/alert_management/components/alert_sidebar_spec.js24
-rw-r--r--spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js4
-rw-r--r--spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap1
-rw-r--r--spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap20
-rw-r--r--spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap8
-rw-r--r--spec/frontend/design_management_new/pages/design/index_spec.js7
-rw-r--r--spec/frontend/design_management_new/pages/index_spec.js44
-rw-r--r--spec/frontend/design_management_new/router_spec.js16
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js164
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js65
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mock_data.js34
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js92
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js56
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js71
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js4
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/quality/helm3_client_spec.rb2
-rw-r--r--spec/lib/quality/kubernetes_client_spec.rb2
-rw-r--r--spec/migrations/20200526115436_dedup_mr_metrics_spec.rb68
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb14
-rw-r--r--spec/models/merge_request_spec.rb15
-rw-r--r--spec/policies/project_policy_spec.rb1
-rw-r--r--spec/presenters/alert_management/alert_presenter_spec.rb6
-rw-r--r--spec/presenters/alert_management/prometheus_alert_presenter_spec.rb6
-rw-r--r--spec/presenters/projects/prometheus/alert_presenter_spec.rb144
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb25
-rw-r--r--spec/requests/api/issues/issues_spec.rb49
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/snippets/destroy_service_spec.rb13
-rw-r--r--spec/services/snippets/update_statistics_service_spec.rb81
-rw-r--r--spec/workers/post_receive_spec.rb7
-rw-r--r--vendor/elastic_stack/values.yaml4
128 files changed, 2352 insertions, 967 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 36d229d64f5..d432afd7dad 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -16,25 +16,24 @@ review-cleanup:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
- gcp_cleanup
-# Temporarily disabling review apps
-#review-build-cng:
-# extends:
-# - .default-retry
-# - .review:rules:review-build-cng
-# image: ruby:2.6-alpine
-# stage: review-prepare
-# before_script:
-# - source scripts/utils.sh
-# - install_api_client_dependencies_with_apk
-# - install_gitlab_gem
-# needs:
-# - job: compile-production-assets
-# artifacts: false
-# script:
-# - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
-# # When the job is manual, review-deploy is also manual and we don't want people
-# # to have to manually start the jobs in sequence, so we do it for them.
-# - '[ -z $CI_JOB_MANUAL ] || play_job "review-deploy"'
+review-build-cng:
+ extends:
+ - .default-retry
+ - .review:rules:review-build-cng
+ image: ruby:2.6-alpine
+ stage: review-prepare
+ before_script:
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - install_gitlab_gem
+ needs:
+ - job: compile-production-assets
+ artifacts: false
+ script:
+ - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
+ # When the job is manual, review-deploy is also manual and we don't want people
+ # to have to manually start the jobs in sequence, so we do it for them.
+ - '[ -z $CI_JOB_MANUAL ] || play_job "review-deploy"'
.review-workflow-base:
extends:
@@ -42,6 +41,7 @@ review-cleanup:
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
+ REVIEW_APPS_DOMAIN: "temp.gitlab-review.app" # FIXME: using temporary domain
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
environment:
@@ -50,37 +50,37 @@ review-cleanup:
on_stop: review-stop
auto_stop_in: 48 hours
-# Temporarily disabling review apps
-#review-deploy:
-# extends:
-# - .review-workflow-base
-# - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
-# stage: review
-# dependencies: []
-# resource_group: "review/${CI_COMMIT_REF_NAME}"
-# before_script:
-# - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
-# - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
-# - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
-# - echo "${CI_ENVIRONMENT_URL}" > environment_url.txt
-# - source ./scripts/utils.sh
-# - install_api_client_dependencies_with_apk
-# - source scripts/review_apps/review-apps.sh
-# script:
-# - check_kube_domain
-# - ensure_namespace
-# - install_external_dns
-# - download_chart
-# - date
-# - deploy || (display_deployment_debug && exit 1)
-# # When the job is manual, review-qa-smoke is also manual and we don't want people
-# # to have to manually start the jobs in sequence, so we do it for them.
-# - '[ -z $CI_JOB_MANUAL ] || play_job "review-qa-smoke"'
-# - '[ -z $CI_JOB_MANUAL ] || play_job "review-performance"'
-# artifacts:
-# paths: [environment_url.txt]
-# expire_in: 2 days
-# when: always
+review-deploy:
+ extends:
+ - .review-workflow-base
+ - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
+ stage: review
+ dependencies: []
+ resource_group: "review/${CI_COMMIT_REF_NAME}"
+ before_script:
+ - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
+ - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
+ - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
+ - echo "${CI_ENVIRONMENT_URL}" > environment_url.txt
+ - source ./scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - source scripts/review_apps/review-apps.sh
+ script:
+ - check_kube_domain
+ - ensure_namespace
+ - install_external_dns
+ - download_chart
+ - date
+ - deploy || (display_deployment_debug && exit 1)
+ - disable_sign_ups
+ # When the job is manual, review-qa-smoke is also manual and we don't want people
+ # to have to manually start the jobs in sequence, so we do it for them.
+ - '[ -z $CI_JOB_MANUAL ] || play_job "review-qa-smoke"'
+ - '[ -z $CI_JOB_MANUAL ] || play_job "review-performance"'
+ artifacts:
+ paths: [environment_url.txt]
+ expire_in: 2 days
+ when: always
.review-stop-base:
extends: .review-workflow-base
@@ -113,110 +113,110 @@ review-stop:
script:
- delete_release
-# Temporarily disabling review apps
-#.review-qa-base:
-# extends:
-# - .default-retry
-# - .use-docker-in-docker
-# image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
-# stage: qa
-# # This is needed so that manual jobs with needs don't block the pipeline.
-# # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
-# dependencies: ["review-deploy"]
-# variables:
-# QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
-# QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
-# QA_DEBUG: "true"
-# GITLAB_USERNAME: "root"
-# GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
-# GITLAB_ADMIN_USERNAME: "root"
-# GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
-# GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
-# EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
-# before_script:
-# - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
-# - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
-# - echo "${CI_ENVIRONMENT_URL}"
-# - echo "${QA_IMAGE}"
-# - source scripts/utils.sh
-# - install_api_client_dependencies_with_apk
-# - gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
-# artifacts:
-# paths:
-# - ./qa/gitlab-qa-run-*
-# expire_in: 7 days
-# when: always
-#
-#review-qa-smoke:
-# extends:
-# - .review-qa-base
-# - .review:rules:review-qa-smoke
-# script:
-# - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
-#
-#review-qa-all:
-# extends:
-# - .review-qa-base
-# - .review:rules:mr-only-manual
-# parallel: 5
-# script:
-# - export KNAPSACK_REPORT_PATH=knapsack/master_report.json
-# - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
-# - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
-#
-#review-performance:
-# extends:
-# - .default-retry
-# - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
-# image:
-# name: sitespeedio/sitespeed.io:6.3.1
-# entrypoint: [""]
-# stage: qa
-# # This is needed so that manual jobs with needs don't block the pipeline.
-# # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
-# dependencies: ["review-deploy"]
-# before_script:
-# - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
-# - echo "${CI_ENVIRONMENT_URL}"
-# - mkdir -p gitlab-exporter
-# - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
-# - mkdir -p sitespeed-results
-# script:
-# - /start.sh --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
-# after_script:
-# - mv sitespeed-results/data/performance.json performance.json
-# artifacts:
-# paths:
-# - sitespeed-results/
-# reports:
-# performance: performance.json
-# expire_in: 31d
-#
-#parallel-spec-reports:
-# extends:
-# - .review:rules:mr-only-manual
-# image: ruby:2.6-alpine
-# stage: post-qa
-# dependencies: ["review-qa-all"]
-# variables:
-# NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
-# BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
-# script:
-# - apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
-# - gem install nokogiri --no-document
-# - cd qa/gitlab-qa-run-*/gitlab-*
-# - ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
-# - cd -
-# - '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
-# - scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
-# artifacts:
-# when: always
-# paths:
-# - qa/report-new.html
-# - qa/gitlab-qa-run-*
-# reports:
-# junit: qa/gitlab-qa-run-*/**/rspec-*.xml
-# expire_in: 31d
+.review-qa-base:
+ extends:
+ - .default-retry
+ - .use-docker-in-docker
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
+ stage: qa
+ # This is needed so that manual jobs with needs don't block the pipeline.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
+ dependencies: ["review-deploy"]
+ variables:
+ QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
+ QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
+ QA_DEBUG: "true"
+ GITLAB_USERNAME: "root"
+ GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
+ GITLAB_ADMIN_USERNAME: "root"
+ GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
+ GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
+ EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
+ SIGNUP_DISABLED: "true"
+ before_script:
+ - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
+ - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
+ - echo "${CI_ENVIRONMENT_URL}"
+ - echo "${QA_IMAGE}"
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
+ artifacts:
+ paths:
+ - ./qa/gitlab-qa-run-*
+ expire_in: 7 days
+ when: always
+
+review-qa-smoke:
+ extends:
+ - .review-qa-base
+ - .review:rules:review-qa-smoke
+ script:
+ - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
+
+review-qa-all:
+ extends:
+ - .review-qa-base
+ - .review:rules:mr-only-manual
+ parallel: 5
+ script:
+ - export KNAPSACK_REPORT_PATH=knapsack/master_report.json
+ - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
+ - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
+
+review-performance:
+ extends:
+ - .default-retry
+ - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
+ image:
+ name: sitespeedio/sitespeed.io:6.3.1
+ entrypoint: [""]
+ stage: qa
+ # This is needed so that manual jobs with needs don't block the pipeline.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
+ dependencies: ["review-deploy"]
+ before_script:
+ - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
+ - echo "${CI_ENVIRONMENT_URL}"
+ - mkdir -p gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir -p sitespeed-results
+ script:
+ - /start.sh --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
+ after_script:
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ performance: performance.json
+ expire_in: 31d
+
+parallel-spec-reports:
+ extends:
+ - .review:rules:mr-only-manual
+ image: ruby:2.6-alpine
+ stage: post-qa
+ dependencies: ["review-qa-all"]
+ variables:
+ NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
+ BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
+ script:
+ - apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
+ - gem install nokogiri --no-document
+ - cd qa/gitlab-qa-run-*/gitlab-*
+ - ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
+ - cd -
+ - '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
+ - scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
+ artifacts:
+ when: always
+ paths:
+ - qa/report-new.html
+ - qa/gitlab-qa-run-*
+ reports:
+ junit: qa/gitlab-qa-run-*/**/rspec-*.xml
+ expire_in: 31d
danger-review:
extends:
diff --git a/Gemfile b/Gemfile
index 099cf5477dc..c86874201fc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -66,7 +66,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.10'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
-gem 'acme-client', '~> 2.0.5'
+gem 'acme-client', '~> 2.0', '>= 2.0.6'
# Browser detection
gem 'browser', '~> 2.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index ae35fa30e6e..ca0884db4a5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,8 +4,8 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
- acme-client (2.0.5)
- faraday (~> 0.9, >= 0.9.1)
+ acme-client (2.0.6)
+ faraday (>= 0.17, < 2.0.0)
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
nio4r (~> 2.0)
@@ -1169,7 +1169,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
- acme-client (~> 2.0.5)
+ acme-client (~> 2.0, >= 2.0.6)
activerecord-explain-analyze (~> 0.1)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.7)
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 3da338bf13f..d3dca22e1e1 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -12,13 +12,15 @@ import {
GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
-import query from '../graphql/queries/details.query.graphql';
+import alertQuery from '../graphql/queries/details.query.graphql';
+import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import initUserPopovers from '~/user_popovers';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
-import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
+import createIssueMutation from '../graphql/mutations/create_issue_from_alert.graphql';
+import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
@@ -52,28 +54,27 @@ export default {
AlertSidebar,
SystemNote,
},
- props: {
+ inject: {
+ projectPath: {
+ default: '',
+ },
alertId: {
type: String,
- required: true,
+ default: '',
},
projectId: {
type: String,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
+ default: '',
},
projectIssuesPath: {
type: String,
- required: true,
+ default: '',
},
},
apollo: {
alert: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query,
+ query: alertQuery,
variables() {
return {
fullPath: this.projectPath,
@@ -88,15 +89,18 @@ export default {
Sentry.captureException(error);
},
},
+ sidebarStatus: {
+ query: sidebarStatusQuery,
+ },
},
data() {
return {
alert: null,
errored: false,
+ sidebarStatus: false,
isErrorDismissed: false,
createIssueError: '',
issueCreationInProgress: false,
- sidebarCollapsed: false,
sidebarErrorMessage: '',
};
},
@@ -132,10 +136,10 @@ export default {
this.sidebarErrorMessage = '';
},
toggleSidebar() {
- this.sidebarCollapsed = !this.sidebarCollapsed;
+ this.$apollo.mutate({ mutation: toggleSidebarStatusMutation });
toggleContainerClasses(containerEl, {
- 'right-sidebar-collapsed': this.sidebarCollapsed,
- 'right-sidebar-expanded': !this.sidebarCollapsed,
+ 'right-sidebar-collapsed': !this.sidebarStatus,
+ 'right-sidebar-expanded': this.sidebarStatus,
});
},
handleAlertSidebarError(errorMessage) {
@@ -147,7 +151,7 @@ export default {
this.$apollo
.mutate({
- mutation: createIssueQuery,
+ mutation: createIssueMutation,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
@@ -197,7 +201,7 @@ export default {
<div
v-if="alert"
class="alert-management-details gl-relative"
- :class="{ 'pr-sm-8': sidebarCollapsed }"
+ :class="{ 'pr-sm-8': sidebarStatus }"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
@@ -330,10 +334,7 @@ export default {
</gl-tab>
</gl-tabs>
<alert-sidebar
- :project-path="projectPath"
- :project-id="projectId"
:alert="alert"
- :sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar"
@alert-error="handleAlertSidebarError"
diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue
index fa9e60e465a..c5112f2cd02 100644
--- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue
+++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue
@@ -4,6 +4,8 @@ import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
+import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
+
export default {
components: {
SidebarAssignees,
@@ -11,27 +13,34 @@ export default {
SidebarTodo,
SidebarStatus,
},
- props: {
- sidebarCollapsed: {
- type: Boolean,
- required: true,
+ inject: {
+ projectPath: {
+ default: '',
},
projectId: {
type: String,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
+ default: '',
},
+ },
+ props: {
alert: {
type: Object,
required: true,
},
},
+ apollo: {
+ sidebarStatus: {
+ query: sidebarStatusQuery,
+ },
+ },
+ data() {
+ return {
+ sidebarStatus: false,
+ };
+ },
computed: {
sidebarCollapsedClass() {
- return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
+ return this.sidebarStatus ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
};
@@ -41,10 +50,10 @@ export default {
<aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar">
<div class="issuable-sidebar js-issuable-update">
<sidebar-header
- :sidebar-collapsed="sidebarCollapsed"
+ :sidebar-collapsed="sidebarStatus"
@toggle-sidebar="$emit('toggle-sidebar')"
/>
- <sidebar-todo v-if="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" />
+ <sidebar-todo v-if="sidebarStatus" :sidebar-collapsed="sidebarStatus" />
<sidebar-status
:project-path="projectPath"
:alert="alert"
@@ -55,7 +64,7 @@ export default {
:project-path="projectPath"
:project-id="projectId"
:alert="alert"
- :sidebar-collapsed="sidebarCollapsed"
+ :sidebar-collapsed="sidebarStatus"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-error="$emit('alert-error', $event)"
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
index cd6069b5c28..2820bcb9665 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/alert_management/details.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import AlertDetails from './components/alert_details.vue';
+import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
Vue.use(VueApollo);
@@ -10,39 +11,51 @@ export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
+ const resolvers = {
+ Mutation: {
+ toggleSidebarStatus: (_, __, { cache }) => {
+ const data = cache.readQuery({ query: sidebarStatusQuery });
+ data.sidebarStatus = !data.sidebarStatus;
+ cache.writeQuery({ query: sidebarStatusQuery, data });
+ },
+ },
+ };
+
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- cacheConfig: {
- dataIdFromObject: object => {
- // eslint-disable-next-line no-underscore-dangle
- if (object.__typename === 'AlertManagementAlert') {
- return object.iid;
- }
- return defaultDataIdFromObject(object);
- },
+ defaultClient: createDefaultClient(resolvers, {
+ cacheConfig: {
+ dataIdFromObject: object => {
+ // eslint-disable-next-line no-underscore-dangle
+ if (object.__typename === 'AlertManagementAlert') {
+ return object.iid;
+ }
+ return defaultDataIdFromObject(object);
},
},
- ),
+ }),
+ });
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ sidebarStatus: false,
+ },
});
// eslint-disable-next-line no-new
new Vue({
el: selector,
+ provide: {
+ projectPath,
+ alertId,
+ projectIssuesPath,
+ projectId,
+ },
apolloProvider,
components: {
AlertDetails,
},
render(createElement) {
- return createElement('alert-details', {
- props: {
- alertId,
- projectPath,
- projectId,
- projectIssuesPath,
- },
- });
+ return createElement('alert-details', {});
},
});
};
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql
index efeaf8fa372..88c374ccf62 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql
@@ -1,4 +1,4 @@
-mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
+mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
alertSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
) {
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql
index 664596ab88f..18c9652b262 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql
@@ -1,4 +1,4 @@
-mutation ($projectPath: ID!, $iid: String!) {
+mutation createAlertIssue($projectPath: ID!, $iid: String!) {
createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
errors
issue {
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
new file mode 100644
index 00000000000..d9c4813f8e8
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
@@ -0,0 +1,3 @@
+mutation toggleSidebarStatus {
+ toggleSidebarStatus @client
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
index 09151f233f5..d07d65bd76c 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
@@ -1,4 +1,4 @@
-mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
+mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
diff --git a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
new file mode 100644
index 00000000000..0836f702189
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
@@ -0,0 +1,3 @@
+query sidebarStatus {
+ sidebarStatus @client
+}
diff --git a/app/assets/javascripts/design_management_new/components/design_destroyer.vue b/app/assets/javascripts/design_management_new/components/design_destroyer.vue
index 62460ca551c..7ae569216f0 100644
--- a/app/assets/javascripts/design_management_new/components/design_destroyer.vue
+++ b/app/assets/javascripts/design_management_new/components/design_destroyer.vue
@@ -13,13 +13,14 @@ export default {
type: Array,
required: true,
},
+ },
+ inject: {
projectPath: {
- type: String,
- required: true,
+ default: '',
},
iid: {
- type: String,
- required: true,
+ from: 'issueIid',
+ defaut: '',
},
},
computed: {
diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue
index b1f3a43a66d..172e61920ef 100644
--- a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue
@@ -60,7 +60,7 @@ export default {
},
mounted() {
if (this.isNoteLinked) {
- this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
+ this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
},
methods: {
@@ -80,7 +80,7 @@ export default {
</script>
<template>
- <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
+ <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
<user-avatar-link
:link-href="author.webUrl"
:img-src="author.avatarUrl"
diff --git a/app/assets/javascripts/design_management_new/components/toolbar/index.vue b/app/assets/javascripts/design_management_new/components/toolbar/index.vue
index b998dfc47b8..0b51035e83e 100644
--- a/app/assets/javascripts/design_management_new/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management_new/components/toolbar/index.vue
@@ -6,7 +6,6 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import Pagination from './pagination.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
@@ -55,19 +54,17 @@ export default {
permissions: {
createDesign: false,
},
- projectPath: '',
- issueIid: null,
};
},
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
+ inject: {
+ projectPath: {
+ default: '',
},
+ issueIid: {
+ default: '',
+ },
+ },
+ apollo: {
permissions: {
query: permissionsQuery,
variables() {
@@ -102,6 +99,7 @@ export default {
query: $route.query,
}"
:aria-label="s__('DesignManagement|Go back to designs')"
+ data-testid="close-design"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
>
<icon :size="18" name="close" />
diff --git a/app/assets/javascripts/design_management_new/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_new/graphql/queries/app_data.query.graphql
deleted file mode 100644
index e1269761206..00000000000
--- a/app/assets/javascripts/design_management_new/graphql/queries/app_data.query.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-query projectFullPath {
- projectPath @client
- issueIid @client
-}
diff --git a/app/assets/javascripts/design_management_new/index.js b/app/assets/javascripts/design_management_new/index.js
index bf9cb9d4776..20c9cacf83f 100644
--- a/app/assets/javascripts/design_management_new/index.js
+++ b/app/assets/javascripts/design_management_new/index.js
@@ -1,29 +1,15 @@
-import $ from 'jquery';
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
import apolloProvider from './graphql';
-import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
-import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
export default () => {
const el = document.querySelector('.js-design-management-new');
- const badge = document.querySelector('.js-designs-count');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
- $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
- if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
- router.push({ name: DESIGNS_ROUTE_NAME });
- } else if (id === 'discussion') {
- router.push({ name: ROOT_ROUTE_NAME });
- }
- });
-
apolloProvider.clients.defaultClient.cache.writeData({
data: {
- projectPath,
- issueIid,
activeDiscussion: {
__typename: 'ActiveDiscussion',
id: null,
@@ -32,25 +18,14 @@ export default () => {
},
});
- apolloProvider.clients.defaultClient
- .watchQuery({
- query: getDesignListQuery,
- variables: {
- fullPath: projectPath,
- iid: issueIid,
- atVersion: null,
- },
- })
- .subscribe(({ data }) => {
- if (badge) {
- badge.textContent = data.project.issue.designCollection.designs.edges.length;
- }
- });
-
return new Vue({
el,
router,
apolloProvider,
+ provide: {
+ projectPath,
+ issueIid,
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/design_management_new/mixins/all_versions.js b/app/assets/javascripts/design_management_new/mixins/all_versions.js
index 3966fe71732..99e2ee9561c 100644
--- a/app/assets/javascripts/design_management_new/mixins/all_versions.js
+++ b/app/assets/javascripts/design_management_new/mixins/all_versions.js
@@ -1,17 +1,8 @@
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
allVersions: {
query: getDesignListQuery,
variables() {
@@ -24,6 +15,14 @@ export default {
update: data => data.project.issue.designCollection.versions.edges,
},
},
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
computed: {
hasValidVersion() {
return (
@@ -55,8 +54,6 @@ export default {
data() {
return {
allVersions: [],
- projectPath: '',
- issueIid: null,
};
},
};
diff --git a/app/assets/javascripts/design_management_new/pages/design/index.vue b/app/assets/javascripts/design_management_new/pages/design/index.vue
index 9a959222e22..47f5e3a786f 100644
--- a/app/assets/javascripts/design_management_new/pages/design/index.vue
+++ b/app/assets/javascripts/design_management_new/pages/design/index.vue
@@ -12,7 +12,6 @@ import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
@@ -62,22 +61,12 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
- projectPath: '',
errorMessage: '',
- issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
diff --git a/app/assets/javascripts/design_management_new/pages/index.vue b/app/assets/javascripts/design_management_new/pages/index.vue
index d14a1fc8c1c..5fda729098c 100644
--- a/app/assets/javascripts/design_management_new/pages/index.vue
+++ b/app/assets/javascripts/design_management_new/pages/index.vue
@@ -259,7 +259,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="designs-root">
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
<div class="d-flex justify-content-between align-items-center w-100">
<design-version-dropdown />
@@ -274,8 +274,6 @@ export default {
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
- :project-path="projectPath"
- :iid="issueIid"
@done="onDesignDelete"
@error="onDesignDeleteError"
>
diff --git a/app/assets/javascripts/design_management_new/router/constants.js b/app/assets/javascripts/design_management_new/router/constants.js
index abeef520e33..dd2ee8d8689 100644
--- a/app/assets/javascripts/design_management_new/router/constants.js
+++ b/app/assets/javascripts/design_management_new/router/constants.js
@@ -1,3 +1,2 @@
-export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management_new/router/index.js b/app/assets/javascripts/design_management_new/router/index.js
index 23537609a40..40e2d35bc40 100644
--- a/app/assets/javascripts/design_management_new/router/index.js
+++ b/app/assets/javascripts/design_management_new/router/index.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
@@ -16,9 +15,7 @@ export default function createRouter(base) {
});
const pageEl = getPageLayoutElement();
- router.beforeEach(({ meta: { el }, name }, _, next) => {
- $(`#${el}`).tab('show');
-
+ router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
diff --git a/app/assets/javascripts/design_management_new/router/routes.js b/app/assets/javascripts/design_management_new/router/routes.js
index 788910e5514..2a25a2bcadc 100644
--- a/app/assets/javascripts/design_management_new/router/routes.js
+++ b/app/assets/javascripts/design_management_new/router/routes.js
@@ -1,44 +1,28 @@
import Home from '../pages/index.vue';
import DesignDetail from '../pages/design/index.vue';
-import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
export default [
{
- name: ROOT_ROUTE_NAME,
+ name: DESIGNS_ROUTE_NAME,
path: '/',
component: Home,
- meta: {
- el: 'discussion',
- },
},
{
- name: DESIGNS_ROUTE_NAME,
- path: '/designs',
- component: Home,
- meta: {
- el: 'designs',
- },
- children: [
+ name: DESIGN_ROUTE_NAME,
+ path: '/designs/:id',
+ component: DesignDetail,
+ beforeEnter(
{
- name: DESIGN_ROUTE_NAME,
- path: ':id',
- component: DesignDetail,
- meta: {
- el: 'designs',
- },
- beforeEnter(
- {
- params: { id },
- },
- from,
- next,
- ) {
- if (typeof id === 'string') {
- next();
- }
- },
- props: ({ params: { id } }) => ({ id }),
+ params: { id },
},
- ],
+ from,
+ next,
+ ) {
+ if (typeof id === 'string') {
+ next();
+ }
+ },
+ props: ({ params: { id } }) => ({ id }),
},
];
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index c2fe7b29c28..d9ee655ea08 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -70,6 +70,7 @@ export default {
>
<gl-form-select
id="jira-project-select"
+ data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
@@ -135,7 +136,13 @@ export default {
</gl-form-group>
<div class="footer-block row-content-block d-flex justify-content-between">
- <gl-button type="submit" category="primary" variant="success" class="js-no-auto-disable">
+ <gl-button
+ type="submit"
+ category="primary"
+ variant="success"
+ class="js-no-auto-disable"
+ data-qa-selector="jira_issues_import_button"
+ >
{{ __('Next') }}
</gl-button>
<gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 860a2e34228..a95f0af46cd 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -67,11 +67,6 @@ export default {
required: false,
default: false,
},
- requirementsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
visibilityHelpPath: {
type: String,
required: false,
@@ -136,7 +131,6 @@ export default {
snippetsAccessLevel: featureAccessLevel.EVERYONE,
pagesAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
- requirementsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
@@ -239,10 +233,6 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.metricsDashboardAccessLevel,
);
- this.requirementsAccessLevel = Math.min(
- featureAccessLevel.PROJECT_MEMBERS,
- this.requirementsAccessLevel,
- );
if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS;
@@ -266,9 +256,6 @@ export default {
this.pagesAccessLevel = featureAccessLevel.EVERYONE;
if (this.metricsDashboardAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE;
- if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
- this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
-
this.highlightChanges();
}
},
@@ -484,18 +471,6 @@ export default {
</project-setting-row>
</div>
<project-setting-row
- v-if="requirementsAvailable"
- ref="requirements-settings"
- :label="s__('ProjectSettings|Requirements')"
- :help-text="s__('ProjectSettings|Requirements management system for this project')"
- >
- <project-feature-setting
- v-model="requirementsAccessLevel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][requirements_access_level]"
- />
- </project-setting-row>
- <project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
:help-text="s__('ProjectSettings|Pages for project documentation')"
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
index 3f5e49fce98..fcbd81416f2 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
@@ -2,7 +2,6 @@ export default {
data() {
return {
packagesEnabled: false,
- requirementsEnabled: false,
};
},
watch: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
new file mode 100644
index 00000000000..fd9a370fe05
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
@@ -0,0 +1,72 @@
+<script>
+import { __ } from '~/locale';
+import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+
+/**
+ * Renders header section with icon and expand button
+ * Renders expanable content section with grey background
+ */
+export default {
+ name: 'MrWidgetExpanableSection',
+ components: {
+ GlButton,
+ GlCollapse,
+ GlIcon,
+ },
+ props: {
+ iconName: {
+ type: String,
+ required: false,
+ default: 'status_warning',
+ },
+ },
+ data() {
+ return {
+ contentIsVisible: false,
+ };
+ },
+ computed: {
+ collapseButtonText() {
+ if (this.contentIsVisible) {
+ return __('Collapse');
+ }
+
+ return __('Expand');
+ },
+ },
+ methods: {
+ updateContentVisibility() {
+ this.contentIsVisible = !this.contentIsVisible;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="mr-widget-body gl-display-flex">
+ <span
+ class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
+ >
+ <gl-icon :name="iconName" :size="24" />
+ </span>
+
+ <div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column gl-md-flex-direction-row">
+ <slot name="header"></slot>
+
+ <div>
+ <gl-button @click="updateContentVisibility">
+ {{ collapseButtonText }}
+ </gl-button>
+ </div>
+ </div>
+ </div>
+
+ <gl-collapse
+ :visible="contentIsVisible"
+ class="gl-bg-gray-10 gl-border-t-solid gl-border-gray-100 gl-border-1"
+ >
+ <slot name="content"></slot>
+ </gl-collapse>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index 05451d089f6..f6e21dc1ec1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,6 +1,8 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
+import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import Poll from '~/lib/utils/poll';
import TerraformPlan from './terraform_plan.vue';
@@ -8,6 +10,8 @@ export default {
name: 'MRWidgetTerraformContainer',
components: {
GlSkeletonLoading,
+ GlSprintf,
+ MrWidgetExpanableSection,
TerraformPlan,
},
props: {
@@ -19,10 +23,43 @@ export default {
data() {
return {
loading: true,
- plans: {},
+ plansObject: {},
poll: null,
};
},
+ computed: {
+ inValidPlanCountText() {
+ if (this.numberOfInvalidPlans === 0) {
+ return null;
+ }
+
+ return n__(
+ 'Terraform|%{number} Terraform report failed to generate',
+ 'Terraform|%{number} Terraform reports failed to generate',
+ this.numberOfInvalidPlans,
+ );
+ },
+ numberOfInvalidPlans() {
+ return Object.values(this.plansObject).filter(plan => plan.tf_report_error).length;
+ },
+ numberOfPlans() {
+ return Object.keys(this.plansObject).length;
+ },
+ numberOfValidPlans() {
+ return this.numberOfPlans - this.numberOfInvalidPlans;
+ },
+ validPlanCountText() {
+ if (this.numberOfValidPlans === 0) {
+ return null;
+ }
+
+ return n__(
+ 'Terraform|%{number} Terraform report was generated in your pipelines',
+ 'Terraform|%{number} Terraform reports were generated in your pipelines',
+ this.numberOfValidPlans,
+ );
+ },
+ },
created() {
this.fetchPlans();
},
@@ -40,15 +77,15 @@ export default {
data: this.endpoint,
method: 'fetchPlans',
successCallback: ({ data }) => {
- this.plans = data;
+ this.plansObject = data;
- if (Object.keys(this.plans).length) {
+ if (this.numberOfPlans > 0) {
this.loading = false;
this.poll.stop();
}
},
errorCallback: () => {
- this.plans = { bad_plan: {} };
+ this.plansObject = { bad_plan: { tf_report_error: 'api_error' } };
this.loading = false;
this.poll.stop();
},
@@ -62,16 +99,42 @@ export default {
<template>
<section class="mr-widget-section">
- <div v-if="loading" class="mr-widget-body media">
+ <div v-if="loading" class="mr-widget-body">
<gl-skeleton-loading />
</div>
- <terraform-plan
- v-for="(plan, key) in plans"
- v-else
- :key="key"
- :plan="plan"
- class="mr-widget-body media"
- />
+ <mr-widget-expanable-section v-else>
+ <template #header>
+ <div
+ data-testid="terraform-header-text"
+ class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column"
+ >
+ <p v-if="validPlanCountText" class="gl-m-0">
+ <gl-sprintf :message="validPlanCountText">
+ <template #number>
+ <strong>{{ numberOfValidPlans }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p v-if="inValidPlanCountText" class="gl-m-0">
+ <gl-sprintf :message="inValidPlanCountText">
+ <template #number>
+ <strong>{{ numberOfInvalidPlans }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </template>
+
+ <template #content>
+ <terraform-plan
+ v-for="(plan, key) in plansObject"
+ :key="key"
+ :plan="plan"
+ class="mr-widget-body"
+ />
+ </template>
+ </mr-widget-expanable-section>
</section>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
index 28c54ca26fb..81e6b234a24 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
@@ -25,21 +25,28 @@ export default {
deleteNum() {
return Number(this.plan.delete);
},
+ iconType() {
+ return this.validPlanValues ? 'doc-changes' : 'warning';
+ },
reportChangeText() {
if (this.validPlanValues) {
return __(
- 'Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
+ 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
);
}
- return __('Generating the report caused an error.');
+ return __('Terraform|Generating the report caused an error.');
},
reportHeaderText() {
- if (this.plan.job_name) {
- return __('The Terraform report %{name} was generated in your pipelines.');
+ if (this.validPlanValues) {
+ return this.plan.job_name
+ ? __('Terraform|The Terraform report %{name} was generated in your pipelines.')
+ : __('Terraform|A Terraform report was generated in your pipelines.');
}
- return __('A Terraform report was generated in your pipelines.');
+ return this.plan.job_name
+ ? __('Terraform|The Terraform report %{name} failed to generate.')
+ : __('Terraform|A Terraform report failed to generate.');
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
@@ -53,11 +60,11 @@ export default {
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
>
- <gl-icon name="status_warning" :size="24" />
+ <gl-icon :name="iconType" :size="18" data-testid="change-type-icon" />
</span>
<div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row">
- <div class="terraform-mr-plan-text normal gl-display-flex gl-flex-direction-column">
+ <div class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column">
<p class="gl-m-0 gl-pr-1">
<gl-sprintf :message="reportHeaderText">
<template #name>
@@ -88,10 +95,11 @@ export default {
v-if="plan.job_path"
:href="plan.job_path"
target="_blank"
+ data-testid="terraform-report-link"
data-track-event="click_terraform_mr_plan_button"
data-track-label="mr_widget_terraform_mr_plan_button"
data-track-property="terraform_mr_plan_button"
- class="btn btn-sm js-terraform-report-link"
+ class="btn btn-sm"
rel="noopener"
>
{{ __('View full log') }}
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
index 1eb24c1d98f..dd1da847001 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
@@ -43,3 +43,7 @@ export const EDITOR_TYPES = {
export const EDITOR_HEIGHT = '100%';
export const EDITOR_PREVIEW_STYLE = 'horizontal';
+
+export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 };
+
+export const MAX_FILE_SIZE = 2097152; // 2Mb
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
new file mode 100644
index 00000000000..dce5d1778b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
@@ -0,0 +1,137 @@
+<script>
+import { isSafeURL } from '~/lib/utils/url_utility';
+import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
+import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { IMAGE_TABS } from '../../constants';
+import UploadImageTab from './upload_image_tab.vue';
+
+export default {
+ components: {
+ UploadImageTab,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlTabs,
+ GlTab,
+ },
+ mixins: [glFeatureFlagMixin()],
+ data() {
+ return {
+ urlError: null,
+ imageUrl: null,
+ description: null,
+ tabIndex: IMAGE_TABS.UPLOAD_TAB,
+ uploadImageTab: null,
+ };
+ },
+ modalTitle: __('Image Details'),
+ okTitle: __('Insert'),
+ urlTabTitle: __('By URL'),
+ urlLabel: __('Image URL'),
+ descriptionLabel: __('Description'),
+ uploadTabTitle: __('Upload file'),
+ computed: {
+ altText() {
+ return this.description;
+ },
+ },
+ methods: {
+ show() {
+ this.urlError = null;
+ this.imageUrl = null;
+ this.description = null;
+ this.tabIndex = IMAGE_TABS.UPLOAD_TAB;
+
+ this.$refs.modal.show();
+ },
+ onOk(event) {
+ if (this.glFeatures.sseImageUploads && this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
+ this.submitFile(event);
+ return;
+ }
+ this.submitURL(event);
+ },
+ setFile(file) {
+ this.file = file;
+ },
+ submitFile(event) {
+ const { file, altText } = this;
+ const { uploadImageTab } = this.$refs;
+
+ uploadImageTab.validateFile();
+
+ if (uploadImageTab.fileError) {
+ event.preventDefault();
+ return;
+ }
+
+ this.$emit('addImage', { file, altText: altText || file.name });
+ },
+ submitURL(event) {
+ if (!this.validateUrl()) {
+ event.preventDefault();
+ return;
+ }
+
+ const { imageUrl, altText } = this;
+
+ this.$emit('addImage', { imageUrl, altText: altText || imageUrl });
+ },
+ validateUrl() {
+ if (!isSafeURL(this.imageUrl)) {
+ this.urlError = __('Please provide a valid URL');
+ this.$refs.urlInput.$el.focus();
+ return false;
+ }
+
+ return true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ ref="modal"
+ modal-id="add-image-modal"
+ :title="$options.modalTitle"
+ :ok-title="$options.okTitle"
+ @ok="onOk"
+ >
+ <gl-tabs v-if="glFeatures.sseImageUploads" v-model="tabIndex">
+ <!-- Upload file Tab -->
+ <gl-tab :title="$options.uploadTabTitle">
+ <upload-image-tab ref="uploadImageTab" @input="setFile" />
+ </gl-tab>
+
+ <!-- By URL Tab -->
+ <gl-tab :title="$options.urlTabTitle">
+ <gl-form-group
+ class="gl-mt-5 gl-mb-3"
+ :label="$options.urlLabel"
+ label-for="url-input"
+ :state="!Boolean(urlError)"
+ :invalid-feedback="urlError"
+ >
+ <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
+ </gl-form-group>
+ </gl-tab>
+ </gl-tabs>
+
+ <gl-form-group
+ v-else
+ class="gl-mt-5 gl-mb-3"
+ :label="$options.urlLabel"
+ label-for="url-input"
+ :state="!Boolean(urlError)"
+ :invalid-feedback="urlError"
+ >
+ <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
+ </gl-form-group>
+
+ <!-- Description Input -->
+ <gl-form-group :label="$options.descriptionLabel" label-for="description-input">
+ <gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
+ </gl-form-group>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
new file mode 100644
index 00000000000..739f8b502c9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
@@ -0,0 +1,56 @@
+<script>
+import { __ } from '~/locale';
+import { GlFormGroup } from '@gitlab/ui';
+import { MAX_FILE_SIZE } from '../../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ },
+ data() {
+ return {
+ file: null,
+ fileError: null,
+ };
+ },
+ fileLabel: __('Select file'),
+ methods: {
+ onInput(event) {
+ [this.file] = event.target.files;
+
+ this.validateFile();
+
+ if (!this.fileError) {
+ this.$emit('input', this.file);
+ }
+ },
+ validateFile() {
+ this.fileError = null;
+
+ if (!this.file) {
+ this.fileError = __('Please choose a file');
+ } else if (this.file.size > MAX_FILE_SIZE) {
+ this.fileError = __('Maximum file size is 2MB. Please select a smaller file.');
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group
+ class="gl-mt-5 gl-mb-3"
+ :label="$options.fileLabel"
+ label-for="file-input"
+ :state="!Boolean(fileError)"
+ :invalid-feedback="fileError"
+ >
+ <input
+ id="file-input"
+ ref="fileInput"
+ class="gl-mt-3 gl-mb-2"
+ type="file"
+ accept="image/*"
+ @input="onInput"
+ />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
deleted file mode 100644
index 40063065926..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import { isSafeURL } from '~/lib/utils/url_utility';
-import { GlModal, GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlModal,
- GlFormGroup,
- GlFormInput,
- },
- data() {
- return {
- error: null,
- imageUrl: null,
- altText: null,
- modalTitle: __('Image Details'),
- okTitle: __('Insert'),
- urlLabel: __('Image URL'),
- descriptionLabel: __('Description'),
- };
- },
- methods: {
- show() {
- this.error = null;
- this.imageUrl = null;
- this.altText = null;
-
- this.$refs.modal.show();
- },
- onOk(event) {
- if (!this.isValid()) {
- event.preventDefault();
- return;
- }
-
- const { imageUrl, altText } = this;
-
- this.$emit('addImage', { imageUrl, altText: altText || __('image') });
- },
- isValid() {
- if (!isSafeURL(this.imageUrl)) {
- this.error = __('Please provide a valid URL');
- this.$refs.urlInput.$el.focus();
- return false;
- }
-
- return true;
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ref="modal"
- modal-id="add-image-modal"
- :title="modalTitle"
- :ok-title="okTitle"
- @ok="onOk"
- >
- <gl-form-group
- :label="urlLabel"
- label-for="url-input"
- :state="!Boolean(error)"
- :invalid-feedback="error"
- >
- <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
- </gl-form-group>
-
- <gl-form-group :label="descriptionLabel" label-for="description-input">
- <gl-form-input id="description-input" ref="descriptionInput" v-model="altText" />
- </gl-form-group>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index aaa13985b09..1d4d3e28a77 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -2,7 +2,7 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
-import AddImageModal from './modals/add_image_modal.vue';
+import AddImageModal from './modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
@@ -18,6 +18,8 @@ import {
getMarkdown,
} from './services/editor_service';
+import { getUrl } from './services/image_service';
+
export default {
components: {
ToastEditor: () =>
@@ -96,7 +98,16 @@ export default {
onOpenAddImageModal() {
this.$refs.addImageModal.show();
},
- onAddImage(image) {
+ onAddImage({ imageUrl, altText, file }) {
+ const image = { imageUrl, altText };
+
+ if (file) {
+ image.imageUrl = getUrl(file);
+ // TODO - persist images locally (local image repository)
+ // TODO - ensure that the actual repo URL for the image is used in Markdown mode
+ // TODO - upload images to the project repository (on submit)
+ }
+
addImage(this.editorInstance, image);
},
onChangeMode(newMode) {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js
new file mode 100644
index 00000000000..a66e464e702
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const getUrl = file => URL.createObjectURL(file);
diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
index 74f28c3da67..9ec50ff8196 100644
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ b/app/controllers/projects/static_site_editor_controller.rb
@@ -9,6 +9,9 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:show]
before_action :assign_ref_and_path, only: [:show]
before_action :authorize_edit_tree!, only: [:show]
+ before_action do
+ push_frontend_feature_flag(:sse_image_uploads)
+ end
def show
@config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url])
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ce7f2ef8287..36ca88da1c7 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -356,20 +356,6 @@ class ProjectsController < Projects::ApplicationController
.merge(import_url_params)
end
- def project_feature_attributes
- %i[
- builds_access_level
- issues_access_level
- forking_access_level
- merge_requests_access_level
- repository_access_level
- snippets_access_level
- wiki_access_level
- pages_access_level
- metrics_dashboard_access_level
- ]
- end
-
def project_params_attributes
[
:allow_merge_on_skipped_pipeline,
@@ -405,10 +391,22 @@ class ProjectsController < Projects::ApplicationController
:initialize_with_readme,
:autoclose_referenced_issues,
:suggestion_commit_message,
+
+ project_feature_attributes: %i[
+ builds_access_level
+ issues_access_level
+ forking_access_level
+ merge_requests_access_level
+ repository_access_level
+ snippets_access_level
+ wiki_access_level
+ pages_access_level
+ metrics_dashboard_access_level
+ ],
project_setting_attributes: %i[
show_default_award_emojis
]
- ] + [project_feature_attributes: project_feature_attributes]
+ ]
end
def project_params_create_attributes
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 8215ccb152c..089d2426158 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -91,6 +91,12 @@ module Types
null: true,
description: 'Assignees of the alert'
+ field :metrics_dashboard_url,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'URL for metrics embed for the alert',
+ resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url }
+
def notes
object.ordered_notes
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 477500248fd..73199817ce5 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -40,7 +40,8 @@ module Ci
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json',
- requirements: 'requirements.json'
+ requirements: 'requirements.json',
+ coverage_fuzzing: 'gl-coverage-fuzzing.json'
}.freeze
INTERNAL_TYPES = {
@@ -73,7 +74,8 @@ module Ci
license_scanning: :raw,
performance: :raw,
terraform: :raw,
- requirements: :raw
+ requirements: :raw,
+ coverage_fuzzing: :raw
}.freeze
DOWNLOADABLE_TYPES = %w[
@@ -187,7 +189,8 @@ module Ci
accessibility: 19,
cluster_applications: 20,
secret_detection: 21, ## EE-specific
- requirements: 22 ## EE-specific
+ requirements: 22, ## EE-specific
+ coverage_fuzzing: 23 ## EE-specific
}
enum file_format: {
diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
index 20b72957ec2..60aa46ce04c 100644
--- a/app/models/concerns/featurable.rb
+++ b/app/models/concerns/featurable.rb
@@ -37,8 +37,7 @@ module Featurable
class_methods do
def set_available_features(available_features = [])
- @available_features ||= []
- @available_features += available_features
+ @available_features = available_features
class_eval do
available_features.each do |feature|
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 756e9532a51..cedcf164a49 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -88,5 +88,3 @@ module ProjectFeaturesCompatibility
project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend
end
end
-
-ProjectsHelper.prepend_if_ee('EE::ProjectFeaturesCompatibility')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 699f82c7e6b..ddd0dbbc8dc 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -21,6 +21,8 @@ class MergeRequest < ApplicationRecord
include MilestoneEventable
include StateEventable
+ extend ::Gitlab::Utils::Override
+
sha_attribute :squash_commit_sha
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
@@ -1582,6 +1584,23 @@ class MergeRequest < ApplicationRecord
super.merge(label_url_method: :project_merge_requests_url)
end
+ override :ensure_metrics
+ def ensure_metrics
+ MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id).tap do |metrics_record|
+ # Make sure we refresh the loaded association object with the newly created/loaded item.
+ # This is needed in order to have the exact functionality than before.
+ #
+ # Example:
+ #
+ # merge_request.metrics.destroy
+ # merge_request.ensure_metrics
+ # merge_request.metrics # should return the metrics record and not nil
+ # merge_request.metrics.merge_request # should return the same MR record
+ metrics_record.association(:merge_request).target = self
+ association(:metrics).target = metrics_record
+ end
+ end
+
private
def with_rebase_lock
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index 36294c88b8c..efd403aa21c 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -37,6 +37,8 @@ module AlertManagement
MARKDOWN
end
+ def metrics_dashboard_url; end
+
private
attr_reader :alert, :project
diff --git a/app/presenters/alert_management/prometheus_alert_presenter.rb b/app/presenters/alert_management/prometheus_alert_presenter.rb
index b3a27d0632f..3bcc98e6784 100644
--- a/app/presenters/alert_management/prometheus_alert_presenter.rb
+++ b/app/presenters/alert_management/prometheus_alert_presenter.rb
@@ -2,6 +2,10 @@
module AlertManagement
class PrometheusAlertPresenter < AlertManagement::AlertPresenter
+ def metrics_dashboard_url
+ alerting_alert.metrics_dashboard_url
+ end
+
private
def alert_markdown
diff --git a/app/presenters/projects/prometheus/alert_presenter.rb b/app/presenters/projects/prometheus/alert_presenter.rb
index 28b8cd6522c..8307c0ed8b8 100644
--- a/app/presenters/projects/prometheus/alert_presenter.rb
+++ b/app/presenters/projects/prometheus/alert_presenter.rb
@@ -68,9 +68,13 @@ module Projects
end
def metric_embed_for_alert
- url = embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
+ "\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
+ end
- "\n[](#{url})" if url
+ def metrics_dashboard_url
+ strong_memoize(:metrics_dashboard_url) do
+ embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
+ end
end
private
@@ -133,6 +137,7 @@ module Projects
project,
gitlab_alert.prometheus_metric_id,
environment_id: environment.id,
+ embedded: true,
**alert_embed_window_params(embed_time)
)
end
@@ -144,6 +149,7 @@ module Projects
project,
environment,
embed_json: dashboard_for_self_managed_alert.to_json,
+ embedded: true,
**alert_embed_window_params(embed_time)
)
end
diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb
index 977626fcf17..146a0b53fc1 100644
--- a/app/services/snippets/destroy_service.rb
+++ b/app/services/snippets/destroy_service.rb
@@ -27,6 +27,11 @@ module Snippets
attempt_destroy!
+ # Update project statistics if the snippet is a Project one
+ if snippet.project_id
+ ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
+ end
+
ServiceResponse.success(message: 'Snippet was deleted.')
rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400)
diff --git a/app/services/snippets/update_statistics_service.rb b/app/services/snippets/update_statistics_service.rb
new file mode 100644
index 00000000000..61fa43e7755
--- /dev/null
+++ b/app/services/snippets/update_statistics_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Snippets
+ class UpdateStatisticsService
+ attr_reader :snippet
+
+ def initialize(snippet)
+ @snippet = snippet
+ end
+
+ def execute
+ unless snippet.repository_exists?
+ return ServiceResponse.error(message: 'Invalid snippet repository', http_status: 400)
+ end
+
+ snippet.repository.expire_statistics_caches
+ statistics.refresh!
+
+ # Update project statistics if the snippet is a Project one
+ if snippet.project_id
+ ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
+ end
+
+ ServiceResponse.success(message: 'Snippet statistics successfully updated.')
+ end
+
+ private
+
+ def statistics
+ @statistics ||= snippet.statistics || snippet.build_statistics
+ end
+ end
+end
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
index 7119b22daef..ea8f53f7342 100644
--- a/app/views/projects/issues/import_csv/_button.html.haml
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -3,7 +3,7 @@
.dropdown.btn-group
%button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon),
- data: { toggle: 'dropdown' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
+ data: { toggle: 'dropdown', qa_selector: 'import_issues_button' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
- if type == :icon
= sprite_icon('import')
- else
@@ -13,4 +13,5 @@
%button{ data: { toggle: 'modal', target: '.issues-import-modal' } }
= _('Import CSV')
- if can_edit
- %li= link_to _('Import from Jira'), project_import_jira_path(@project)
+ %li{ data: { qa_selector: 'import_from_jira_link' } }
+ = link_to _('Import from Jira'), project_import_jira_path(@project)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f0d5ff930b6..0ddba14bb4a 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -77,6 +77,9 @@
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
+ - if Feature.enabled?(:design_management_moved, @project)
+ = render 'projects/issues/design_management'
+
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
@@ -94,6 +97,9 @@
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
- = render 'projects/issues/tabs'
+ - if Feature.enabled?(:design_management_moved, @project)
+ = render 'projects/issues/discussion'
+ - else
+ = render 'projects/issues/tabs'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 62d76294bc0..8f844bd0b47 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -79,7 +79,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
return false unless user
expire_caches(post_received, snippet.repository)
- snippet.repository.expire_statistics_caches
+ Snippets::UpdateStatisticsService.new(snippet).execute
end
# Expire the repository status, branch, and tag cache once per push.
diff --git a/changelogs/unreleased/222964-collapse-button.yml b/changelogs/unreleased/222964-collapse-button.yml
new file mode 100644
index 00000000000..e4720033df2
--- /dev/null
+++ b/changelogs/unreleased/222964-collapse-button.yml
@@ -0,0 +1,5 @@
+---
+title: Add expand/collapse view to Terraform MR widget
+merge_request: 34879
+author:
+type: changed
diff --git a/changelogs/unreleased/35349-reorder-api.yml b/changelogs/unreleased/35349-reorder-api.yml
new file mode 100644
index 00000000000..e45a0b00a9d
--- /dev/null
+++ b/changelogs/unreleased/35349-reorder-api.yml
@@ -0,0 +1,5 @@
+---
+title: "Added support for reordering issues to the v4 API"
+merge_request: 35349
+author: Joel @jjshoe, Lee Tickett @leetickett
+type: added
diff --git a/changelogs/unreleased/add_requirements_visibility_access_project_settings.yml b/changelogs/unreleased/add_requirements_visibility_access_project_settings.yml
deleted file mode 100644
index 0ffae27175c..00000000000
--- a/changelogs/unreleased/add_requirements_visibility_access_project_settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add requirements visibility/access project settings
-merge_request: 34420
-author: Lee Tickett
-type: added
diff --git a/changelogs/unreleased/dedup-merge-request-metrics.yml b/changelogs/unreleased/dedup-merge-request-metrics.yml
new file mode 100644
index 00000000000..276b68ed686
--- /dev/null
+++ b/changelogs/unreleased/dedup-merge-request-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Deduplicate merge_request_metrics table
+merge_request: 29566
+author:
+type: other
diff --git a/changelogs/unreleased/disable_ilm_on_ELK_yaml.yml b/changelogs/unreleased/disable_ilm_on_ELK_yaml.yml
new file mode 100644
index 00000000000..d48f1d935da
--- /dev/null
+++ b/changelogs/unreleased/disable_ilm_on_ELK_yaml.yml
@@ -0,0 +1,5 @@
+---
+title: Disable ILM on ELK vendor yaml
+merge_request: 35398
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-logrotate-su-parameter.yml b/changelogs/unreleased/fix-logrotate-su-parameter.yml
new file mode 100644
index 00000000000..a35fec0e651
--- /dev/null
+++ b/changelogs/unreleased/fix-logrotate-su-parameter.yml
@@ -0,0 +1,5 @@
+---
+title: Make logrotate run as git user for source installations
+merge_request: 35519
+author:
+type: security
diff --git a/changelogs/unreleased/fj-223701-update-snippets-statistics-after-post-receive.yml b/changelogs/unreleased/fj-223701-update-snippets-statistics-after-post-receive.yml
new file mode 100644
index 00000000000..93959519d9f
--- /dev/null
+++ b/changelogs/unreleased/fj-223701-update-snippets-statistics-after-post-receive.yml
@@ -0,0 +1,5 @@
+---
+title: Update snippet and project statistics after certain events
+merge_request: 35340
+author:
+type: changed
diff --git a/changelogs/unreleased/sy-metrics-embeds-in-alerts.yml b/changelogs/unreleased/sy-metrics-embeds-in-alerts.yml
new file mode 100644
index 00000000000..ec7d201359b
--- /dev/null
+++ b/changelogs/unreleased/sy-metrics-embeds-in-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Expose metrics dashboard URL for alert GraphQL query
+merge_request: 35293
+author:
+type: added
diff --git a/db/migrate/20200615203153_add_requirements_access_level_to_project_features.rb b/db/migrate/20200615203153_add_requirements_access_level_to_project_features.rb
deleted file mode 100644
index 2dff8e3cc4e..00000000000
--- a/db/migrate/20200615203153_add_requirements_access_level_to_project_features.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class AddRequirementsAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- def up
- with_lock_retries do
- add_column :project_features, :requirements_access_level, :integer, default: 20, null: false
- end
- end
-
- def down
- with_lock_retries do
- remove_column :project_features, :requirements_access_level, :integer
- end
- end
-end
diff --git a/db/post_migrate/20200526115436_dedup_mr_metrics.rb b/db/post_migrate/20200526115436_dedup_mr_metrics.rb
new file mode 100644
index 00000000000..d2660504939
--- /dev/null
+++ b/db/post_migrate/20200526115436_dedup_mr_metrics.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+class DedupMrMetrics < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX_NAME = 'tmp_unique_merge_request_metrics_by_merge_request_id'
+ INDEX_NAME = 'unique_merge_request_metrics_by_merge_request_id'
+
+ disable_ddl_transaction!
+
+ class MergeRequestMetrics < ActiveRecord::Base
+ self.table_name = 'merge_request_metrics'
+
+ include EachBatch
+ end
+
+ def up
+ last_metrics_record_id = MergeRequestMetrics.maximum(:id) || 0
+
+ # This index will disallow further duplicates while we're deduplicating the data.
+ add_concurrent_index(:merge_request_metrics, :merge_request_id, where: "id > #{Integer(last_metrics_record_id)}", unique: true, name: TMP_INDEX_NAME)
+
+ MergeRequestMetrics.each_batch do |relation|
+ duplicated_merge_request_ids = MergeRequestMetrics
+ .where(merge_request_id: relation.select(:merge_request_id))
+ .select(:merge_request_id)
+ .group(:merge_request_id)
+ .having('COUNT(merge_request_metrics.merge_request_id) > 1')
+ .pluck(:merge_request_id)
+
+ duplicated_merge_request_ids.each do |merge_request_id|
+ deduplicate_item(merge_request_id)
+ end
+ end
+
+ add_concurrent_index(:merge_request_metrics, :merge_request_id, unique: true, name: INDEX_NAME)
+ remove_concurrent_index_by_name(:merge_request_metrics, TMP_INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:merge_request_metrics, TMP_INDEX_NAME)
+ remove_concurrent_index_by_name(:merge_request_metrics, INDEX_NAME)
+ end
+
+ private
+
+ def deduplicate_item(merge_request_id)
+ merge_request_metrics_records = MergeRequestMetrics.where(merge_request_id: merge_request_id).order(updated_at: :asc).to_a
+
+ attributes = {}
+ merge_request_metrics_records.each do |merge_request_metrics_record|
+ params = merge_request_metrics_record.attributes.except('id')
+ attributes.merge!(params.compact)
+ end
+
+ ActiveRecord::Base.transaction do
+ record_to_keep = merge_request_metrics_records.pop
+ records_to_delete = merge_request_metrics_records
+
+ MergeRequestMetrics.where(id: records_to_delete.map(&:id)).delete_all
+ record_to_keep.update!(attributes)
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 7f0434a612a..72893e84a28 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13976,8 +13976,7 @@ CREATE TABLE public.project_features (
repository_access_level integer DEFAULT 20 NOT NULL,
pages_access_level integer NOT NULL,
forking_access_level integer,
- metrics_dashboard_access_level integer,
- requirements_access_level integer DEFAULT 20 NOT NULL
+ metrics_dashboard_access_level integer
);
CREATE SEQUENCE public.project_features_id_seq
@@ -20416,6 +20415,8 @@ CREATE INDEX tmp_index_ci_pipelines_lock_version ON public.ci_pipelines USING bt
CREATE INDEX tmp_index_ci_stages_lock_version ON public.ci_stages USING btree (id) WHERE (lock_version IS NULL);
+CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON public.merge_request_metrics USING btree (merge_request_id);
+
CREATE UNIQUE INDEX users_security_dashboard_projects_unique_index ON public.users_security_dashboard_projects USING btree (project_id, user_id);
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON public.vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
@@ -23410,6 +23411,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200525144525
20200526000407
20200526013844
+20200526115436
20200526120714
20200526142550
20200526153844
@@ -23459,7 +23461,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200615121217
20200615123055
20200615193524
-20200615203153
20200615232735
20200615234047
20200616145031
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index 75183436046..784e496d10e 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -1,20 +1,5 @@
---
-type: reference
+redirect_to: '../postgresql/index.md'
---
-# Configuring PostgreSQL for Scaling and High Availability
-
-In this section, you'll be guided through configuring a PostgreSQL database to
-be used with GitLab in one of our [Scalable and Highly Available Setups](../reference_architectures/index.md).
-
-## Provide your own PostgreSQL instance **(CORE ONLY)**
-
-This content has been moved to a [new location](../postgresql/external.md).
-
-## Standalone PostgreSQL using Omnibus GitLab **(CORE ONLY)**
-
-This content has been moved to a [new location](../postgresql/standalone.md).
-
-## PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
-
-This content has been moved to a [new location](../postgresql/replication_and_failover.md).
+This document was moved to [another location](../postgresql/index.md).
diff --git a/doc/administration/postgresql/index.md b/doc/administration/postgresql/index.md
new file mode 100644
index 00000000000..7e0a2f3cae1
--- /dev/null
+++ b/doc/administration/postgresql/index.md
@@ -0,0 +1,36 @@
+---
+type: reference
+---
+
+# Configuring PostgreSQL for scaling
+
+In this section, you'll be guided through configuring a PostgreSQL database to
+be used with GitLab in one of our [Scalable and Highly Available Setups](../reference_architectures/index.md).
+There are essentially three setups to choose from.
+
+## PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
+
+This setup is for when you have installed GitLab using the
+[Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee).
+
+All the tools that are needed like PostgreSQL, PgBouncer, Repmgr are bundled in
+the package, so you can it to set up the whole PostgreSQL infrastructure (primary, replica).
+
+[> Read how to set up PostgreSQL replication and failover using Omnibus GitLab](replication_and_failover.md)
+
+## Standalone PostgreSQL using Omnibus GitLab **(CORE ONLY)**
+
+This setup is for when you have installed the
+[Omnibus GitLab packages](https://about.gitlab.com/install/) (CE or EE),
+to use the bundled PostgreSQL having only its service enabled.
+
+[> Read how to set up a standalone PostgreSQL instance using Omnibus GitLab](standalone.md)
+
+## Provide your own PostgreSQL instance **(CORE ONLY)**
+
+This setup is for when you have installed GitLab using the
+[Omnibus GitLab packages](https://about.gitlab.com/install/) (CE or EE),
+or installed it [from source](../../install/installation.md), but you want to use
+your own external PostgreSQL server.
+
+[> Read how to set up an external PostgreSQL instance](external.md)
diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md
index 5b2c50c21d9..3a682a49fd0 100644
--- a/doc/administration/postgresql/replication_and_failover.md
+++ b/doc/administration/postgresql/replication_and_failover.md
@@ -1,16 +1,15 @@
# PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM ONLY)**
-> Important notes:
->
-> - This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
-> - If you are a Community Edition or Starter user, consider using a cloud hosted solution.
-> - This document will not cover installations from source.
->
-> - If a setup with replication and failover is not what you were looking for, see the [database configuration document](https://docs.gitlab.com/omnibus/settings/database.html)
-> for the Omnibus GitLab packages.
->
-> Please read this document fully before attempting to configure PostgreSQL with
-> replication and failover for GitLab.
+This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
+If you are a Community Edition or Starter user, consider using a cloud hosted solution.
+This document will not cover installations from source.
+
+If a setup with replication and failover is not what you were looking for, see
+the [database configuration document](https://docs.gitlab.com/omnibus/settings/database.html)
+for the Omnibus GitLab packages.
+
+It's recommended to read this document fully before attempting to configure PostgreSQL with
+replication and failover for GitLab.
## Architecture
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 86685a3cf98..ba5a22e070f 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -260,6 +260,11 @@ type AlertManagementAlert implements Noteable {
issueIid: ID
"""
+ URL for metrics embed for the alert
+ """
+ metricsDashboardUrl: String
+
+ """
Monitoring tool the alert came from
"""
monitoringTool: String
@@ -11310,6 +11315,11 @@ type SecurityReportSummary {
containerScanning: SecurityReportSummarySection
"""
+ Aggregated counts for the coverage_fuzzing scan
+ """
+ coverageFuzzing: SecurityReportSummarySection
+
+ """
Aggregated counts for the dast scan
"""
dast: SecurityReportSummarySection
@@ -13994,7 +14004,8 @@ type Vulnerability {
"""
Type of the security report that found the vulnerability (SAST,
- DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)
+ DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION,
+ COVERAGE_FUZZING)
"""
reportType: VulnerabilityReportType
@@ -14332,6 +14343,7 @@ The type of the security scan that found the vulnerability.
"""
enum VulnerabilityReportType {
CONTAINER_SCANNING
+ COVERAGE_FUZZING
DAST
DEPENDENCY_SCANNING
SAST
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 375f64c1c94..76fd767d002 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -717,6 +717,20 @@
"deprecationReason": null
},
{
+ "name": "metricsDashboardUrl",
+ "description": "URL for metrics embed for the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "monitoringTool",
"description": "Monitoring tool the alert came from",
"args": [
@@ -33205,6 +33219,20 @@
"deprecationReason": null
},
{
+ "name": "coverageFuzzing",
+ "description": "Aggregated counts for the coverage_fuzzing scan",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "SecurityReportSummarySection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "dast",
"description": "Aggregated counts for the dast scan",
"args": [
@@ -41227,7 +41255,7 @@
},
{
"name": "reportType",
- "description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)",
+ "description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING)",
"args": [
],
@@ -42297,6 +42325,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "COVERAGE_FUZZING",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"possibleTypes": null
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fdf49bf1795..f25f99b3ee2 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -69,6 +69,7 @@ Describes an alert from the project's Alert Management
| `hosts` | String! => Array | List of hosts the alert came from |
| `iid` | ID! | Internal ID of the alert |
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
+| `metricsDashboardUrl` | String | URL for metrics embed for the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
@@ -1642,6 +1643,7 @@ Represents summary of a security report
| Name | Type | Description |
| --- | ---- | ---------- |
| `containerScanning` | SecurityReportSummarySection | Aggregated counts for the container_scanning scan |
+| `coverageFuzzing` | SecurityReportSummarySection | Aggregated counts for the coverage_fuzzing scan |
| `dast` | SecurityReportSummarySection | Aggregated counts for the dast scan |
| `dependencyScanning` | SecurityReportSummarySection | Aggregated counts for the dependency_scanning scan |
| `sast` | SecurityReportSummarySection | Aggregated counts for the sast scan |
@@ -2100,7 +2102,7 @@ Represents a vulnerability.
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `primaryIdentifier` | VulnerabilityIdentifier | Primary identifier of the vulnerability. |
| `project` | Project | The project on which the vulnerability was found |
-| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION) |
+| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING) |
| `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 9d6bbf7f0e1..e9f655ed983 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -901,6 +901,25 @@ DELETE /projects/:id/issues/:issue_iid
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85"
```
+## Reorder an issue
+
+Reorders an issue, you can see the results when sorting issues manually
+
+```plaintext
+PUT /projects/:id/issues/:issue_iid/reorder
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `move_after_id` | integer | no | The ID of a projet's issue to move this issue after |
+| `move_before_id` | integer | no | The ID of a projet's issue to move this issue before |
+
+```shell
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85/reorder?move_after_id=51&move_before_id=92"
+```
+
## Move an issue
Moves an issue to a different project. If the target project
diff --git a/doc/api/projects.md b/doc/api/projects.md
index af1c8028531..6d56ab9befe 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1048,7 +1048,6 @@ POST /projects
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
-| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
@@ -1120,7 +1119,6 @@ POST /projects/user/:user_id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
-| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
@@ -1191,7 +1189,6 @@ PUT /projects/:id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
-| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 3d7aea89e73..9910f0651b8 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -30,7 +30,7 @@ subgraph "2. gitlab `review-prepare` stage"
end
subgraph "3. gitlab `review` stage"
- C["review-deploy<br><br>Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br><br>Cloud Native images are deployed to the `review-apps-ce` or `review-apps-ee`<br>Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
+ C["review-deploy<br><br>Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br><br>Cloud Native images are deployed to the `review-apps`<br>Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
end
subgraph "4. gitlab `qa` stage"
@@ -62,7 +62,7 @@ subgraph "CNG-mirror pipeline"
job, which runs only for tags, and triggers itself a [`CNG`](https://gitlab.com/gitlab-org/build/CNG) pipeline.
1. Once the `test` stage is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
deploys the Review App using [the official GitLab Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/) to
- the [`review-apps-ce`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps) / [`review-apps-ee`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps)
+ the [`review-apps`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps?project=gitlab-review-apps)
Kubernetes cluster on GCP.
- The actual scripts used to deploy the Review App can be found at
[`scripts/review_apps/review-apps.sh`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/review_apps/review-apps.sh).
@@ -136,11 +136,10 @@ browser performance testing using a
### Node pools
-The `review-apps-ee` and `review-apps-ce` clusters are currently set up with
+The `review-apps` cluster is currently set up with
the following node pools:
-- `review-apps-ee` of pre-emptible `e2-highcpu-16` (16 vCPU, 16 GB memory) nodes with autoscaling
-- `review-apps-ce` of pre-emptible `n1-standard-8` (8 vCPU, 16 GB memory) nodes with autoscaling
+- `e2-highcpu-16` (16 vCPU, 16 GB memory) pre-emptible nodes with autoscaling
### Helm
@@ -189,9 +188,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
default command or
- - Run `kubectl exec --namespace review-apps-ce review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and
- - Replace `review-apps-ce` with `review-apps-ee` if the Review App
- is running EE, and
+ - Run `kubectl exec --namespace review-apps review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and
- Replace `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`
with your Pod's name.
diff --git a/doc/user/project/code_intelligence.md b/doc/user/project/code_intelligence.md
index 3717e46a7ae..e2c2cae3158 100644
--- a/doc/user/project/code_intelligence.md
+++ b/doc/user/project/code_intelligence.md
@@ -23,12 +23,14 @@ Enable code intelligence for a project by adding a GitLab CI/CD job to the proje
```yaml
code_navigation:
+ image: golang:1.14.0
+ allow_failure: true # recommended
script:
- go get github.com/sourcegraph/lsif-go/cmd/lsif-go
- lsif-go
-artifacts:
- reports:
- lsif: dump.lsif
+ artifacts:
+ reports:
+ lsif: dump.lsif
```
The generated LSIF file must be less than 170MiB.
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 6375333c5ec..0798c39fff5 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -62,7 +62,6 @@ Use the switches to enable or disable the following features:
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
-| **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md)
Some features depend on others:
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 93b0fbc5223..de24de291ec 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -289,6 +289,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Reorder an existing issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ optional :move_after_id, type: Integer, desc: 'The ID of the issue we want to be after'
+ optional :move_before_id, type: Integer, desc: 'The ID of the issue we want to be before'
+ at_least_one_of :move_after_id, :move_before_id
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ put ':id/issues/:issue_iid/reorder' do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
+ not_found!('Issue') unless issue
+
+ authorize! :update_issue, issue
+
+ if ::Issues::ReorderService.new(user_project, current_user, params).execute(issue)
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_api_error!({ error: 'Unprocessable Entity' }, 422)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
desc 'Move an existing issue' do
success Entities::Issue
end
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 74736b24d73..38dfea2a0c1 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications
- requirements].freeze
+ requirements coverage_fuzzing].freeze
attributes ALLOWED_KEYS
@@ -25,7 +25,8 @@ module Gitlab
with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true
- validates :codequality, array_of_strings_or_string: true
+ validates :coverage_fuzzing, array_of_strings_or_string: true
+ validates :sast, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :secret_detection, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true
diff --git a/lib/gitlab/metrics/methods.rb b/lib/gitlab/metrics/methods.rb
index 5955987541c..83a7b925392 100644
--- a/lib/gitlab/metrics/methods.rb
+++ b/lib/gitlab/metrics/methods.rb
@@ -35,7 +35,7 @@ module Gitlab
end
def init_metric(type, name, opts = {}, &block)
- options = MetricOptions.new(opts)
+ options = ::Gitlab::Metrics::Methods::MetricOptions.new(opts)
options.evaluate(&block)
if disabled_by_feature(options)
diff --git a/lib/support/logrotate/gitlab b/lib/support/logrotate/gitlab
index d9b07b61ec3..c34db47e214 100644
--- a/lib/support/logrotate/gitlab
+++ b/lib/support/logrotate/gitlab
@@ -2,6 +2,7 @@
# based on: http://stackoverflow.com/a/4883967
/home/git/gitlab/log/*.log {
+ su git git
daily
missingok
rotate 90
@@ -11,6 +12,7 @@
}
/home/git/gitlab-shell/gitlab-shell.log {
+ su git git
daily
missingok
rotate 90
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8b93938a3de..2a0bcd664d3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -999,9 +999,6 @@ msgstr ""
msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
msgstr ""
-msgid "A Terraform report was generated in your pipelines."
-msgstr ""
-
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
@@ -3823,6 +3820,9 @@ msgstr ""
msgid "By %{user_name}"
msgstr ""
+msgid "By URL"
+msgstr ""
+
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
msgstr ""
@@ -6552,6 +6552,12 @@ msgstr ""
msgid "Coverage"
msgstr ""
+msgid "Coverage Fuzzing"
+msgstr ""
+
+msgid "Crash State"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -10028,6 +10034,9 @@ msgstr ""
msgid "Find File"
msgstr ""
+msgid "Find bugs in your code with coverage-guided fuzzing"
+msgstr ""
+
msgid "Find by path"
msgstr ""
@@ -10274,9 +10283,6 @@ msgstr ""
msgid "Generate new export"
msgstr ""
-msgid "Generating the report caused an error."
-msgstr ""
-
msgid "Geo"
msgstr ""
@@ -13862,6 +13868,9 @@ msgstr ""
msgid "Maximum field length"
msgstr ""
+msgid "Maximum file size is 2MB. Please select a smaller file."
+msgstr ""
+
msgid "Maximum import size (MB)"
msgstr ""
@@ -16660,6 +16669,9 @@ msgstr ""
msgid "Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}."
msgstr ""
+msgid "Please choose a file"
+msgstr ""
+
msgid "Please choose a group URL with no special characters."
msgstr ""
@@ -17785,12 +17797,6 @@ msgstr ""
msgid "ProjectSettings|Repository"
msgstr ""
-msgid "ProjectSettings|Requirements"
-msgstr ""
-
-msgid "ProjectSettings|Requirements management system for this project"
-msgstr ""
-
msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgstr ""
@@ -19100,9 +19106,6 @@ msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr ""
-msgid "Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
-msgstr ""
-
msgid "Reporter"
msgstr ""
@@ -20310,6 +20313,9 @@ msgstr ""
msgid "Select due date"
msgstr ""
+msgid "Select file"
+msgstr ""
+
msgid "Select group or project"
msgstr ""
@@ -21556,6 +21562,9 @@ msgstr ""
msgid "Stack trace"
msgstr ""
+msgid "Stacktrace snippet"
+msgstr ""
+
msgid "Stage"
msgstr ""
@@ -22363,6 +22372,34 @@ msgstr ""
msgid "Terms of Service and Privacy Policy"
msgstr ""
+msgid "Terraform|%{number} Terraform report failed to generate"
+msgid_plural "Terraform|%{number} Terraform reports failed to generate"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Terraform|%{number} Terraform report was generated in your pipelines"
+msgid_plural "Terraform|%{number} Terraform reports were generated in your pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Terraform|A Terraform report failed to generate."
+msgstr ""
+
+msgid "Terraform|A Terraform report was generated in your pipelines."
+msgstr ""
+
+msgid "Terraform|Generating the report caused an error."
+msgstr ""
+
+msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
+msgstr ""
+
+msgid "Terraform|The Terraform report %{name} failed to generate."
+msgstr ""
+
+msgid "Terraform|The Terraform report %{name} was generated in your pipelines."
+msgstr ""
+
msgid "Test"
msgstr ""
@@ -22490,9 +22527,6 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
-msgid "The Terraform report %{name} was generated in your pipelines."
-msgstr ""
-
msgid "The URL defined on the primary node that secondary nodes should use to contact it."
msgstr ""
@@ -25575,6 +25609,9 @@ msgstr ""
msgid "Vulnerability|Class"
msgstr ""
+msgid "Vulnerability|Crash Address"
+msgstr ""
+
msgid "Vulnerability|Description"
msgstr ""
@@ -26866,6 +26903,9 @@ msgstr ""
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
msgstr ""
+msgid "ciReport|Coverage Fuzzing"
+msgstr ""
+
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
msgstr ""
@@ -27177,9 +27217,6 @@ msgstr ""
msgid "https://your-bitbucket-server"
msgstr ""
-msgid "image"
-msgstr ""
-
msgid "image diff"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 78ee8957789..b28904173a9 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -309,6 +309,7 @@ module QA
autoload :New, 'qa/page/project/issue/new'
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
+ autoload :JiraImport, 'qa/page/project/issue/jira_import'
end
module Fork
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
index ace2537fc0e..e0c10220fbc 100644
--- a/qa/qa/page/project/issue/index.rb
+++ b/qa/qa/page/project/issue/index.rb
@@ -18,6 +18,11 @@ module QA
element :export_issues_modal
end
+ view 'app/views/projects/issues/import_csv/_button.html.haml' do
+ element :import_issues_button
+ element :import_from_jira_link
+ end
+
view 'app/views/projects/issues/_issue.html.haml' do
element :issue
element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern
@@ -51,10 +56,25 @@ module QA
click_element(:export_issues_button)
end
+ def click_import_from_jira_link
+ click_element(:import_from_jira_link)
+ end
+
+ def click_import_issues_dropdown
+ # When there are no issues, the image that loads causes the buttons to jump
+ has_loaded_all_images?
+ click_element(:import_issues_button)
+ end
+
def export_issues_modal
find_element(:export_issues_modal)
end
+ def go_to_jira_import_form
+ click_import_issues_dropdown
+ click_import_from_jira_link
+ end
+
def has_assignee_link_count?(count)
all_elements(:assignee_link, count: count)
end
diff --git a/qa/qa/page/project/issue/jira_import.rb b/qa/qa/page/project/issue/jira_import.rb
new file mode 100644
index 00000000000..d3be24464ab
--- /dev/null
+++ b/qa/qa/page/project/issue/jira_import.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Issue
+ class JiraImport < Page::Base
+ view 'app/assets/javascripts/jira_import/components/jira_import_form.vue' do
+ element :jira_project_dropdown
+ element :jira_issues_import_button
+ end
+
+ def select_jira_project(jira_project)
+ select_element(:jira_project_dropdown, jira_project)
+ end
+
+ def select_project_and_import(jira_project)
+ select_jira_project(jira_project)
+ click_element(:jira_issues_import_button)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb
new file mode 100644
index 00000000000..ba8e8635c87
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'Jira issue import', :jira, :orchestrated, :requires_admin do
+ let(:jira_project_key) { "JITD" }
+ let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" }
+ let(:jira_issue_description) { "This issue is for testing importing Jira issues to GitLab." }
+ let(:jira_issue_label_1) { "jira-import::#{jira_project_key}-1" }
+ let(:jira_issue_label_2) { "QA" }
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = "jira_issue_import"
+ end
+ end
+
+ it 'imports issues from Jira' do
+ set_up_jira_integration
+ import_jira_issues
+
+ QA::Support::Retrier.retry_on_exception do
+ Page::Project::Menu.perform(&:click_issues)
+
+ Page::Project::Issue::Index.perform do |issues_page|
+ issues_page.click_issue_link(jira_issue_title)
+ end
+ end
+
+ expect(page).to have_content(jira_issue_description)
+
+ Page::Project::Issue::Show.perform do |issue|
+ expect(issue).to have_label(jira_issue_label_1)
+ expect(issue).to have_label(jira_issue_label_2)
+ end
+ end
+
+ private
+
+ def set_up_jira_integration
+ # Retry is required because allow_local_requests_from_web_hooks_and_services
+ # takes some time to get enabled.
+ # Bug issue: https://gitlab.com/gitlab-org/gitlab/-/issues/217010
+ QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 3) do
+ Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
+
+ page.visit Runtime::Scenario.gitlab_address
+ Flow::Login.sign_in_unless_signed_in
+
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_integrations_settings)
+ QA::Page::Project::Settings::Integrations.perform(&:click_jira_link)
+
+ QA::Page::Project::Settings::Services::Jira.perform do |jira|
+ jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url))
+ end
+
+ expect(page).not_to have_text("Url is blocked")
+ expect(page).to have_text("Jira activated")
+ end
+ end
+
+ def import_jira_issues
+ Page::Project::Menu.perform(&:click_issues)
+ Page::Project::Issue::Index.perform(&:go_to_jira_import_form)
+
+ Page::Project::Issue::JiraImport.perform do |form|
+ form.select_project_and_import(jira_project_key)
+ end
+
+ expect(page).to have_content("Import in progress")
+ end
+ end
+ end
+end
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index a9659071a2f..f52edd18ba8 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -40,7 +40,7 @@ class AutomatedCleanup
end
def review_apps_namespace
- self.class.ee? ? 'review-apps-ee' : 'review-apps-ce'
+ 'review-apps'
end
def helm
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 6fb6943fb90..9aa518e3bc7 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -7,7 +7,7 @@ global:
external-dns.alpha.kubernetes.io/ttl: 10
configureCertmanager: false
tls:
- secretName: tls-cert
+ secretName: review-apps-tls
initialRootPassword:
secret: shared-gitlab-initial-root-password
certmanager:
@@ -61,11 +61,11 @@ gitlab:
task-runner:
resources:
requests:
- cpu: 50m
- memory: 350M
+ cpu: 300m
+ memory: 800M
limits:
- cpu: 100m
- memory: 700M
+ cpu: 450m
+ memory: 1200M
webservice:
resources:
requests:
diff --git a/scripts/review_apps/gcp_cleanup.sh b/scripts/review_apps/gcp_cleanup.sh
index f289a50f629..3225631e8c7 100755
--- a/scripts/review_apps/gcp_cleanup.sh
+++ b/scripts/review_apps/gcp_cleanup.sh
@@ -11,7 +11,7 @@ function setup_gcp_dependencies() {
# These scripts require the following environment variables:
# - REVIEW_APPS_GCP_REGION - e.g `us-central1`
-# - KUBE_NAMESPACE - e.g `review-apps-ee`
+# - KUBE_NAMESPACE - e.g `review-apps`
function delete_firewall_rules() {
if [[ ${#@} -eq 0 ]]; then
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 1214ee5f462..1e3cdaea3ea 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -66,7 +66,7 @@ function kubectl_cleanup_release() {
local release="${2}"
echoinfo "Deleting all K8s resources matching '${release}'..." true
- kubectl --namespace "${namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd 2>&1 \
+ kubectl --namespace "${namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,clusterrole,clusterrolebinding,role,rolebinding,sa,crd 2>&1 \
| grep "${release}" \
| awk '{print $1}' \
| xargs kubectl --namespace "${namespace}" delete \
@@ -126,6 +126,38 @@ function get_pod() {
echo "${pod_name}"
}
+function run_task() {
+ local namespace="${KUBE_NAMESPACE}"
+ local ruby_cmd="${1}"
+ local task_runner_pod=$(get_pod "task-runner")
+
+ kubectl exec -it --namespace "${namespace}" "${task_runner_pod}" -- gitlab-rails runner "${ruby_cmd}"
+}
+
+function disable_sign_ups() {
+ if [ -z ${REVIEW_APPS_ROOT_TOKEN+x} ]; then
+ echoerr "In order to protect Review Apps, REVIEW_APPS_ROOT_TOKEN variable must be set"
+ false
+ else
+ true
+ fi
+
+ # Create the root token
+ local ruby_cmd="token = User.find_by_username('root').personal_access_tokens.create(scopes: [:api], name: 'Token to disable sign-ups'); token.set_token('${REVIEW_APPS_ROOT_TOKEN}'); begin; token.save!; rescue(ActiveRecord::RecordNotUnique); end"
+ run_task "${ruby_cmd}"
+
+ # Disable sign-ups
+ curl --silent --show-error --request PUT --header "PRIVATE-TOKEN: ${REVIEW_APPS_ROOT_TOKEN}" "${CI_ENVIRONMENT_URL}/api/v4/application/settings?signup_enabled=false"
+
+ local signup_enabled=$(curl --silent --show-error --request GET --header "PRIVATE-TOKEN: ${REVIEW_APPS_ROOT_TOKEN}" "${CI_ENVIRONMENT_URL}/api/v4/application/settings" | jq ".signup_enabled")
+ if [[ "${signup_enabled}" == "false" ]]; then
+ echoinfo "Sign-ups have been disabled successfully."
+ else
+ echoerr "Sign-ups should be disabled but are still enabled!"
+ false
+ fi
+}
+
function check_kube_domain() {
echoinfo "Checking that Kube domain exists..." true
@@ -181,6 +213,32 @@ function install_external_dns() {
fi
}
+# This script is used to install cert-manager in the cluster
+# The installation steps are documented in
+# https://gitlab.com/gitlab-org/quality/team-tasks/snippets/1990286
+function install_certmanager() {
+ local namespace="${KUBE_NAMESPACE}"
+ local release="cert-manager-review-app-helm3"
+
+ echoinfo "Installing cert-manager..." true
+
+ if ! deploy_exists "${namespace}" "${release}" || previous_deploy_failed "${namespace}" "${release}" ; then
+ kubectl apply \
+ -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml
+
+ echoinfo "Installing cert-manager Helm chart"
+ helm repo add jetstack https://charts.jetstack.io
+ helm repo update
+
+ helm install "${release}" jetstack/cert-manager \
+ --namespace "${namespace}" \
+ --version v0.15.1 \
+ --set installCRDS=true
+ else
+ echoinfo "The cert-manager Helm chart is already successfully deployed."
+ fi
+}
+
function create_application_secret() {
local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}"
diff --git a/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb b/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
index 1b3231bf9ee..8d5e99d7e2b 100644
--- a/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
+++ b/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
@@ -27,10 +27,6 @@ RSpec.describe 'viewing issues with design references' do
MD
end
- before do
- stub_feature_flags(design_management_moved: false)
- end
-
def visit_page_with_design_references
public_issue = create(:issue, project: public_project, description: description)
visit project_issue_path(public_issue.project, public_issue)
diff --git a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
index 72638125f09..aff8951d9de 100644
--- a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
@@ -8,34 +8,57 @@ RSpec.describe 'User paginates issue designs', :js do
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
- before do
- enable_design_management
- stub_feature_flags(design_management_moved: false)
+ context 'design_management_moved flag disabled' do
+ before do
+ stub_feature_flags(design_management_moved: false)
+ enable_design_management
- create_list(:design, 2, :with_file, issue: issue)
+ create_list(:design, 2, :with_file, issue: issue)
+ visit project_issue_path(project, issue)
+ click_link 'Designs'
+ wait_for_requests
+ find('.js-design-list-item', match: :first).click
+ end
- visit project_issue_path(project, issue)
+ it 'paginates to next design' do
+ expect(find('.js-previous-design')[:disabled]).to eq('true')
- click_link 'Designs'
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('1 of 2')
+ end
- wait_for_requests
+ find('.js-next-design').click
- find('.js-design-list-item', match: :first).click
- end
+ expect(find('.js-previous-design')[:disabled]).not_to eq('true')
- it 'paginates to next design' do
- expect(find('.js-previous-design')[:disabled]).to eq('true')
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('2 of 2')
+ end
+ end
+ end
- page.within(find('.js-design-header')) do
- expect(page).to have_content('1 of 2')
+ context 'design_management_moved flag enabled' do
+ before do
+ enable_design_management
+ create_list(:design, 2, :with_file, issue: issue)
+ visit project_issue_path(project, issue)
+ find('.js-design-list-item', match: :first).click
end
- find('.js-next-design').click
+ it 'paginates to next design' do
+ expect(find('.js-previous-design')[:disabled]).to eq('true')
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('1 of 2')
+ end
+
+ find('.js-next-design').click
- expect(find('.js-previous-design')[:disabled]).not_to eq('true')
+ expect(find('.js-previous-design')[:disabled]).not_to eq('true')
- page.within(find('.js-design-header')) do
- expect(page).to have_content('2 of 2')
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('2 of 2')
+ end
end
end
end
diff --git a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
index 25686774e7d..4e45312eac3 100644
--- a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
+++ b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
@@ -8,18 +8,32 @@ RSpec.describe 'User design permissions', :js do
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
- before do
- enable_design_management
- stub_feature_flags(design_management_moved: false)
+ context 'design_management_moved flag disabled' do
+ before do
+ enable_design_management
+ stub_feature_flags(design_management_moved: false)
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- click_link 'Designs'
+ click_link 'Designs'
- wait_for_requests
+ wait_for_requests
+ end
+
+ it 'user does not have permissions to upload design' do
+ expect(page).not_to have_field('design_file')
+ end
end
- it 'user does not have permissions to upload design' do
- expect(page).not_to have_field('design_file')
+ context 'design_management_moved flag enabled' do
+ before do
+ enable_design_management
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'user does not have permissions to upload design' do
+ expect(page).not_to have_field('design_file')
+ end
end
end
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index 0861c0bd631..a173d633f2c 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -13,44 +13,81 @@ RSpec.describe 'User uploads new design', :js do
sign_in(user)
end
- context "when the feature is available" do
- before do
- enable_design_management
- stub_feature_flags(design_management_moved: false)
+ context 'design_management_moved flag disabled' do
+ context "when the feature is available" do
+ before do
+ enable_design_management
+ stub_feature_flags(design_management_moved: false)
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- click_link 'Designs'
+ click_link 'Designs'
- wait_for_requests
- end
+ wait_for_requests
+ end
+
+ it 'uploads designs' do
+ attach_file(:design_file, logo_fixture, make_visible: true)
- it 'uploads designs' do
- attach_file(:design_file, logo_fixture, make_visible: true)
+ expect(page).to have_selector('.js-design-list-item', count: 1)
- expect(page).to have_selector('.js-design-list-item', count: 1)
+ within first('#designs-tab .js-design-list-item') do
+ expect(page).to have_content('dk.png')
+ end
- within first('#designs-tab .js-design-list-item') do
- expect(page).to have_content('dk.png')
+ attach_file(:design_file, gif_fixture, make_visible: true)
+
+ expect(page).to have_selector('.js-design-list-item', count: 2)
end
+ end
- attach_file(:design_file, gif_fixture, make_visible: true)
+ context 'when the feature is not available' do
+ before do
+ stub_feature_flags(design_management_moved: false)
+ visit project_issue_path(project, issue)
- expect(page).to have_selector('.js-design-list-item', count: 2)
+ click_link 'Designs'
+
+ wait_for_requests
+ end
+
+ it 'shows the message about requirements' do
+ expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
+ end
end
end
- context 'when the feature is not available' do
- before do
- visit project_issue_path(project, issue)
+ context 'design_management_moved flag enabled' do
+ context "when the feature is available" do
+ before do
+ enable_design_management
- click_link 'Designs'
+ visit project_issue_path(project, issue)
+ end
+
+ it 'uploads designs' do
+ attach_file(:design_file, logo_fixture, make_visible: true)
+
+ expect(page).to have_selector('.js-design-list-item', count: 1)
+
+ within first('[data-testid="designs-root"] .js-design-list-item') do
+ expect(page).to have_content('dk.png')
+ end
+
+ attach_file(:design_file, gif_fixture, make_visible: true)
- wait_for_requests
+ expect(page).to have_selector('.js-design-list-item', count: 2)
+ end
end
- it 'shows the message about requirements' do
- expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
+ context 'when the feature is not available' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows the message about requirements' do
+ expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
+ end
end
end
diff --git a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
index 14c418e26b8..4a4c33cb881 100644
--- a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe 'Users views raw design image files' do
before do
enable_design_management
- stub_feature_flags(design_management_moved: false)
end
it 'serves the latest design version when no ref is given' do
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
index da2928e9092..49245218e81 100644
--- a/spec/features/projects/issues/design_management/user_views_design_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -9,22 +9,42 @@ RSpec.describe 'User views issue designs', :js do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, issue: issue) }
- before do
- enable_design_management
- stub_feature_flags(design_management_moved: false)
+ context 'design_management_moved flag disabled' do
+ before do
+ enable_design_management
+ stub_feature_flags(design_management_moved: false)
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, issue)
- click_link 'Designs'
+ click_link 'Designs'
+ end
+
+ it 'opens design detail' do
+ click_link design.filename
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content(design.filename)
+ end
+
+ expect(page).to have_selector('.js-design-image')
+ end
end
- it 'opens design detail' do
- click_link design.filename
+ context 'design_management_moved flag enabled' do
+ before do
+ enable_design_management
- page.within(find('.js-design-header')) do
- expect(page).to have_content(design.filename)
+ visit project_issue_path(project, issue)
end
- expect(page).to have_selector('.js-design-image')
+ it 'opens design detail' do
+ click_link design.filename
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content(design.filename)
+ end
+
+ expect(page).to have_selector('.js-design-image')
+ end
end
end
diff --git a/spec/features/projects/issues/design_management/user_views_designs_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
index 5d6571f8339..772a9ffbe6f 100644
--- a/spec/features/projects/issues/design_management/user_views_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
@@ -9,40 +9,78 @@ RSpec.describe 'User views issue designs', :js do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, issue: issue) }
- before do
- enable_design_management
- stub_feature_flags(design_management_moved: false)
- end
-
- context 'navigates from the issue view' do
+ context 'design_management_moved flag disabled' do
before do
- visit project_issue_path(project, issue)
- click_link 'Designs'
- wait_for_requests
+ enable_design_management
+ stub_feature_flags(design_management_moved: false)
end
- it 'fetches list of designs' do
- expect(page).to have_selector('.js-design-list-item', count: 1)
+ context 'navigates from the issue view' do
+ before do
+ visit project_issue_path(project, issue)
+ click_link 'Designs'
+ wait_for_requests
+ end
+
+ it 'fetches list of designs' do
+ expect(page).to have_selector('.js-design-list-item', count: 1)
+ end
end
- end
- context 'navigates directly to the design collection view' do
- before do
- visit designs_project_issue_path(project, issue)
+ context 'navigates directly to the design collection view' do
+ before do
+ visit designs_project_issue_path(project, issue)
+ end
+
+ it 'expands the sidebar' do
+ expect(page).to have_selector('.layout-page.right-sidebar-expanded')
+ end
end
- it 'expands the sidebar' do
- expect(page).to have_selector('.layout-page.right-sidebar-expanded')
+ context 'navigates directly to the individual design view' do
+ before do
+ visit designs_project_issue_path(project, issue, vueroute: design.filename)
+ end
+
+ it 'sees the design' do
+ expect(page).to have_selector('.js-design-detail')
+ end
end
end
- context 'navigates directly to the individual design view' do
+ context 'design_management_moved flag enabled' do
before do
- visit designs_project_issue_path(project, issue, vueroute: design.filename)
+ enable_design_management
end
- it 'sees the design' do
- expect(page).to have_selector('.js-design-detail')
+ context 'navigates from the issue view' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'fetches list of designs' do
+ expect(page).to have_selector('.js-design-list-item', count: 1)
+ end
+ end
+
+ context 'navigates directly to the design collection view' do
+ before do
+ visit designs_project_issue_path(project, issue)
+ end
+
+ it 'expands the sidebar' do
+ expect(page).to have_selector('.layout-page.right-sidebar-expanded')
+ end
+ end
+
+ context 'navigates directly to the individual design view' do
+ before do
+ visit designs_project_issue_path(project, issue, vueroute: design.filename)
+ end
+
+ it 'sees the design' do
+ expect(page).to have_selector('.js-design-detail')
+ end
end
end
end
diff --git a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
index bde8df0393b..0fe84ab47ed 100644
--- a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
@@ -12,7 +12,6 @@ RSpec.describe 'User views an SVG design that contains XSS', :js do
before do
enable_design_management
- stub_feature_flags(design_management_moved: false)
visit designs_project_issue_path(
project,
@@ -30,6 +29,7 @@ RSpec.describe 'User views an SVG design that contains XSS', :js do
end
it 'displays the SVG' do
+ find("[data-testid='close-design']").click
expect(page).to have_selector("img.design-img[alt='xss.svg']", count: 1, visible: false)
end
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
index 7fe9cae238a..1cbcb3d756d 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -3,7 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertDetails from '~/alert_management/components/alert_details.vue';
-import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
+import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import {
trackAlertsDetailsViewsOptions,
@@ -25,14 +25,14 @@ describe('AlertDetails', () => {
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, {
- propsData: {
+ provide: {
alertId: 'alertId',
projectPath,
projectIssuesPath,
projectId,
},
data() {
- return { alert: { ...mockAlert }, ...data };
+ return { alert: { ...mockAlert }, sidebarStatus: false, ...data };
},
mocks: {
$apollo: {
@@ -41,6 +41,7 @@ describe('AlertDetails', () => {
alert: {
loading,
},
+ sidebarStatus: {},
},
},
},
@@ -135,7 +136,7 @@ describe('AlertDetails', () => {
it('should display "View issue" button that links the issue page when issue exists', () => {
const issueIid = '3';
mountComponent({
- data: { alert: { ...mockAlert, issueIid } },
+ data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false },
});
expect(findViewIssueBtn().exists()).toBe(true);
expect(findViewIssueBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, issueIid));
@@ -148,8 +149,11 @@ describe('AlertDetails', () => {
mountMethod: mount,
data: { alert: { ...mockAlert, issueIid } },
});
- expect(findViewIssueBtn().exists()).toBe(false);
- expect(findCreateIssueBtn().exists()).toBe(true);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findViewIssueBtn().exists()).toBe(false);
+ expect(findCreateIssueBtn().exists()).toBe(true);
+ });
});
it('calls `$apollo.mutate` with `createIssueQuery`', () => {
@@ -160,7 +164,7 @@ describe('AlertDetails', () => {
findCreateIssueBtn().trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: createIssueQuery,
+ mutation: createIssueMutation,
variables: {
iid: mockAlert.iid,
projectPath,
diff --git a/spec/frontend/alert_management/components/alert_sidebar_spec.js b/spec/frontend/alert_management/components/alert_sidebar_spec.js
index 3c9cc860ed5..2536e0c230a 100644
--- a/spec/frontend/alert_management/components/alert_sidebar_spec.js
+++ b/spec/frontend/alert_management/components/alert_sidebar_spec.js
@@ -11,20 +11,28 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
- function mountComponent({
- sidebarCollapsed = true,
- mountMethod = shallowMount,
- stubs = {},
- alert = {},
- } = {}) {
+ function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
wrapper = mountMethod(AlertSidebar, {
+ data() {
+ return {
+ sidebarStatus: false,
+ };
+ },
propsData: {
alert,
- sidebarCollapsed,
+ },
+ provide: {
projectPath: 'projectPath',
projectId: '1',
},
stubs,
+ mocks: {
+ $apollo: {
+ queries: {
+ sidebarStatus: {},
+ },
+ },
+ },
});
}
@@ -42,7 +50,7 @@ describe('Alert Details Sidebar', () => {
});
it('open as default', () => {
- expect(wrapper.props('sidebarCollapsed')).toBe(true);
+ expect(wrapper.classes('right-sidebar-expanded')).toBe(true);
});
it('should render side bar assignee dropdown', () => {
diff --git a/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js
index 4c6db8a9ce0..401ce64e859 100644
--- a/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js
@@ -61,6 +61,10 @@ describe('Design discussions component', () => {
...data,
};
},
+ provide: {
+ projectPath: 'project-path',
+ issueIid: '1',
+ },
mocks: {
$apollo,
$route: {
diff --git a/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap
index e55cff8de3d..eaf2ad6955a 100644
--- a/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap
@@ -7,6 +7,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
<a
aria-label="Go back to designs"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
+ data-testid="close-design"
>
<icon-stub
name="close"
diff --git a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap
index 3ba63fd14f0..dafbf037689 100644
--- a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
-<div>
+<div
+ data-testid="designs-root"
+>
<!---->
<div
@@ -73,7 +75,9 @@ exports[`Design management index page designs does not render toolbar when there
`;
exports[`Design management index page designs renders designs list and header with upload button 1`] = `
-<div>
+<div
+ data-testid="designs-root"
+>
<header
class="row-content-block border-top-0 p-2 d-flex"
>
@@ -188,7 +192,9 @@ exports[`Design management index page designs renders designs list and header wi
`;
exports[`Design management index page designs renders error 1`] = `
-<div>
+<div
+ data-testid="designs-root"
+>
<!---->
<div
@@ -216,7 +222,9 @@ exports[`Design management index page designs renders error 1`] = `
`;
exports[`Design management index page designs renders loading icon 1`] = `
-<div>
+<div
+ data-testid="designs-root"
+>
<!---->
<div
@@ -236,7 +244,9 @@ exports[`Design management index page designs renders loading icon 1`] = `
`;
exports[`Design management index page when has no designs renders empty text 1`] = `
-<div>
+<div
+ data-testid="designs-root"
+>
<!---->
<div
diff --git a/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap
index 65c4811536e..83bcebd513e 100644
--- a/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Design management design index page renders design index 1`] = `
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- projectpath=""
+ project-path="project-path"
/>
<!---->
@@ -60,7 +60,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
+ markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
@@ -108,7 +108,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
+ markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
</gl-collapse-stub>
@@ -140,7 +140,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- projectpath=""
+ project-path="project-path"
/>
<div
diff --git a/spec/frontend/design_management_new/pages/design/index_spec.js b/spec/frontend/design_management_new/pages/design/index_spec.js
index cedfccfa342..3822b0b3b71 100644
--- a/spec/frontend/design_management_new/pages/design/index_spec.js
+++ b/spec/frontend/design_management_new/pages/design/index_spec.js
@@ -95,9 +95,12 @@ describe('Design management design index page', () => {
DesignSidebar,
DesignReplyForm,
},
+ provide: {
+ issueIid: '1',
+ projectPath: 'project-path',
+ },
data() {
return {
- issueIid: '1',
activeDiscussion: {
id: null,
source: null,
@@ -149,7 +152,7 @@ describe('Design management design index page', () => {
expect(findSidebar().props()).toEqual({
design,
- markdownPreviewPath: '//preview_markdown?target_type=Issue',
+ markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
resolvedDiscussionsExpanded: false,
});
});
diff --git a/spec/frontend/design_management_new/pages/index_spec.js b/spec/frontend/design_management_new/pages/index_spec.js
index bab9b501bf1..7f812b0d413 100644
--- a/spec/frontend/design_management_new/pages/index_spec.js
+++ b/spec/frontend/design_management_new/pages/index_spec.js
@@ -92,19 +92,23 @@ describe('Design management index page', () => {
};
wrapper = shallowMount(Index, {
+ data() {
+ return {
+ designs,
+ allVersions,
+ permissions: {
+ createDesign,
+ },
+ };
+ },
mocks: { $apollo },
localVue,
router,
stubs: { DesignDestroyer, ApolloMutation, ...stubs },
attachToDocument: true,
- });
-
- wrapper.setData({
- designs,
- allVersions,
- issueIid: '1',
- permissions: {
- createDesign,
+ provide: {
+ projectPath: 'project-path',
+ issueIid: '1',
},
});
}
@@ -117,9 +121,7 @@ describe('Design management index page', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
it('renders error', () => {
@@ -135,25 +137,19 @@ describe('Design management index page', () => {
it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- return wrapper.vm.$nextTick().then(() => {
- expect(findToolbar().exists()).toBe(true);
- });
+ expect(findToolbar().exists()).toBe(true);
});
it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
it('does not render toolbar when there is no permission', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
});
@@ -185,7 +181,7 @@ describe('Design management index page', () => {
mutation: uploadDesignQuery,
variables: {
files: [{ name: 'test' }],
- projectPath: '',
+ projectPath: 'project-path',
iid: '1',
},
optimisticResponse: {
@@ -442,9 +438,9 @@ describe('Design management index page', () => {
});
});
- it('on latest version when has no designs does not render toolbar buttons', () => {
+ it('on latest version when has no designs toolbar buttons are invisible', () => {
createComponent({ designs: [], allVersions: [mockVersion] });
- expect(findToolbar().exists()).toBe(false);
+ expect(findToolbar().classes()).toContain('d-none');
});
describe('on non-latest version', () => {
@@ -535,7 +531,7 @@ describe('Design management index page', () => {
it('ensures fullscreen layout is not applied', () => {
createComponent(true);
- wrapper.vm.$router.push('/designs');
+ wrapper.vm.$router.push('/');
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});
diff --git a/spec/frontend/design_management_new/router_spec.js b/spec/frontend/design_management_new/router_spec.js
index 972af86195f..4d63e622724 100644
--- a/spec/frontend/design_management_new/router_spec.js
+++ b/spec/frontend/design_management_new/router_spec.js
@@ -5,11 +5,7 @@ import App from '~/design_management_new/components/app.vue';
import Designs from '~/design_management_new/pages/index.vue';
import DesignDetail from '~/design_management_new/pages/design/index.vue';
import createRouter from '~/design_management_new/router';
-import {
- ROOT_ROUTE_NAME,
- DESIGNS_ROUTE_NAME,
- DESIGN_ROUTE_NAME,
-} from '~/design_management_new/router/constants';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management_new/router/constants';
import '~/commons/bootstrap';
function factory(routeArg) {
@@ -49,7 +45,7 @@ describe('Design management router', () => {
window.location.hash = '';
});
- describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => {
+ describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', routeArg => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
@@ -57,14 +53,6 @@ describe('Design management router', () => {
});
});
- describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => {
- it('pushes designs root component', () => {
- const wrapper = factory(routeArg);
-
- expect(wrapper.find(Designs).exists()).toBe(true);
- });
- });
-
describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])(
'designs detail route',
routeArg => {
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index 7bb760be3a8..7dea6d819b9 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -84,20 +84,19 @@ describe('Pipeline DAG graph wrapper', () => {
describe('when there is a dataUrl', () => {
describe('but the data fetch fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(500);
createComponent({ graphUrl: dataPath });
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('shows the LOAD_FAILURE alert and not the graph', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE));
- expect(getGraph().exists()).toBe(false);
- });
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE));
+ expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
@@ -106,20 +105,19 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('the data fetch succeeds but the parse fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, unparseableGraph);
createComponent({ graphUrl: dataPath });
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('shows the PARSE_FAILURE alert and not the graph', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(getErrorText(PARSE_FAILURE));
- expect(getGraph().exists()).toBe(false);
- });
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe(getErrorText(PARSE_FAILURE));
+ expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
@@ -128,133 +126,103 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('and the data fetch and parse succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, mockBaseData);
createComponent({ graphUrl: dataPath }, mount);
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
- it('shows the graph and not the beta alert', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getAllAlerts().length).toBe(1);
- expect(getAlert().text()).toContain('This feature is currently in beta.');
- expect(getGraph().exists()).toBe(true);
- });
+ it('shows the graph and the beta alert', () => {
+ expect(getAllAlerts().length).toBe(1);
+ expect(getAlert().text()).toContain('This feature is currently in beta.');
+ expect(getGraph().exists()).toBe(true);
});
it('does not render the empty state', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getEmptyState().exists()).toBe(false);
- });
+ expect(getEmptyState().exists()).toBe(false);
});
});
describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, tooSmallGraph);
createComponent({ graphUrl: dataPath });
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA));
- expect(getGraph().exists()).toBe(false);
- });
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA));
+ expect(getGraph().exists()).toBe(false);
});
it('does not show the empty dag graph state', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getEmptyState().exists()).toBe(false);
- });
+ expect(getEmptyState().exists()).toBe(false);
});
});
- describe('the data fetch and parse succeeds, but the resulting graph is empty', () => {
- beforeEach(() => {
+ describe('the data fetch succeeds but the returned data is empty', () => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies);
createComponent({ graphUrl: dataPath }, mount);
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('does not render an error alert or the graph', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getAllAlerts().length).toBe(1);
- expect(getAlert().text()).toContain('This feature is currently in beta.');
- expect(getGraph().exists()).toBe(false);
- });
+ expect(getAllAlerts().length).toBe(1);
+ expect(getAlert().text()).toContain('This feature is currently in beta.');
+ expect(getGraph().exists()).toBe(false);
});
it('shows the empty dag graph state', () => {
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(getEmptyState().exists()).toBe(true);
- });
+ expect(getEmptyState().exists()).toBe(true);
});
});
});
describe('annotations', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, mockBaseData);
createComponent({ graphUrl: dataPath }, mount);
+
+ await wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
- it('toggles on link mouseover and mouseout', () => {
+ it('toggles on link mouseover and mouseout', async () => {
const currentNote = singleNote['dag-link103'];
expect(getNotes().exists()).toBe(false);
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(getNotes().exists()).toBe(true);
- getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(getNotes().exists()).toBe(false);
- });
+ getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote });
+ await wrapper.vm.$nextTick();
+ expect(getNotes().exists()).toBe(true);
+
+ getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote });
+ await wrapper.vm.$nextTick();
+ expect(getNotes().exists()).toBe(false);
});
- it('toggles on node and link click', () => {
+ it('toggles on node and link click', async () => {
expect(getNotes().exists()).toBe(false);
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(getNotes().exists()).toBe(true);
- getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(getNotes().exists()).toBe(false);
- });
+ getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote });
+ await wrapper.vm.$nextTick();
+ expect(getNotes().exists()).toBe(true);
+
+ getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} });
+ await wrapper.vm.$nextTick();
+ expect(getNotes().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
new file mode 100644
index 00000000000..69a50899d4d
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
@@ -0,0 +1,65 @@
+import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
+
+describe('MrWidgetExpanableSection', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.find(GlButton);
+ const findCollapse = () => wrapper.find(GlCollapse);
+
+ beforeEach(() => {
+ wrapper = shallowMount(MrCollapsibleSection, {
+ slots: {
+ content: '<span>Collapsable Content</span>',
+ header: '<span>Header Content</span>',
+ },
+ });
+ });
+
+ it('renders Icon', () => {
+ expect(wrapper.contains(GlIcon)).toBe(true);
+ });
+
+ it('renders header slot', () => {
+ expect(wrapper.text()).toContain('Header Content');
+ });
+
+ it('renders content slot', () => {
+ expect(wrapper.text()).toContain('Collapsable Content');
+ });
+
+ describe('when collapse section is closed', () => {
+ it('renders button with expand text', () => {
+ expect(findButton().text()).toBe('Expand');
+ });
+
+ it('renders a collpased section with no visibility', () => {
+ const collapse = findCollapse();
+
+ expect(collapse.exists()).toBe(true);
+ expect(collapse.attributes('visible')).toBeUndefined();
+ });
+ });
+
+ describe('when collapse section is open', () => {
+ beforeEach(() => {
+ findButton().vm.$emit('click');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders button with collapse text', () => {
+ const button = findButton();
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Collapse');
+ });
+
+ it('renders a collpased section with visible content', () => {
+ const collapse = findCollapse();
+
+ expect(collapse.exists()).toBe(true);
+ expect(collapse.attributes('visible')).toBe('true');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
index 3373defdc47..ae280146c22 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
@@ -1,21 +1,31 @@
-export const invalidPlan = {};
+export const invalidPlanWithName = {
+ job_name: 'Invalid Plan',
+ job_path: '/path/to/ci/logs/1',
+ tf_report_error: 'api_error',
+};
+
+export const invalidPlanWithoutName = {
+ tf_report_error: 'invalid_json_format',
+};
+
+export const validPlanWithName = {
+ create: 10,
+ update: 20,
+ delete: 30,
+ job_name: 'Valid Plan',
+ job_path: '/path/to/ci/logs/1',
+};
-export const validPlan = {
+export const validPlanWithoutName = {
create: 10,
update: 20,
delete: 30,
- job_name: 'Plan Changes',
job_path: '/path/to/ci/logs/1',
};
export const plans = {
- '1': validPlan,
- '2': invalidPlan,
- '3': {
- create: 1,
- update: 2,
- delete: 3,
- job_name: 'Plan 3',
- job_path: '/path/to/ci/logs/3',
- },
+ invalid_plan_one: invalidPlanWithName,
+ invalid_plan_two: invalidPlanWithName,
+ valid_plan_one: validPlanWithName,
+ valid_plan_two: validPlanWithoutName,
};
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index 52c1850deb1..be43f10c03e 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,8 +1,9 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
-import { plans } from './mock_data';
+import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
import MrWidgetTerraformContainer from '~/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue';
import Poll from '~/lib/utils/poll';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
@@ -13,6 +14,7 @@ describe('MrWidgetTerraformConainer', () => {
const propsData = { endpoint: '/path/to/terraform/report.json' };
+ const findHeader = () => wrapper.find('[data-testid="terraform-header-text"]');
const findPlans = () => wrapper.findAll(TerraformPlan).wrappers.map(x => x.props('plan'));
const mockPollingApi = (response, body, header) => {
@@ -20,7 +22,10 @@ describe('MrWidgetTerraformConainer', () => {
};
const mountWrapper = () => {
- wrapper = shallowMount(MrWidgetTerraformContainer, { propsData });
+ wrapper = shallowMount(MrWidgetTerraformContainer, {
+ propsData,
+ stubs: { MrWidgetExpanableSection, GlSprintf },
+ });
return axios.waitForAll();
};
@@ -44,9 +49,76 @@ describe('MrWidgetTerraformConainer', () => {
});
it('diplays loading skeleton', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.contains(GlSkeletonLoading)).toBe(true);
+ expect(wrapper.contains(MrWidgetExpanableSection)).toBe(false);
+ });
+ });
+
+ describe('when data has finished loading', () => {
+ beforeEach(() => {
+ mockPollingApi(200, plans, {});
+ return mountWrapper();
+ });
+
+ it('displays terraform content', () => {
+ expect(wrapper.contains(GlSkeletonLoading)).toBe(false);
+ expect(wrapper.contains(MrWidgetExpanableSection)).toBe(true);
+ expect(findPlans()).toEqual(Object.values(plans));
+ });
+
+ describe('when data includes one invalid plan', () => {
+ beforeEach(() => {
+ const invalidPlanGroup = { bad_plan: invalidPlanWithName };
+ mockPollingApi(200, invalidPlanGroup, {});
+ return mountWrapper();
+ });
- expect(findPlans()).toEqual([]);
+ it('displays header text for one invalid plan', () => {
+ expect(findHeader().text()).toBe('1 Terraform report failed to generate');
+ });
+ });
+
+ describe('when data includes multiple invalid plans', () => {
+ beforeEach(() => {
+ const invalidPlanGroup = {
+ bad_plan_one: invalidPlanWithName,
+ bad_plan_two: invalidPlanWithName,
+ };
+
+ mockPollingApi(200, invalidPlanGroup, {});
+ return mountWrapper();
+ });
+
+ it('displays header text for multiple invalid plans', () => {
+ expect(findHeader().text()).toBe('2 Terraform reports failed to generate');
+ });
+ });
+
+ describe('when data includes one valid plan', () => {
+ beforeEach(() => {
+ const validPlanGroup = { valid_plan: validPlanWithName };
+ mockPollingApi(200, validPlanGroup, {});
+ return mountWrapper();
+ });
+
+ it('displays header text for one valid plans', () => {
+ expect(findHeader().text()).toBe('1 Terraform report was generated in your pipelines');
+ });
+ });
+
+ describe('when data includes multiple valid plans', () => {
+ beforeEach(() => {
+ const validPlanGroup = {
+ valid_plan_one: validPlanWithName,
+ valid_plan_two: validPlanWithName,
+ };
+ mockPollingApi(200, validPlanGroup, {});
+ return mountWrapper();
+ });
+
+ it('displays header text for multiple valid plans', () => {
+ expect(findHeader().text()).toBe('2 Terraform reports were generated in your pipelines');
+ });
});
});
@@ -71,12 +143,6 @@ describe('MrWidgetTerraformConainer', () => {
return mountWrapper();
});
- it('diplays terraform components and stops loading', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
-
- expect(findPlans()).toEqual(Object.values(plans));
- });
-
it('does not make additional requests after poll is successful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
expect(pollStop).toHaveBeenCalledTimes(1);
@@ -90,11 +156,11 @@ describe('MrWidgetTerraformConainer', () => {
});
it('stops loading', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.contains(GlSkeletonLoading)).toBe(false);
});
it('generates one broken plan', () => {
- expect(findPlans()).toEqual([{}]);
+ expect(findPlans()).toEqual([{ tf_report_error: 'api_error' }]);
});
it('does not make additional requests after poll is unsuccessful', () => {
diff --git a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
index b2c72581b42..cc68ba0d9df 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
@@ -1,12 +1,18 @@
-import { invalidPlan, validPlan } from './mock_data';
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
+import {
+ invalidPlanWithName,
+ invalidPlanWithoutName,
+ validPlanWithName,
+ validPlanWithoutName,
+} from './mock_data';
describe('TerraformPlan', () => {
let wrapper;
- const findLogButton = () => wrapper.find('.js-terraform-report-link');
+ const findIcon = () => wrapper.find('[data-testid="change-type-icon"]');
+ const findLogButton = () => wrapper.find('[data-testid="terraform-report-link"]');
const mountWrapper = propsData => {
wrapper = shallowMount(TerraformPlan, { stubs: { GlLink, GlSprintf }, propsData });
@@ -16,20 +22,24 @@ describe('TerraformPlan', () => {
wrapper.destroy();
});
- describe('validPlan', () => {
+ describe('valid plan with job_name', () => {
beforeEach(() => {
- mountWrapper({ plan: validPlan });
+ mountWrapper({ plan: validPlanWithName });
});
- it('diplays the plan job_name', () => {
+ it('displays a document icon', () => {
+ expect(findIcon().attributes('name')).toBe('doc-changes');
+ });
+
+ it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
- `The Terraform report ${validPlan.job_name} was generated in your pipelines.`,
+ `The Terraform report ${validPlanWithName.job_name} was generated in your pipelines.`,
);
});
it('diplays the reported changes', () => {
expect(wrapper.text()).toContain(
- `Reported Resource Changes: ${validPlan.create} to add, ${validPlan.update} to change, ${validPlan.delete} to delete`,
+ `Reported Resource Changes: ${validPlanWithName.create} to add, ${validPlanWithName.update} to change, ${validPlanWithName.delete} to delete`,
);
});
@@ -39,18 +49,44 @@ describe('TerraformPlan', () => {
});
});
- describe('invalidPlan', () => {
+ describe('valid plan without job_name', () => {
beforeEach(() => {
- mountWrapper({ plan: invalidPlan });
+ mountWrapper({ plan: validPlanWithoutName });
});
- it('diplays generic header since job_name is missing', () => {
+ it('diplays the header text without a name', () => {
expect(wrapper.text()).toContain('A Terraform report was generated in your pipelines.');
});
+ });
+
+ describe('invalid plan with job_name', () => {
+ beforeEach(() => {
+ mountWrapper({ plan: invalidPlanWithName });
+ });
+
+ it('displays a warning icon', () => {
+ expect(findIcon().attributes('name')).toBe('warning');
+ });
+
+ it('diplays the header text with a name', () => {
+ expect(wrapper.text()).toContain(
+ `The Terraform report ${invalidPlanWithName.job_name} failed to generate.`,
+ );
+ });
it('diplays generic error since report values are missing', () => {
expect(wrapper.text()).toContain('Generating the report caused an error.');
});
+ });
+
+ describe('invalid plan with out job_name', () => {
+ beforeEach(() => {
+ mountWrapper({ plan: invalidPlanWithoutName });
+ });
+
+ it('diplays the header text without a name', () => {
+ expect(wrapper.text()).toContain('A Terraform report failed to generate.');
+ });
it('does not render button because url is missing', () => {
expect(findLogButton().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
new file mode 100644
index 00000000000..6e2bf21b692
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -0,0 +1,71 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlTabs } from '@gitlab/ui';
+import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
+import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants';
+
+describe('Add Image Modal', () => {
+ let wrapper;
+
+ const findModal = () => wrapper.find(GlModal);
+ const findTabs = () => wrapper.find(GlTabs);
+ const findUploadImageTab = () => wrapper.find(UploadImageTab);
+ const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
+ const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
+
+ beforeEach(() => {
+ wrapper = shallowMount(AddImageModal, { provide: { glFeatures: { sseImageUploads: true } } });
+ });
+
+ describe('when content is loaded', () => {
+ it('renders a modal component', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('renders a Tabs component', () => {
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('renders an upload image tab', () => {
+ expect(findUploadImageTab().exists()).toBe(true);
+ });
+
+ it('renders an input to add an image URL', () => {
+ expect(findUrlInput().exists()).toBe(true);
+ });
+
+ it('renders an input to add an image description', () => {
+ expect(findDescriptionInput().exists()).toBe(true);
+ });
+ });
+
+ describe('add image', () => {
+ describe('Upload', () => {
+ it('validates the file', () => {
+ const preventDefault = jest.fn();
+ const description = 'some description';
+
+ wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() };
+ wrapper.setData({ description, tabIndex: IMAGE_TABS.UPLOAD_TAB });
+
+ findModal().vm.$emit('ok', { preventDefault });
+
+ expect(wrapper.vm.$refs.uploadImageTab.validateFile).toHaveBeenCalled();
+ });
+ });
+
+ describe('URL', () => {
+ it('emits an addImage event when a valid URL is specified', () => {
+ const preventDefault = jest.fn();
+ const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' };
+ wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB });
+
+ findModal().vm.$emit('ok', { preventDefault });
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(wrapper.emitted('addImage')).toEqual([
+ [{ imageUrl: mockImage.imageUrl, altText: mockImage.description }],
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js
new file mode 100644
index 00000000000..ded490b2568
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+
+describe('Upload Image Tab', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(UploadImageTab);
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ const triggerInputEvent = size => {
+ const file = { size, name: 'file-name.png' };
+ const mockEvent = new Event('input');
+
+ Object.defineProperty(mockEvent, 'target', { value: { files: [file] } });
+
+ wrapper.find({ ref: 'fileInput' }).element.dispatchEvent(mockEvent);
+
+ return file;
+ };
+
+ describe('onInput', () => {
+ it.each`
+ size | fileError
+ ${2000000000} | ${'Maximum file size is 2MB. Please select a smaller file.'}
+ ${200} | ${null}
+ `('validates the file correctly', ({ size, fileError }) => {
+ triggerInputEvent(size);
+
+ expect(wrapper.vm.fileError).toBe(fileError);
+ });
+ });
+
+ it('emits input event when file is valid', () => {
+ const file = triggerInputEvent(200);
+
+ expect(wrapper.emitted('input')).toEqual([[file]]);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
deleted file mode 100644
index 4889bc8538d..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
-
-describe('Add Image Modal', () => {
- let wrapper;
-
- const findModal = () => wrapper.find(GlModal);
- const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
- const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
-
- beforeEach(() => {
- wrapper = shallowMount(AddImageModal);
- });
-
- describe('when content is loaded', () => {
- it('renders a modal component', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('renders an input to add an image URL', () => {
- expect(findUrlInput().exists()).toBe(true);
- });
-
- it('renders an input to add an image description', () => {
- expect(findDescriptionInput().exists()).toBe(true);
- });
- });
-
- describe('add image', () => {
- it('emits an addImage event when a valid URL is specified', () => {
- const preventDefault = jest.fn();
- const mockImage = { imageUrl: '/some/valid/url.png', altText: 'some description' };
- wrapper.setData({ ...mockImage });
-
- findModal().vm.$emit('ok', { preventDefault });
- expect(preventDefault).not.toHaveBeenCalled();
- expect(wrapper.emitted('addImage')).toEqual([[mockImage]]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index 94f9764ad91..18e768a76aa 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
+import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
@@ -119,7 +119,7 @@ describe('Rich Content Editor', () => {
});
it('calls the onAddImage method when the addImage event is emitted', () => {
- const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
+ const mockImage = { imageUrl: 'some/url.png', altText: 'some description' };
const mockInstance = { exec: jest.fn() };
wrapper.vm.$refs.editor = mockInstance;
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 1ccce1f2319..45ac673986d 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
assignees
notes
discussions
+ metrics_dashboard_url
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 5e0e2c2b464..0d112bfdb2a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -533,7 +533,6 @@ Project:
- merge_requests_enabled
- wiki_enabled
- snippets_enabled
-- requirements_enabled
- visibility_level
- archived
- created_at
@@ -601,7 +600,6 @@ ProjectFeature:
- repository_access_level
- pages_access_level
- metrics_dashboard_access_level
-- requirements_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/lib/quality/helm3_client_spec.rb b/spec/lib/quality/helm3_client_spec.rb
index 1144ee9369d..a579540e09d 100644
--- a/spec/lib/quality/helm3_client_spec.rb
+++ b/spec/lib/quality/helm3_client_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
RSpec.describe Quality::Helm3Client do
- let(:namespace) { 'review-apps-ee' }
+ let(:namespace) { 'review-apps' }
let(:release_name) { 'my-release' }
let(:raw_helm_list_page1) do
<<~OUTPUT
diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb
index 1cfee5200f3..93b74ff6544 100644
--- a/spec/lib/quality/kubernetes_client_spec.rb
+++ b/spec/lib/quality/kubernetes_client_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
RSpec.describe Quality::KubernetesClient do
- let(:namespace) { 'review-apps-ee' }
+ let(:namespace) { 'review-apps' }
let(:release_name) { 'my-release' }
let(:pod_for_release) { "pod-my-release-abcd" }
let(:raw_resource_names_str) { "NAME\nfoo\n#{pod_for_release}\nbar" }
diff --git a/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb b/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
new file mode 100644
index 00000000000..f2698a0f352
--- /dev/null
+++ b/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200526115436_dedup_mr_metrics')
+
+RSpec.describe DedupMrMetrics, :migration, schema: 20200526013844 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:metrics) { table(:merge_request_metrics) }
+ let(:merge_request_params) { { source_branch: 'x', target_branch: 'y', target_project_id: project.id } }
+
+ let!(:namespace) { namespaces.create(name: 'foo', path: 'foo') }
+ let!(:project) { projects.create!(namespace_id: namespace.id) }
+ let!(:merge_request_1) { merge_requests.create!(merge_request_params) }
+ let!(:merge_request_2) { merge_requests.create!(merge_request_params) }
+ let!(:merge_request_3) { merge_requests.create!(merge_request_params) }
+
+ let!(:duplicated_metrics_1) { metrics.create(merge_request_id: merge_request_1.id, latest_build_started_at: 1.day.ago, first_deployed_to_production_at: 5.days.ago, updated_at: 2.months.ago) }
+ let!(:duplicated_metrics_2) { metrics.create(merge_request_id: merge_request_1.id, latest_build_started_at: Time.now, merged_at: Time.now, updated_at: 1.month.ago) }
+
+ let!(:duplicated_metrics_3) { metrics.create(merge_request_id: merge_request_3.id, diff_size: 30, commits_count: 20, updated_at: 2.months.ago) }
+ let!(:duplicated_metrics_4) { metrics.create(merge_request_id: merge_request_3.id, added_lines: 5, commits_count: nil, updated_at: 1.month.ago) }
+
+ let!(:non_duplicated_metrics) { metrics.create(merge_request_id: merge_request_2.id, latest_build_started_at: 2.days.ago) }
+
+ it 'deduplicates merge_request_metrics table' do
+ expect { migrate! }.to change { metrics.count }.from(5).to(3)
+ end
+
+ it 'merges `duplicated_metrics_1` with `duplicated_metrics_2`' do
+ migrate!
+
+ expect(metrics.where(id: duplicated_metrics_1.id)).not_to exist
+
+ merged_metrics = metrics.find_by(id: duplicated_metrics_2.id)
+
+ expect(merged_metrics).to be_present
+ expect(merged_metrics.latest_build_started_at).to be_like_time(duplicated_metrics_2.latest_build_started_at)
+ expect(merged_metrics.merged_at).to be_like_time(duplicated_metrics_2.merged_at)
+ expect(merged_metrics.first_deployed_to_production_at).to be_like_time(duplicated_metrics_1.first_deployed_to_production_at)
+ end
+
+ it 'merges `duplicated_metrics_3` with `duplicated_metrics_4`' do
+ migrate!
+
+ expect(metrics.where(id: duplicated_metrics_3.id)).not_to exist
+
+ merged_metrics = metrics.find_by(id: duplicated_metrics_4.id)
+
+ expect(merged_metrics).to be_present
+ expect(merged_metrics.diff_size).to eq(duplicated_metrics_3.diff_size)
+ expect(merged_metrics.commits_count).to eq(duplicated_metrics_3.commits_count)
+ expect(merged_metrics.added_lines).to eq(duplicated_metrics_4.added_lines)
+ end
+
+ it 'does not change non duplicated records' do
+ expect { migrate! }.not_to change { non_duplicated_metrics.reload.attributes }
+ end
+
+ it 'does nothing when there are no metrics' do
+ metrics.delete_all
+
+ migrate!
+
+ expect(metrics.count).to eq(0)
+ end
+end
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
index 9ea93c178a6..62123ffa542 100644
--- a/spec/models/clusters/applications/elastic_stack_spec.rb
+++ b/spec/models/clusters/applications/elastic_stack_spec.rb
@@ -27,6 +27,20 @@ RSpec.describe Clusters::Applications::ElasticStack do
expect(subject.preinstall).to be_empty
end
+ context 'within values.yaml' do
+ let(:values_yaml_content) {subject.files[:"values.yaml"]}
+
+ it 'contains the disabled index lifecycle management' do
+ expect(values_yaml_content).to include "setup.ilm.enabled: false"
+ end
+
+ it 'contains daily indices with respective template' do
+ expect(values_yaml_content).to include "index: \"filebeat-%{[agent.version]}-%{+yyyy.MM.dd}\""
+ expect(values_yaml_content).to include "setup.template.name: 'filebeat'"
+ expect(values_yaml_content).to include "setup.template.pattern: 'filebeat-*'"
+ end
+ end
+
context 'on a non rbac enabled cluster' do
before do
elastic_stack.cluster.platform_kubernetes.abac!
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3e743da41e3..69548bb7ab4 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -280,6 +280,21 @@ RSpec.describe MergeRequest do
expect(MergeRequest::Metrics.count).to eq(1)
end
+
+ it 'does not create duplicated metrics records when MR is concurrently updated' do
+ merge_request = create(:merge_request)
+
+ merge_request.metrics.destroy
+
+ instance1 = MergeRequest.find(merge_request.id)
+ instance2 = MergeRequest.find(merge_request.id)
+
+ instance1.ensure_metrics
+ instance2.ensure_metrics
+
+ metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
+ expect(metrics_records.size).to eq(1)
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 5997a3d2497..4589fb055a1 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -80,7 +80,6 @@ RSpec.describe ProjectPolicy do
let(:additional_guest_permissions) { [] }
let(:additional_reporter_permissions) { [] }
let(:additional_maintainer_permissions) { [] }
- let(:additional_owner_permissions) { [] }
let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb
index 6c20404ffca..9a92048c487 100644
--- a/spec/presenters/alert_management/alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/alert_presenter_spec.rb
@@ -38,4 +38,10 @@ RSpec.describe AlertManagement::AlertPresenter do
)
end
end
+
+ describe '#metrics_dashboard_url' do
+ it 'is not defined' do
+ expect(presenter.metrics_dashboard_url).to be_nil
+ end
+ end
end
diff --git a/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb b/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
index 2d42b718337..4e6683ee68e 100644
--- a/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
@@ -45,4 +45,10 @@ RSpec.describe AlertManagement::PrometheusAlertPresenter do
)
end
end
+
+ describe '#metrics_dashboard_url' do
+ it 'is not defined' do
+ expect(presenter.metrics_dashboard_url).to be_nil
+ end
+ end
end
diff --git a/spec/presenters/projects/prometheus/alert_presenter_spec.rb b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
index e558c651734..e8bcbb4378f 100644
--- a/spec/presenters/projects/prometheus/alert_presenter_spec.rb
+++ b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::Prometheus::AlertPresenter do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:project, reload: true) { create(:project) }
let(:presenter) { described_class.new(alert) }
@@ -14,7 +16,39 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
let(:metric_id) { gitlab_alert.prometheus_metric_id }
let(:alert) do
- create(:alerting_alert, project: project, metric_id: metric_id)
+ create(:alerting_alert, project: project, metric_id: metric_id, payload: payload)
+ end
+ end
+
+ shared_context 'self-managed prometheus alert with metrics data' do
+ let!(:environment) { create(:environment, project: project, name: 'production') }
+
+ let(:title) { 'title' }
+ let(:y_label) { 'y_label' }
+ let(:query) { 'avg(metric) > 1.0' }
+ let(:embed_content) do
+ {
+ panel_groups: [{
+ panels: [{
+ type: 'line-graph',
+ title: title,
+ y_label: y_label,
+ metrics: [{ query_range: query }]
+ }]
+ }]
+ }
+ end
+
+ before do
+ payload['startsAt'] = starts_at
+ payload['generatorURL'] = "http://host?g0.expr=#{CGI.escape(query)}"
+
+ payload['labels'] ||= {}
+ payload['labels']['gitlab_environment_name'] = 'production'
+
+ payload['annotations'] ||= {}
+ payload['annotations']['title'] = 'title'
+ payload['annotations']['gitlab_y_label'] = 'y_label'
end
end
@@ -171,7 +205,7 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
**Start time:** #{presenter.start_time}#{markdown_line_break}
**full_query:** `avg(metric) > 1.0`
- [](#{url})
+ [](#{presenter.metrics_dashboard_url})
MARKDOWN
end
@@ -193,55 +227,17 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
end
context 'for gitlab-managed prometheus alerts' do
- let(:gitlab_alert) { create(:prometheus_alert, project: project) }
- let(:metric_id) { gitlab_alert.prometheus_metric_id }
- let(:env_id) { gitlab_alert.environment_id }
+ include_context 'gitlab alert'
before do
payload['labels'] = { 'gitlab_alert_id' => metric_id }
end
- let(:url) { "http://localhost/#{project.full_path}/prometheus/alerts/#{metric_id}/metrics_dashboard?end=2018-03-12T09%3A36%3A00Z&environment_id=#{env_id}&start=2018-03-12T08%3A36%3A00Z" }
-
it_behaves_like 'markdown with metrics embed'
end
context 'for alerts from a self-managed prometheus' do
- let!(:environment) { create(:environment, project: project, name: 'production') }
- let(:url) { "http://localhost/#{project.full_path}/-/environments/#{environment.id}/metrics_dashboard?embed_json=#{CGI.escape(embed_content.to_json)}&end=2018-03-12T09%3A36%3A00Z&start=2018-03-12T08%3A36%3A00Z" }
-
- let(:title) { 'title' }
- let(:y_label) { 'y_label' }
- let(:query) { 'avg(metric) > 1.0' }
- let(:embed_content) do
- {
- panel_groups: [{
- panels: [{
- type: 'line-graph',
- title: title,
- y_label: y_label,
- metrics: [{ query_range: query }]
- }]
- }]
- }
- end
-
- before do
- # Setup embed time range
- payload['startsAt'] = starts_at
-
- # Setup query
- payload['generatorURL'] = "http://host?g0.expr=#{CGI.escape(query)}"
-
- # Setup environment
- payload['labels'] ||= {}
- payload['labels']['gitlab_environment_name'] = 'production'
-
- # Setup chart title & axis labels
- payload['annotations'] ||= {}
- payload['annotations']['title'] = 'title'
- payload['annotations']['gitlab_y_label'] = 'y_label'
- end
+ include_context 'self-managed prometheus alert with metrics data'
it_behaves_like 'markdown with metrics embed'
@@ -359,10 +355,7 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
end
describe '#performance_dashboard_link' do
- let(:expected_link) do
- Gitlab::Routing.url_helpers
- .metrics_project_environment_url(project, alert.environment)
- end
+ let(:expected_link) { metrics_project_environment_url(project, alert.environment) }
subject { presenter.performance_dashboard_link }
@@ -370,15 +363,34 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
end
describe '#incident_issues_link' do
- let(:expected_link) do
- Gitlab::Routing.url_helpers
- .project_issues_url(project, label_name: described_class::INCIDENT_LABEL_NAME)
- end
+ let(:expected_link) { project_issues_url(project, label_name: described_class::INCIDENT_LABEL_NAME) }
subject { presenter.incident_issues_link }
it { is_expected.to eq(expected_link) }
end
+
+ describe '#metrics_dashboard_url' do
+ let(:starts_at) { '2018-03-12T09:06:00Z' }
+ let(:expected_url) do
+ metrics_dashboard_project_prometheus_alert_url(
+ project,
+ metric_id,
+ environment_id: gitlab_alert.environment_id,
+ embedded: true,
+ end: '2018-03-12T09:36:00Z',
+ start: '2018-03-12T08:36:00Z'
+ )
+ end
+
+ subject { presenter.metrics_dashboard_url }
+
+ before do
+ payload['startsAt'] = starts_at
+ end
+
+ it { is_expected.to eq(expected_url) }
+ end
end
context 'without gitlab alert' do
@@ -413,13 +425,39 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
end
describe '#performance_dashboard_link' do
- let(:expected_link) do
- Gitlab::Routing.url_helpers.metrics_project_environments_url(project)
- end
+ let(:expected_link) { metrics_project_environments_url(project) }
subject { presenter.performance_dashboard_link }
it { is_expected.to eq(expected_link) }
end
+
+ describe '#metrics_dashboard_url' do
+ subject { presenter.metrics_dashboard_url }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'with self-managed prometheus alert with metrics data' do
+ include_context 'self-managed prometheus alert with metrics data'
+
+ describe '#metrics_dashboard_url' do
+ let(:starts_at) { '2018-03-12T09:06:00Z' }
+ let(:expected_url) do
+ metrics_dashboard_project_environment_url(
+ project,
+ environment,
+ embed_json: embed_content.to_json,
+ embedded: true,
+ end: '2018-03-12T09:36:00Z',
+ start: '2018-03-12T08:36:00Z'
+ )
+ end
+
+ subject { presenter.metrics_dashboard_url }
+
+ it { is_expected.to eq(expected_url) }
+ end
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index 497041f99ce..9896b0b0cf5 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -73,7 +73,8 @@ RSpec.describe 'getting Alert Management Alerts' do
'endedAt' => nil,
'details' => { 'custom.alert' => 'payload' },
'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
+ 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'metricsDashboardUrl' => nil
)
expect(second_alert).to include(
@@ -135,6 +136,28 @@ RSpec.describe 'getting Alert Management Alerts' do
it { expect(alerts.size).to eq(0) }
end
end
+
+ context 'with prometheus payload' do
+ let_it_be(:gitlab_alert) { create(:prometheus_alert, project: project) }
+ let_it_be(:metric_id) { gitlab_alert.prometheus_metric_id }
+ let_it_be(:prometheus_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id }, 'startsAt' => '2018-03-12T09:06:00Z' } }
+ let_it_be(:self_managed_alert) { create(:alert_management_alert, :prometheus, project: project, payload: prometheus_payload) }
+
+ let(:expected_url) do
+ Gitlab::Routing.url_helpers.metrics_dashboard_project_prometheus_alert_url(
+ project,
+ metric_id,
+ environment_id: gitlab_alert.environment_id,
+ start: '2018-03-12T08:36:00Z',
+ end: '2018-03-12T09:36:00Z',
+ embedded: true
+ )
+ end
+
+ it 'includes a metrics dashboard url' do
+ expect(first_alert).to include('metricsDashboardUrl' => expected_url)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 05d14c3cd4d..519bea22501 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -886,4 +886,53 @@ RSpec.describe API::Issues do
include_examples 'time tracking endpoints', 'issue'
end
+
+ describe 'PUT /projects/:id/issues/:issue_iid/reorder' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue1) { create(:issue, project: project, relative_position: 10) }
+ let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) }
+ let_it_be(:issue3) { create(:issue, project: project, relative_position: 30) }
+
+ context 'when user has access' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'with valid params' do
+ it 'reorders issues and returns a successful 200 response' do
+ put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(issue1.reload.relative_position)
+ .to be_between(issue2.reload.relative_position, issue3.reload.relative_position)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns a unprocessable entity 422 response for invalid move ids' do
+ put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: non_existing_record_id }
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+
+ it 'returns a not found 404 response for invalid issue id' do
+ put api("/projects/#{project.id}/issues/#{non_existing_record_iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 403 forbidden' do
+ put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 93f64336cad..bf26be57980 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
job_artifacts_cobertura needs job_artifacts_accessibility
- job_artifacts_requirements].freeze
+ job_artifacts_requirements job_artifacts_coverage_fuzzing].freeze
ignore_accessors =
%i[type lock_version target_url base_tags trace_sections
diff --git a/spec/services/snippets/destroy_service_spec.rb b/spec/services/snippets/destroy_service_spec.rb
index 12423ad2d73..70862e0be17 100644
--- a/spec/services/snippets/destroy_service_spec.rb
+++ b/spec/services/snippets/destroy_service_spec.rb
@@ -105,6 +105,13 @@ RSpec.describe Snippets::DestroyService do
it_behaves_like 'a successful destroy'
it_behaves_like 'deletes the snippet repository'
+
+ it 'schedules a project cache update for snippet_size' do
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(snippet.project_id, [], [:snippets_size])
+
+ subject
+ end
end
context 'when user is not able to admin_project_snippet' do
@@ -122,6 +129,12 @@ RSpec.describe Snippets::DestroyService do
it_behaves_like 'a successful destroy'
it_behaves_like 'deletes the snippet repository'
+
+ it 'does not schedule a project cache update' do
+ expect(ProjectCacheWorker).not_to receive(:perform_async)
+
+ subject
+ end
end
context 'when user is not able to admin_personal_snippet' do
diff --git a/spec/services/snippets/update_statistics_service_spec.rb b/spec/services/snippets/update_statistics_service_spec.rb
new file mode 100644
index 00000000000..b4c4b067c51
--- /dev/null
+++ b/spec/services/snippets/update_statistics_service_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Snippets::UpdateStatisticsService do
+ describe '#execute' do
+ subject { described_class.new(snippet).execute }
+
+ shared_examples 'updates statistics' do
+ it 'returns a successful response' do
+ expect(subject).to be_success
+ end
+
+ it 'expires statistics cache' do
+ expect(snippet.repository).to receive(:expire_statistics_caches)
+
+ subject
+ end
+
+ it 'schedules project cache worker based on type' do
+ if snippet.project_id
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(snippet.project_id, [], [:snippets_size])
+ else
+ expect(ProjectCacheWorker).not_to receive(:perform_async)
+ end
+
+ subject
+ end
+
+ context 'when snippet statistics does not exist' do
+ it 'creates snippet statistics' do
+ snippet.statistics.delete
+ snippet.reload
+
+ expect do
+ subject
+ end.to change(SnippetStatistics, :count).by(1)
+
+ expect(snippet.statistics.commit_count).not_to be_zero
+ expect(snippet.statistics.file_count).not_to be_zero
+ expect(snippet.statistics.repository_size).not_to be_zero
+ end
+ end
+
+ context 'when snippet statistics exists' do
+ it 'updates snippet statistics' do
+ expect(snippet.statistics.commit_count).to be_zero
+ expect(snippet.statistics.file_count).to be_zero
+ expect(snippet.statistics.repository_size).to be_zero
+
+ subject
+
+ expect(snippet.statistics.commit_count).not_to be_zero
+ expect(snippet.statistics.file_count).not_to be_zero
+ expect(snippet.statistics.repository_size).not_to be_zero
+ end
+ end
+
+ context 'when snippet does not have a repository' do
+ it 'returns an error response' do
+ expect(snippet).to receive(:repository_exists?).and_return(false)
+
+ expect(subject).to be_error
+ end
+ end
+ end
+
+ context 'with PersonalSnippet' do
+ let!(:snippet) { create(:personal_snippet, :repository) }
+
+ it_behaves_like 'updates statistics'
+ end
+
+ context 'with ProjectSnippet' do
+ let!(:snippet) { create(:project_snippet, :repository) }
+
+ it_behaves_like 'updates statistics'
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 0b9ace3db87..f64ee4aa2f7 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -428,7 +428,12 @@ RSpec.describe PostReceive do
it 'expires the status cache' do
expect(snippet.repository).to receive(:empty?).and_return(true)
expect(snippet.repository).to receive(:expire_status_cache)
- expect(snippet.repository).to receive(:expire_statistics_caches)
+
+ perform
+ end
+
+ it 'updates snippet statistics' do
+ expect(Snippets::UpdateStatisticsService).to receive(:new).with(snippet).and_call_original
perform
end
diff --git a/vendor/elastic_stack/values.yaml b/vendor/elastic_stack/values.yaml
index 21352dd35e2..a6c9fdd39a4 100644
--- a/vendor/elastic_stack/values.yaml
+++ b/vendor/elastic_stack/values.yaml
@@ -14,8 +14,12 @@ filebeat:
filebeatConfig:
filebeat.yml: |
output.file.enabled: false
+ setup.ilm.enabled: false
+ setup.template.name: 'filebeat'
+ setup.template.pattern: 'filebeat-*'
output.elasticsearch:
hosts: ["http://elastic-stack-elasticsearch-master:9200"]
+ index: "filebeat-%{[agent.version]}-%{+yyyy.MM.dd}"
filebeat.inputs:
- type: container
paths: