diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-30 15:08:48 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-30 15:08:48 +0000 |
commit | 340f15b402eec795fca0e0f29709baef0ecf14a7 (patch) | |
tree | 6a7eac5d394f4002b9e5b0c9da12bc12e59ed59c | |
parent | 1e254d9f5a46a85c9bb6f24da8265a30fd388db4 (diff) | |
download | gitlab-ce-340f15b402eec795fca0e0f29709baef0ecf14a7.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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: @@ -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 "" @@ -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: |