New upstream version 12.5.4

This commit is contained in:
Pirate Praveen 2019-12-26 22:10:19 +05:30
parent 6212feb931
commit 510528bb68
2910 changed files with 92827 additions and 25855 deletions

2
.gitignore vendored
View file

@ -59,6 +59,7 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
/spec/examples.txt
/rails_best_practices_output.html
/tags
/vendor/bundle/*
@ -82,3 +83,4 @@ jsdoc/
**/tmp/rubocop_cache/**
.overcommit.yml
.projections.json
/qa/.rakeTasks

View file

@ -1,6 +1,7 @@
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
stages:
- sync
- prepare
- quick-test
- test
@ -8,7 +9,6 @@ stages:
- review
- qa
- post-test
- notification
- pages
variables:
@ -33,7 +33,6 @@ include:
- local: .gitlab/ci/frontend.gitlab-ci.yml
- local: .gitlab/ci/global.gitlab-ci.yml
- local: .gitlab/ci/memory.gitlab-ci.yml
- local: .gitlab/ci/notifications.gitlab-ci.yml
- local: .gitlab/ci/pages.gitlab-ci.yml
- local: .gitlab/ci/qa.gitlab-ci.yml
- local: .gitlab/ci/reports.gitlab-ci.yml
@ -42,3 +41,4 @@ include:
- local: .gitlab/ci/setup.gitlab-ci.yml
- local: .gitlab/ci/test-metadata.gitlab-ci.yml
- local: .gitlab/ci/yaml.gitlab-ci.yml
- local: .gitlab/ci/releases.gitlab-ci.yml

View file

@ -3,11 +3,12 @@
*.rake @gitlab-org/maintainers/rails-backend
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia @eread @mikelewis
/doc/ @gl-docsteam
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
app/assets/ @gitlab-org/maintainers/frontend
*.scss @annabeldunstone @gitlab-org/maintainers/frontend
/scripts/frontend/ @gitlab-org/maintainers/frontend
# Database maintainers should review changes in `db/`
db/ @gitlab-org/maintainers/database
@ -32,4 +33,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database
/.gitlab/ci/ @gl-quality/eng-prod
Dangerfile @gl-quality/eng-prod
/danger/ @gl-quality/eng-prod
/lib/gitlab/danger/ @gl-quality/eng-prod
/scripts/ @gl-quality/eng-prod

View file

@ -1,4 +1,5 @@
cloud-native-image:
extends: .only:variables-canonical-dot-com
image: ruby:2.6-alpine
dependencies: []
stage: post-test
@ -12,5 +13,3 @@ cloud-native-image:
only:
refs:
- tags
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"

View file

@ -2,12 +2,11 @@
extends:
- .default-tags
- .default-retry
- .only-docs-changes
- .only:variables-canonical-dot-com
- .only:changes-docs
only:
refs:
- merge_requests
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
image: ruby:2.6-alpine
stage: review
dependencies: []
@ -50,7 +49,7 @@ docs lint:
- .default-tags
- .default-retry
- .default-only
- .only-docs-changes
- .only:changes-docs
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-docs-lint"
stage: test
dependencies: []
@ -68,7 +67,7 @@ docs lint:
# Check the internal anchor links
- bundle exec nanoc check internal_anchors
graphql-docs-verify:
graphql-reference-verify:
extends:
- .only-ee
- .default-tags
@ -76,10 +75,10 @@ graphql-docs-verify:
- .default-cache
- .default-only
- .default-before_script
- .only-graphql-changes
variables:
SETUP_DB: "false"
- .only:changes-code-backstage-qa
- .use-pg9
stage: test
needs: ["setup-test-env"]
script:
- bundle exec rake gitlab:graphql:check_docs
- bundle exec rake gitlab:graphql:check_schema

View file

@ -12,7 +12,7 @@
- .default-only
- .default-before_script
- .assets-compile-cache
- .only-code-qa-changes
- .only:changes-code-backstage-qa
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
stage: test
dependencies: ["setup-test-env"]
@ -73,7 +73,7 @@ gitlab:assets:compile pull-cache:
- .default-only
- .default-before_script
- .assets-compile-cache
- .only-code-qa-changes
- .only:changes-code-backstage-qa
- .use-pg9
stage: prepare
script:
@ -128,7 +128,7 @@ compile-assets pull-cache foss:
- .default-cache
- .default-only
- .default-before_script
- .only-code-changes
- .only:changes-code-backstage
- .use-pg9
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
@ -205,7 +205,7 @@ jest-foss:
- .default-retry
- .default-cache
- .default-only
- .only-code-changes
- .only:changes-code-backstage
stage: test
dependencies: []
cache:
@ -238,7 +238,7 @@ webpack-dev-server:
- .default-retry
- .default-cache
- .default-only
- .only-code-changes
- .only:changes-code-backstage
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "compile-assets pull-cache"]

View file

@ -40,14 +40,51 @@
- merge_requests
- tags
.only-code-changes:
.only:variables-canonical-dot-com:
only:
changes:
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ # Matches the gitlab-org group or its subgroups
.only:variables_refs-canonical-dot-com-schedules:
extends: .only:variables-canonical-dot-com
only:
refs:
- schedules
.except:refs-deploy:
except:
refs:
- /^\d+-\d+-auto-deploy-\d+$/
.except:refs-master-tags-stable-deploy:
except:
refs:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /^\d+-\d+-auto-deploy-\d+$/
.only:kubernetes:
only:
kubernetes: active
.only-review:
extends:
- .only:variables-canonical-dot-com
- .only:kubernetes
- .except:refs-master-tags-stable-deploy
.only-review-schedules:
extends:
- .only:variables_refs-canonical-dot-com-schedules
- .only:kubernetes
- .except:refs-deploy
.code-patterns: &code-patterns
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dangerfile"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
@ -55,36 +92,45 @@
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{app,bin,config,danger,db,ee,fixtures,haml_lint,lib,locale,public,rubocop,scripts,spec,symbol,vendor}/**/*"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
.backstage-patterns: &backstage-patterns
- "Dangerfile"
- "danger/**/*"
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
.only-qa-changes:
only:
changes:
.qa-patterns: &qa-patterns
- ".dockerignore"
- "qa/**/*"
.only-docs-changes:
only:
changes:
.docs-patterns: &docs-patterns
- ".gitlab/route-map.yml"
- "doc/**/*"
- ".markdownlint.json"
.only-graphql-changes:
.only:changes-code:
only:
changes:
- "{,ee/}app/graphql/**/*"
- "{,ee/}lib/gitlab/graphql/**/*"
changes: *code-patterns
.only-code-qa-changes:
.only:changes-qa:
only:
changes: *qa-patterns
.only:changes-docs:
only:
changes: *docs-patterns
.only:changes-code-backstage:
only:
changes:
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dangerfile"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
@ -92,35 +138,62 @@
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{app,bin,config,danger,db,ee,fixtures,haml_lint,lib,locale,public,rubocop,scripts,spec,symbol,vendor}/**/*"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# Backstage changes
- "Dangerfile"
- "danger/**/*"
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
.only:changes-code-qa:
only:
changes:
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
- "Rakefile"
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# QA changes
- ".dockerignore"
- "qa/**/*"
.only-review:
.only:changes-code-backstage-qa:
only:
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
kubernetes: active
except:
refs:
- master
- /^\d+-\d+-auto-deploy-\d+$/
- /^[\d-]+-stable(-ee)?$/
.only-review-schedules:
only:
refs:
- schedules
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
kubernetes: active
.only-canonical-schedules:
only:
refs:
- schedules@gitlab-org/gitlab
- schedules@gitlab-org/gitlab-foss
changes:
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
- "Rakefile"
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# Backstage changes
- "Dangerfile"
- "danger/**/*"
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
# QA changes
- ".dockerignore"
- "qa/**/*"
.use-pg9:
services:

View file

@ -5,7 +5,7 @@
- .default-cache
- .default-only
- .default-before_script
- .only-code-changes
- .only:changes-code
memory-static:
extends: .only-code-memory-job-base

View file

@ -1,29 +0,0 @@
.notify:
image: alpine
stage: notification
dependencies: []
cache: {}
before_script:
- apk update && apk add git curl bash
schedule:package-and-qa:notify-success:
extends:
- .only-canonical-schedules
- .notify
variables:
COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
script:
- 'scripts/notify-slack qa-master ":tada: Scheduled QA against master passed! :tada: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_passing'
needs: ["schedule:package-and-qa"]
when: on_success
schedule:package-and-qa:notify-failure:
extends:
- .only-canonical-schedules
- .notify
variables:
COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
script:
- 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_failing'
needs: ["schedule:package-and-qa"]
when: on_failure

View file

@ -4,12 +4,11 @@ pages:
- .default-retry
- .default-cache
- .default-only
- .only-code-qa-changes
- .only:variables-canonical-dot-com
- .only:changes-code-backstage-qa
only:
refs:
- master
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
script:

View file

@ -3,7 +3,7 @@
- .default-tags
- .default-retry
- .default-only
- .only-code-qa-changes
- .only:changes-code-qa
stage: test
dependencies: []
cache:
@ -31,7 +31,6 @@ qa:selectors-foss:
- .only-ee-as-if-foss
.package-and-qa-base:
extends: .default-only
image: ruby:2.6-alpine
stage: qa
dependencies: []
@ -40,35 +39,31 @@ qa:selectors-foss:
- source scripts/utils.sh
- install_gitlab_gem
- ./scripts/trigger-build omnibus
only:
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ # Matches the gitlab-org group or its subgroups
package-and-qa-manual:
extends:
- .package-and-qa-base
- .only-code-changes
except:
refs:
- master
- /^\d+-\d+-auto-deploy-\d+$/
- .default-only
- .only:variables-canonical-dot-com
- .except:refs-deploy
- .only:changes-code
when: manual
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
package-and-qa:
extends:
- .package-and-qa-base
- .only-qa-changes
except:
refs:
- master
- /^\d+-\d+-auto-deploy-\d+$/
- .default-only
- .only:variables-canonical-dot-com
- .except:refs-master-tags-stable-deploy
- .only:changes-qa
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
allow_failure: true
schedule:package-and-qa:
extends:
- .package-and-qa-base
- .only-code-qa-changes
- .only-canonical-schedules
- .default-only
- .only:variables_refs-canonical-dot-com-schedules
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
allow_failure: true

View file

@ -22,7 +22,7 @@
- .default-cache
- .default-only
- .default-before_script
- .only-code-changes
- .only:changes-code-backstage
.only-code-qa-rails-job-base:
extends:
@ -31,7 +31,7 @@
- .default-cache
- .default-only
- .default-before_script
- .only-code-qa-changes
- .only:changes-code-backstage-qa
setup-test-env:
extends:
@ -92,6 +92,14 @@ setup-test-env:
- .use-pg10
- .only-master
rspec migration pg9:
extends: .rspec-base-pg9
parallel: 4
rspec migration pg9-foss:
extends: .rspec-base-pg9-foss
parallel: 4
rspec unit pg9:
extends: .rspec-base-pg9
parallel: 20
@ -140,9 +148,13 @@ rspec system pg10:
- .only-ee
- .use-pg10-ee
rspec-ee migration pg9:
extends: .rspec-ee-base-pg9
parallel: 2
rspec-ee unit pg9:
extends: .rspec-ee-base-pg9
parallel: 7
parallel: 5
rspec-ee integration pg9:
extends: .rspec-ee-base-pg9
@ -152,11 +164,17 @@ rspec-ee system pg9:
extends: .rspec-ee-base-pg9
parallel: 5
rspec-ee migration pg10:
extends:
- .rspec-ee-base-pg10
- .only-master
parallel: 2
rspec-ee unit pg10:
extends:
- .rspec-ee-base-pg10
- .only-master
parallel: 7
parallel: 5
rspec-ee integration pg10:
extends:
@ -239,6 +257,7 @@ static-analysis:
dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
SETUP_DB: "false"
parallel: 2
script:
- scripts/static-analysis
cache:
@ -251,13 +270,8 @@ static-analysis:
downtime_check:
extends:
- .rake-exec
- .only-code-changes
except:
refs:
- master
- tags
variables:
- $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
- .only:changes-code-backstage
- .except:refs-master-tags-stable-deploy
stage: test
needs: ["setup-test-env"]
dependencies: ["setup-test-env"]

View file

@ -0,0 +1,22 @@
---
# Syncs any changes pushed to a stable branch to the corresponding CE stable
# branch. We run this prior to any tests so that random failures don't prevent a
# sync.
sync-stable-branch:
# We don't need/want any global before/after commands, so we overwrite these
# settings.
image: alpine:edge
stage: sync
# This job should only run on EE stable branches on the canonical GitLab.com
# repository.
only:
variables:
- $CI_SERVER_HOST == "gitlab.com"
refs:
- /^[\d-]+-stable-ee$/@gitlab-org/gitlab
before_script:
- apk add --no-cache --update curl bash
after_script: []
script:
- bash scripts/sync-stable-branch.sh

View file

@ -11,7 +11,7 @@ code_quality:
extends:
- .default-retry
- .default-only
- .only-code-changes
- .only:changes-code-backstage
stage: test
image: docker:stable
allow_failure: true
@ -50,7 +50,7 @@ sast:
extends:
- .default-retry
- .default-only
- .only-code-changes
- .only:changes-code-backstage-qa
stage: test
image: docker:stable
variables:
@ -132,7 +132,7 @@ dependency_scanning:
extends:
- .default-retry
- .default-only
- .only-code-changes
- .only:changes-code-backstage-qa
stage: test
image: docker:stable
variables:
@ -195,7 +195,7 @@ dast:
extends:
- .default-retry
- .default-only
- .only-code-qa-changes
- .only:changes-code-qa
- .only-review
stage: qa
needs: ["review-deploy"]

View file

@ -1,14 +1,8 @@
.except-deploys:
except:
refs:
- /^\d+-\d+-auto-deploy-\d+$/
.review-docker:
extends:
- .default-tags
- .default-retry
- .default-only
- .except-deploys
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
services:
- docker:19.03.0-dind
@ -23,10 +17,9 @@
build-qa-image:
extends:
- .review-docker
- .only-code-qa-changes
only:
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
- .only:variables-canonical-dot-com
- .except:refs-deploy
- .only:changes-code-qa
stage: prepare
script:
- '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
@ -35,14 +28,11 @@ build-qa-image:
- echo "${CI_JOB_TOKEN}" | docker login --username gitlab-ci-token --password-stdin ${CI_REGISTRY}
- time docker push ${QA_IMAGE}
schedule:review-cleanup:
.base-review-cleanup:
extends:
- .default-tags
- .default-retry
- .default-only
- .only-code-qa-changes
- .only-review-schedules
- .except-deploys
stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true
@ -55,11 +45,22 @@ schedule:review-cleanup:
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
schedule:review-cleanup:
extends:
- .base-review-cleanup
- .only-review-schedules
manual:review-cleanup:
extends:
- .base-review-cleanup
- .only:changes-code-qa
when: manual
.review-build-cng-base:
extends:
- .default-tags
- .default-retry
- .default-only
- .only-code-qa-changes
- .except-deploys
image: ruby:2.6-alpine
stage: review-prepare
before_script:
@ -74,6 +75,7 @@ review-build-cng:
extends:
- .review-build-cng-base
- .only-review
- .only:changes-code-qa
needs: ["gitlab:assets:compile pull-cache"]
schedule:review-build-cng:
@ -82,26 +84,30 @@ schedule:review-build-cng:
- .only-review-schedules
needs: ["gitlab:assets:compile pull-cache"]
.review-deploy-base:
.review-workflow-base:
extends:
- .default-tags
- .default-retry
- .default-only
- .only-code-qa-changes
- .except-deploys
stage: review
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
dependencies: []
allow_failure: true
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "v2.3.7"
# v2.4.4 + two improvements:
# - Allow to pass an EE license when installing the chart: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1008
# - Allow to customize the livenessProbe for `gitlab-shell`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1021
GITLAB_HELM_CHART_REF: "6c655ed77e60f1f7f533afb97bef8c9cb7dc61eb"
GITLAB_EDITION: "ce"
environment:
name: review/${CI_COMMIT_REF_NAME}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop
.review-deploy-base:
extends: .review-workflow-base
stage: review
allow_failure: true
before_script:
- '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
@ -112,21 +118,13 @@ schedule:review-build-cng:
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
script:
- date
- check_kube_domain
- date
- ensure_namespace
- date
- install_tiller
- date
- install_external_dns
- date
- download_chart
- date
- deploy || (display_deployment_debug && exit 1)
- date
- add_license
- date
artifacts:
paths: [review_app_url.txt]
expire_in: 2 days
@ -136,6 +134,7 @@ review-deploy:
extends:
- .review-deploy-base
- .only-review
- .only:changes-code-qa
needs: ["review-build-cng"]
schedule:review-deploy:
@ -144,11 +143,11 @@ schedule:review-deploy:
- .only-review-schedules
needs: ["schedule:review-build-cng"]
review-stop:
.base-review-stop:
extends:
- .review-deploy-base
- .review-workflow-base
- .only-review
when: manual
- .only:changes-code-qa
environment:
action: stop
variables:
@ -161,24 +160,26 @@ review-stop:
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/utils.sh
- source utils.sh
- source review-apps.sh
script:
- delete_release
artifacts:
paths: []
review-cleanup-failed-deployment:
extends: review-stop
review-stop-failed-deployment:
extends: .base-review-stop
stage: prepare
when: on_success
allow_failure: false
script:
- delete_failed_release
review-stop:
extends: .base-review-stop
stage: review
when: manual
allow_failure: true
script:
- delete_release
.review-qa-base:
extends:
- .review-docker
- .only-review
- .only-code-qa-changes
- .only:changes-code-qa
stage: qa
allow_failure: true
variables:
@ -223,9 +224,7 @@ review-qa-all:
- 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-base:
extends:
- .review-docker
- .only-code-qa-changes
extends: .review-docker
stage: qa
allow_failure: true
before_script:
@ -248,6 +247,7 @@ review-performance:
extends:
- .review-performance-base
- .only-review
- .only:changes-code-qa
needs: ["review-deploy"]
dependencies: ["review-deploy"]
before_script:
@ -277,9 +277,8 @@ parallel-spec-reports:
extends:
- .default-tags
- .default-only
- .only-code-qa-changes
- .only-review
- .except-deploys
- .only:changes-code-qa
image: ruby:2.6-alpine
stage: post-test
dependencies: ["review-qa-all"]
@ -310,18 +309,13 @@ danger-review:
- .default-retry
- .default-cache
- .default-only
- .except:refs-master-tags-stable-deploy
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
dependencies: []
only:
variables:
- $DANGER_GITLAB_API_TOKEN
except:
refs:
- master
variables:
- $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/
- $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
script:
- git version
- node --version

View file

@ -6,7 +6,8 @@ cache gems:
- .default-retry
- .default-cache
- .default-before_script
- .only-code-qa-changes
- .only:variables-canonical-dot-com
- .only:changes-code-backstage-qa
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
@ -21,15 +22,13 @@ cache gems:
refs:
- master
- tags
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
.minimal-job:
extends:
- .default-tags
- .default-retry
- .default-only
- .only-code-changes
- .only:changes-code-backstage
dependencies: []
gitlab_git_test:

View file

@ -1,7 +1,7 @@
.tests-metadata-state:
extends:
- .default-only
- .only-code-changes
- .only:changes-code-backstage
variables:
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
before_script:
@ -48,7 +48,7 @@ flaky-examples-check:
- .default-tags
- .default-retry
- .default-only
- .only-code-changes
- .only:changes-code-backstage
image: ruby:2.6-alpine
stage: post-test
variables:

View file

@ -29,7 +29,7 @@ Set the title to: `Description of the original issue`
#### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)

View file

@ -34,7 +34,7 @@ All reviewers can help ensure accuracy, clarity, completeness, and adherence to
**3. Maintainer**
1. [ ] Review by assigned maintainer, who can always request/require the above reviews. Maintainer's review can occur before or after a technical writer review.
1. [ ] Ensure a release milestone is set and that you merge the equivalent EE MR before the CE MR if both exist.
1. [ ] Ensure a release milestone is set.
1. [ ] If there has not been a technical writer review, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab/issues/new?issuable_template=Doc%20Review).
/label ~documentation

View file

@ -416,9 +416,6 @@ linters:
- 'app/views/u2f/_register.html.haml'
- 'app/views/users/_deletion_guidance.html.haml'
- 'ee/app/views/admin/_namespace_plan_info.html.haml'
- 'ee/app/views/admin/application_settings/_elasticsearch_form.html.haml'
- 'ee/app/views/admin/application_settings/_slack.html.haml'
- 'ee/app/views/admin/application_settings/_snowplow.html.haml'
- 'ee/app/views/admin/application_settings/_templates.html.haml'
- 'ee/app/views/admin/audit_logs/index.html.haml'
- 'ee/app/views/admin/dashboard/stats.html.haml'
@ -495,7 +492,6 @@ linters:
- 'ee/app/views/projects/services/prometheus/_metrics.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_additional_email_text.html.haml'
- 'ee/app/views/shared/_geo_info_modal.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
- 'ee/app/views/shared/_shared_runners_minutes_limit.html.haml'
- 'ee/app/views/shared/audit_events/_event_table.html.haml'

View file

@ -16,10 +16,25 @@
# Uncomment the following lines to make the configuration take effect.
PreCommit:
AuthorName:
enabled: false
EsLint:
enabled: true
# https://github.com/sds/overcommit/issues/338
command: './node_modules/eslint/bin/eslint.js'
HamlLint:
enabled: true
MergeConflicts:
enabled: true
exclude:
- '**/conflict/file_spec.rb'
- '**/git/conflict/parser_spec.rb'
# prettier? https://github.com/sds/overcommit/issues/614 https://github.com/sds/overcommit/issues/390#issuecomment-495703284
RuboCop:
enabled: true
# on_warn: fail # Treat all warnings as failures
#
ScssLint:
enabled: true
#PostCheckout:
# ALL: # Special hook name that customizes all hooks of this type
# quiet: true # Change all post-checkout hooks to only display output on failure

View file

@ -56,7 +56,7 @@ Style/FrozenStringLiteralComment:
- 'qa/**/*'
- 'rubocop/**/*'
- 'scripts/**/*'
- 'spec/**/*'
- 'spec/lib/gitlab/**/*'
RSpec/FilePath:
Exclude:
@ -297,3 +297,6 @@ Graphql/Descriptions:
Include:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
RSpec/AnyInstanceOf:
Enabled: false

View file

@ -401,13 +401,6 @@ Rails/FilePath:
Rails/HasManyOrHasOneDependent:
Enabled: false
# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: numeric, symbolic
Rails/HttpStatus:
Enabled: false
# Offense count: 2
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb

View file

@ -1,10 +1,21 @@
Please view this file on the master branch, on stable branches it's out of date.
## 12.4.5
## 12.5.3
### Performance (1 change)
- Geo - Improve query performance to determine job artifacts to sync when selective sync is enabled. !19583
### Other (1 change)
- Geo - Does not schedule duplicated jobs while backfilling uploads, LFS objects and job artifacts. !20324
## 12.5.2
- No changes.
## 12.4.4
## 12.5.1
### Security (6 changes)
@ -16,12 +27,90 @@ Please view this file on the master branch, on stable branches it's out of date.
- Prevent IDOR when adding users to protected environments.
## 12.4.3
## 12.5.0
### Fixed (2 changes)
### Security (5 changes)
- Fixes a Open Redirect issue in `InternalRedirect`.
- Filter out packages the user does'nt have permission to see at group level.
- Do not show private cross references in epic notes.
- Redact search results based on Ability.allowed?.
- Do not index system notes for issue update.
### Removed (2 changes, 1 of them is from the community)
- Remove the Geo Clone Modal. !18897 (Zack Cuddy)
- Remove Pendo Snippet. !19400
### Fixed (17 changes)
- Fix notification button size in notification settings. !16672
- Don't store full blob path in ES filename field. !18470
- Add messages to warn and stop users when attempting to change the path of projects with NPM packages. !18515
- Pass pipeline variables when expanding Bridge downstream variables. !18875
- Fix equality operator for Prometheus alerts. !18919
- Fix rake task to rollback Geo migrations. !18975
- Default current user to mirror user when creating pipelines for GitHub pull requests. !19072
- Fix overlapping `Skip Trial` block. !19218
- Fix Dependency List is empty if last pipeline is retried. !19241
- SCIM pagination startIndex handles string input. !19331
- Display packages with multiple licenses. !19333
- Expose commit sha on Vulnerabilities::Occurrence. !19668
- Fix admin welcome image not found. !19676
- Revert ES support for public/internal project snippets. !19715
- Updated View documentation link on cluster page. !19780
- Enable pod logs nav menu only for maintainers in projects with k8s environments. !19927
- Hide labels from issue board cards. !20072
### Changed (13 changes)
- Inherit children epics start and due dates. !14366
- Update the frontend diffing code to support v2 license scan reports. !18105
- Implement pod logs page using Vue. !18567
- Move DAST reports logic for the Merge Request widget to the backend. !18660
- Add created_before/after filter to audit events. !19035
- Get rid of unnecessary duplication of alerts title from Alert Details. !19214
- Hashed storage is now a requirement for Design Management. !19259
- Expose epic in issues API. !19300
- SCIM GET /Users supports requests without a filter. !19421
- Enable Cycle Analytics Feature by default. !19484
- Enforce a max size accepted for sentry issues list. !19649
- Limit input size for Prometheus alert JSON payload. !19940
- Adds in a URL field for DAST reports modal data. !20162
### Performance (2 changes)
- Fix new project page load performance. !18180
- Geo - Improve query performance to determine LFS objects to sync when selective sync is enabled. !19051
### Added (17 changes, 1 of them is from the community)
- Add filter for dismissed vulnerabilities on security dashboards. !16692
- Data API endpoint for tasks by type chart within the analytics workspace. !17944
- Hide labels from issue board cards. !18533
- Skip Onboarding feedback when tracking is disabled. !18671
- API endpoint to list the packages of a group. !18871
- Allow to create epics with GraphQL. !19030
- CI_JOB_TOKEN can be accepted with 'Bearer ' prefix to allow for NPM registry usage. !19059
- Add issue IID to a title of generic alerts with a default title. !19086
- Update sidebar to differentiate between groups, subgroups, and projects. !19158
- SCIM can be used to manage group membership. !19329
- Expose number of sub-epics and epic issues in GraphQL API. !19450
- Add logs menu item to the sidebar. !19471
- Add public API for Feature Flags. !19547
- Ignore project_ci_cd_settings.merge_trains_enabled column. !19695
- Add a usage ping metric for number of activated Alert Services. !19765
- New discussions on designs will generate a system note on the issue. !19990
- Expose SHA of squashed commit via API when fast-forward merge is enabled. (minghuan lei)
### Other (6 changes, 1 of them is from the community)
- Migrated contributors charts to echarts. !16677
- Added autogenerated Markdown support for Vulnerability title and description. !18283
- Rename user_id to author_id in design_management_versions table. !18506
- Revert notification for updated privacy policy. !18900
- Remove plaintext tokens for feature flags clients. !18923
- Remove IIFEs from jira_connect.js file. !19248 (nuwe1)
## 12.4.2
@ -4205,7 +4294,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Show hook errors for fast-forward merges. !1375
- Allow all parameters of group webhooks to be set through the UI. !1376
- Fix Elasticsearch queries when a group_id is specified. !1423
- Check the right index mapping based on Rails environment for rake gitlab:elastic:add_feature_visiblity_levels_to_project. !1473
- Check the right index mapping based on Rails environment for rake gitlab:elastic:add_feature_visibility_levels_to_project. !1473
- Fix issues with another milestone that has a matching list label could not be added to a board.
- Only admins or group owners can set LDAP overrides.
- Add support for load balancing database queries.

View file

@ -2,38 +2,397 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 12.4.6
## 12.5.4
- No changes.
## 12.4.5
## 12.5.3
- No changes.
### Fixed (4 changes)
## 12.4.4
- Fix project creation with templates using /projects/user/:id API. !20590
- Fix merging merge requests from push options. !20639
- Fix Crossplane help link in cluster applications page. !20668
- Fixes job log not scrolling to the bottom.
### Security (12 changes)
### Changed (1 change)
- Flatten exception details in API and controller logs. !20434
## 12.5.2
### Security (1 change)
- Fix 500 error caused by invalid byte sequences in links.
## 12.5.1
### Security (11 changes)
- Do not create todos for approvers without access. !1442
- Limit potential for DNS rebind SSRF in chat notifications.
- Hide commit counts from guest users in Cycle Analytics.
- Encrypt application setting tokens.
- Update Workhorse and Gitaly to fix a security issue.
- Add maven file_name regex validation on incoming files.
- Hide commit counts from guest users in Cycle Analytics.
- Check permissions before showing a forked project's source.
- Fix 500 error caused by invalid byte sequences in links.
- Limit potential for DNS rebind SSRF in chat notifications.
- Ensure are cleaned by ImportExport::AttributeCleaner.
- Remove notes regarding Related Branches from Issue activity feeds for guest users.
- Escape namespace in label references to prevent XSS.
- Add authorization to using filter vulnerable in Dependency List.
## 12.4.3
## 12.5.0
### Fixed (2 changes)
### Security (15 changes)
- Enable the HttpOnly flag for experimentation_subject_id cookie. !19189
- Update incrementing of failed logins to be thread-safe. !19614
- Sanitize all wiki markup formats with GitLab sanitization pipelines.
- Sanitize search text to prevent XSS.
- Remove deploy access level when project/group link is deleted.
- Mask sentry auth token in Error Tracking dashboard.
- Return 404 on LFS request if project doesn't exist.
- Don't leak private members in project member autocomplete suggestions.
- Require Maintainer permission on group where project is transferred to.
- Don't allow maintainers of a target project to delete the source branch of a merge request from a fork.
- Disallow unprivileged users from commenting on private repository commits.
- Analyze incoming GraphQL queries and check for recursion.
- Show cross-referenced label and milestones in issues' activities only to authorized users.
- Do not display project labels that are not visible for user accessing group labels.
- Standardize error response when route is missing.
### Fixed (99 changes, 14 of them are from the community)
- Fix incorrect selection of custom templates. !17205
- Smaller width for design comments layout, truncate image title. !17547
- Correctly cleanup orphan job artifacts. !17679 (Adam Mulvany)
- Add Infinite scroll to Add Projects modal in the operations dashboard. !17842
- Allow emojis to be linkable. !18014
- Enable image link and lazy loading in AsciiDoc documents. !18164 (Guillaume Grossetie)
- Expose prometheus status to monitor dashboard. !18289
- Time limit the database lock when rebasing a merge request. !18481
- Fix missing admin mode UI buttons on bigger screen sizes. !18585 (Diego Louzán)
- Abort only MWPS when FF only merge is impossible. !18591
- Remove pointer cursor from MemoryUsage chart on MR widget deployment. !18599
- Fix keyboard shortcuts in header search autocomplete. !18685
- Fix empty chart in collapsed sections. !18699
- Fix error when viewing group billing page. !18740
- Fix query validation in custom metrics form. !18769
- Fix Gitaly call duration measurements. !18785
- Resolve Error when uploading a few designs in a row. !18811
- Block MR with OMIPS on skipped pipelines. !18838
- Pipeline vulnerability dashboard sort vulnerabilities by severity then confidence. !18863
- Remove empty Github service templates from database. !18868
- Fix broken images when previewing markdown files in Web IDE. !18899
- fixed #27164 Image cannot be collapsed on merge request changes tab. !18917 (Jannik Lehmann)
- Let ANSI \r code replace the current job log line. !18933
- Fix serverless function descriptions not showing on Knative 0.7. !18973
- Fix "project or group was moved" alerts showing up in the wrong pages. !18985
- Add missing breadcrumb in Project > Settings > Integrations. !18990
- Fixed admin geo collapsed sidebar fly out not showing. !19012
- Serialize short sha as nil if head commit is blank. !19014
- Add max width on manifest file attachment input. !19028
- Do not generate To-Dos additional when editing group mentions. !19037
- Fix previewing quick actions for epics. !19042
- Fix errors in GraphQL Todos API due to missing TargetTypeEnum values. !19052
- Hashed Storage Migration: Handle failed attachment migrations with existing target path. !19061
- Set shorter TTL for all unauthenticated requests. !19064
- Fix Todo IDs in GraphQL API. !19068
- Triggers the correct endpoint on licence approval. !19078
- Fix search button height on 404 page. !19080
- Fix Kubernetes help text link. !19121
- Make `jobs/request` to be resillient. !19150
- Disable pull mirror if repository is in read-only state. !19182
- Only enable protected paths for POST requests. !19184
- Enforce default, global project and snippet visibilities. !19188
- Make Bitbucket Cloud superseded pull requests as closed. !19193
- Fix crash when docker fails deleting tags. !19208
- Fix environment name in rollback dialog. !19209
- Fixed a typo in the "Keyboard Shortcuts" pop-up. !19217 (Manuel Stein)
- Fix unable to expand or collapse files in merge request by clicking caret. !19222 (Brian T)
- Allow release block edit button to be visible. !19226
- Fix double escaping in /tableflip quick action. !19271 (Brian T)
- Add missing bottom padding in CI/CD settings. !19284 (George Tsiolis)
- Prevents console warning on design upload. !19297
- Resolve: Web IDE does not create POSIX Compliant Files. !19339
- Use initial commit SHA instead of branch id to request IDE files and contents. !19348 (David Palubin)
- Resolve: Web IDE Throws Error When Viewing Diff for Renamed Files. !19348
- Fix project service API 500 error. !19367
- Fix cluster feature highlight popover image. !19372
- Fix template selector filename bug. !19376
- Fixes mobile styling issues on security modals. !19391
- Only move repos for legacy project storage. !19410
- Show correct total number of commit diff's changes. !19424
- Increase the timeout for GitLab-managed cert-manager installation to 90 seconds (was 30 seconds). !19447
- Fix uninitialized constant SystemDashboardService. !19453
- Properly handle exceptions in StuckCiJobsWorker. !19465
- Fix user popover not being displayed when the user has a status message. !19519
- Update omniauth_openid_connect to v0.3.3. !19525
- Fix project clone dropdown button width. !19551 (George Tsiolis)
- Do not escape HTML tags in Ansi2json as they are escaped in the frontend. !19610
- [Geo] Fix: undefined Gitlab::BackgroundMigration::PruneOrphanedGeoEvents. !19638
- Revert btn-xs styling in projects scss. !19640
- Fix canary badge and favicon inconsistency. !19645
- Use fingerprint when comparing security reports in MR widget. !19654
- Update GCP credit URLs. !19683
- Update squash_commit_sha only on successful merge. !19688
- Fix import of snippets having `award_emoji` (Project Export/Import). !19690
- Allow admins to administer personal snippets. !19693 (Oren Kanner)
- Re-add missing file sizes in 2-Up diff file viewer. !19710
- Fix checking task item when previous tasks contain only spaces. !19724
- Fix Bitbucket Cloud importer pull request state. !19734
- Fix merge train is not refreshed when the system aborts/drops a merge request. !19763
- Resolve Hide Delete selected in designs when viewing an old version. !19889
- Use new trial registration URL in billing. !19978
- Helm v2.16.1. !19981
- Ensure milestone titles are never empty. !19985
- Remove unused image/screenshot. !20030 (Lee Tickett)
- Remove local qualifier from geo sync indicators. !20034 (Lee Tickett)
- Fixed the scale of embedded videos to fit the page. !20056
- Fix broken monitor cluster health dashboard. !20120
- Fix expanding collapsed threads when reference link clicked. !20148
- Fix sub group export to export direct children. !20172
- Remove update hook from date filter to prevent js from getting stuck. !20215
- Prevent Dropzone.js initialisation error by checking target element existence. !20256 (Fabio Huser)
- Fix style reset in job log when empty ANSI sequence is encoutered. !20367
- Add productivity analytics merge date filtering limit. !32052
- Fix productivity analytics listing with multiple labels. !33182
- Fix closed board list loading issue.
- Apply correctly the limit of 10 designs per upload.
- Only allow confirmed users to run pipelines.
- Fix scroll to bottom with new job log.
- Fixed protected branches flash styling.
### Deprecated (2 changes)
- Ignore deprecated column and remove references to it. !18911
- Move some project routes under - scope. !19954
### Changed (56 changes, 6 of them are from the community)
- Upgrade design/copy for issue weights locked feature. !17352
- Reduce new MR page redundancy by moving the source/target branch selector to the top. !17559
- Replace raven-js with @sentry/browser. !17715
- Ask if the user is setting up GitLab for a company during signup. !17999
- When a user views a file's blame or blob and switches to a branch where the current file does not exist, they will now be redirected to the root of the repository. !18169 (Jesse Hall @jessehall3)
- Propagate custom environment variables to SAST analyzers. !18193
- Fix any approver project rule records. !18265
- Minor UX improvements to Environments Dashboard page. !18280
- Reduce the allocated IP for Cluster and Services. !18341
- Update flash messages color sitewide. !18369
- Add modsecurity template for ingress-controller. !18485
- Hide projects without access to admin user when admin mode is disabled. !18530 (Diego Louzán)
- Update Runners Settings Text + Link to Docs. !18534
- Store Zoom URLs in a table rather than in the issue description. !18620
- Improve admin dashboard features. !18666
- Drop `id` column from `ci_build_trace_sections` table. !18741
- Truncate recommended branch name to a sane length. !18821
- Add support for YAML anchors in CI scripts. !18849
- Save dashboard changes by the user into the vuex store. !18862
- Update expired trial status copy. !18962
- Can directly add approvers to approval rule. !18965
- Rename Vulnerabilities API to Vulnerability Findings API. !19029
- Improve clarity of text for merge train position. !19031
- Updated Auto-DevOps to kubectl v1.13.12 and helm v2.15.1. !19054 (Leo Antunes)
- Refactor maximum user counts in license. !19071 (briankabiro)
- Change return type of getDateInPast to Date. !19081
- Show approval required status in license compliance. !19114
- Handle new Container Scanning report format. !19123
- Allow container scanning to run offline by specifying the Clair DB image to use. !19161
- Add maven cli opts flag to maven security analyzer (part of dependency scanning). !19174
- Added report_type attribute to Vulnerabilities. !19179
- Migrate enabled flag on grafana_integrations table. !19234
- Improve handling of gpg-agent processes. !19311
- Update help text of "Tag name" field on Edit Release page. !19321
- Add user filtering to abuse reports page. !19365
- Move add license button to project buttons. !19370
- Update to Mermaid v8.4.2 to support more graph types. !19444
- Move release meta-data into footer on Releases page. !19451
- Expose subscribed field in issue lists queried with GraphQL. !19458 (briankabiro)
- [Geo] Fix: rake gitlab:geo:check on the primary is cluttered. !19460
- Hide trial banner for namespaces with expired trials. !19510
- Hide repeated trial offers on self-hosted instances. !19511
- Add loading icon to error tracking settings page. !19539
- Upgrade to Gitaly v1.71.0. !19611
- Make role required when editing profile. !19636
- Made `name` optional parameter of Release entity. !19705
- Vulnerabilities history chart - use sparklines. !19745
- Add event tracking to container registry. !19772
- Update SaaS trial header to include the tier Gold. !19970
- Update start a trial option in top right drop down to include Gold. !19971
- Improve merge request description placeholder. !20032 (Jacopo Beschi @jacopo-beschi)
- Add backtrace to production_json.log. !20122
- Change the default concurrency factor of merge train to 20. !20201
- Upgrade to Gitaly v1.72.0.
- Require explicit null parameters to remove pages domain certificate and allow to use Let's Encrypt certificates through API.
- Replace wording trace with log.
### Performance (13 changes)
- Record latencies for Sidekiq failures. !18909
- Fix N+1 for group container repositories view. !18979
- Do not render links in commit message on blame page. !19128
- Puma only: database connection pool now always >= number of worker threads. !19286
- Run check_mergeability only if merge status requires it. !19364
- Execute limited request for diff commits instead of preloading. !19485
- Improve performance of admin/abuse_reports page. !19630
- Remove N+1 DB calls from branches API. !19661
- Improve performance of linking LFS objects during import. !19709
- Optimize MergeRequest#mergeable_discussions_state? method. !19988
- Add index for unauthenticated requests to projects API default endpoint. !19989
- Add index for authenticated requests to projects API default endpoint. !19993
- Increase PumaWorkerKiller memory limit in development environment. !20039
### Added (83 changes, 8 of them are from the community)
- Adds Application Settings and ui settings in the integration admin area for Pendo. !15086
- Add endpoint for a group's vulnerable projects. !15317
- Added new chart component to display an anomaly boundary. !16530
- Add links to associated releases on the Milestones page. !16558
- Merge Details Page and Edit Page for Page Domains. !16687
- Share groups with groups. !17117
- Add links to associated release(s) to the milestone detail page. !17278
- New group path uniqueness check. !17394
- Unify html email layout for member html emails. !17699 (Diego Louzán)
- The Security Dashboard displays DAST vulnerabilities for all the scanned sites, not just the first. !17779
- Create table for elastic stack. !18015
- Allow to define a default CI configuration path for new projects. !18073 (Mathieu Parent)
- Issues queried in GraphQL now sortable by due date. !18094
- Add cleanup status to clusters. !18144
- Added Tests tab to pipeline detail that contains a UI for browsing test reports produced by JUnit. !18255
- Users can verify SAML configuration and view SamlResponse XML. !18362
- Support Enable/Disable operations in Feature Flag API. !18368
- Expose arbitrary job artifacts in Merge Request widget. !18385
- Add project option for deleting source branch. !18408 (Zsolt Kovari)
- Adds ability to set management project for cluster via API. !18429
- Close issues on Prometheus alert recovery. !18431
- Add ApplicationSetting for snowplow_iglu_registry_url. !18449
- Allow Grafana charts to be embedded in Gitlab Flavored Markdown. !18486
- Mark todo done by GraphQL API. !18581
- Create a users_security_dashboard_projects table to store the projects a user has added to their personal security dashboard. !18708
- New API endpoint for creating anonymous merge request discussions from Visual Review Tools. !18710
- Enable the color chip in AsciiDoc documents. !18723
- Add prevent_ldap_sign_in option so LDAP can be used exclusively for sync. !18749
- Show inherited group variables in project view. !18759
- Add "release" filter to issue search page. !18761
- Search list of Sentry errors by title in Gitlab. !18772
- Add migrations and changes for soft-delete for projects. !18791
- Support for Crossplane as a managed app. !18797 (Mahendra Bagul)
- Bump Auto-Deploy image to v0.3.0. !18809
- Set X-GitLab-NotificationReason header if notification reason is explicit subscription. !18812
- Add issues, MRs, participants, and labels tabs in group milestone page. !18818
- Add ability to reorder projects on operations dashboard. !18855
- Make `Job`, `Bridge` and `Default` inheritable. !18867
- Show epic events on group activity page. !18869
- Detail view of Sentry error in GitLab. !18878
- Expose mergeable state of a merge request. !18888 (briankabiro)
- Add ability to select a Cluster management project. !18928
- Add a Slack slash command to add a comment to an issue. !18946
- Added installation commands for npm and yarn packages to package detail page. !18999
- Show start and end dates in Epics list page. !19006
- Populate new pipeline CI vars from params. !19023
- Add warnings about pages access control settings. !19067
- Graphql mutation for (un)subscribing to an epic. !19083
- API for stack trace & detail view of Sentry error in GitLab. !19137
- Add grafana integration active status checkbox. !19255
- GraphQL: Add Merge Request milestone mutation. !19257
- Add MergeRequestSetAssignees GraphQL mutation. !19272
- Add edit button to metrics dashboard. !19279
- Add "release" filter to merge request search page. !19315
- Add dead jobs to Sidekiq metrics API. !19350 (Marco Peterseil)
- Add pipeline information to dependency list header. !19352
- Build CI cache key from commit SHAs that changed given files. !19392
- Adding support for searching tags using '^' and '$'. !19435 (Cauhx Milloy)
- Sentry error stacktrace. !19492
- Add an `error_code` attribute to the API response when a cherry-pick or revert fails. !19518
- Add documentation for sign-in application setting. !19561 (Horatiu Eugen Vlad)
- Create AWS EKS cluster. !19578
- Add modsecurity logging sidecar to ingress controller. !19600
- Add start a trial option in the top-right user dropdown. !19632
- Manage and display labels from epic in the GraphQL API. !19642
- Allow order_by updated_at in Deployments API. !19658
- Add can_edit and project_blob_path to metrics_dashboard endpoint. !19663
- Add usage ping data for project services. !19687
- Graphql query for issues can now be sorted by relative_position. !19713
- Add API endpoint to trigger Group Structure Export. !19779
- Show Tree UI containing child Epics and Issues within an Epic. !19812
- Enable environments dashboard by default. !19838
- Update the DB schema to allow linking between Vulnerabilities and Issues. !19852
- Add Group Audit Events API. !19868
- Adds a copy button next to package metadata on the details page. !19881
- GraphQL: Create MR mutations needed for the sidebar. !19913
- Add id_before, id_after filter param to projects API. !19949
- Add modsecurity feature flag to usage ping. !20194
- Specify management project for a Kubernetes cluster. !20216
- Upgrade pages to 1.12.0. !20217
- Support template_project_id parameter in project creation API. !20258
- Add heatmap chart support. !32424
- Add template for Serverless Framework/JS. !33805
### Other (59 changes, 26 of them are from the community)
- Add EKS cluster count to usage data. !17059
- Track the starting and stopping of the current signup flow and the experimental signup flow. !17521
- Attribute Sidekiq workers according to their workloads. !18066
- Add ApplicationSetting entries for EKS integration. !18307
- Geo: Add resigns-related fields to Geo Node Status table. !18379
- Allow adding requests to performance bar manually. !18464
- Removes `export_designs` feature flag. !18507 (nate geslin)
- Update AWS SDK to 2.11.374. !18601
- Remove required dependecy of Postgresql for Gitaly. !18659
- Add deployment_merge_requests table. !18755
- Bump Gitaly to 1.70.0 and remove cache invalidation feature flag. !18766
- Update gRPC to v1.24.0. !18837
- Update GitLab Runner Helm Chart to 0.10.0. !18879
- Adds a Sidekiq queue duration metric. !19005
- Create explicit Default and Free plans. !19033
- Improve instance mirroring help text. !19047
- Add Codesandbox metrics to usage ping. !19075
- Add internal_socket_dir to gitaly config in setup helper. !19170
- Use Rails 5.2 Redis caching store. !19202
- Update GitLab Runner Helm Chart to 0.10.1. !19232
- Rename snowplow_site_id to snowplow_app_id in application_settings table. !19252
- Removed IIFEs from network.js file. !19254 (nuwe1)
- Remove IIFEs from project_select.js. !19288 (minghuan lei)
- Remove IIFEs from merge_request.js. !19294 (minghuan lei)
- Make snippet list easier to scan. !19490
- Removed IIFEs from image_file.js. !19548 (nuwe1)
- Fix api docs for deleting project cluster. !19558
- Change blob edit view button styling. !19566
- Include exception and backtrace in API logs. !19671
- Add index on marked_for_deletion_at in projects table. !19788
- Visual design for edit buttons in blob view. !19932
- Refactor disabled sidebar notifications to Vue. !20007 (minghuan lei)
- Remove IIFEs from branch_graph.js. !20008 (minghuan lei)
- Remove IIFEs from new_branch_form.js. !20009 (minghuan lei)
- Remove duplication from slugifyWithUnderscore function. !20016 (Arun Kumar Mohan)
- Update registry.gitlab.com/gitlab-org/security-products/codequality to 12-5-stable. !20046 (Takuya Noguchi)
- Add mb-2 class to global alerts. !20081 (2knal)
- Remove var from syntax_highlight_spec.js. !20086 (Lee Tickett)
- Remove var from merge_request_tabs_spec.js. !20087 (Lee Tickett)
- Remove var from bootstrap_jquery_spec.js. !20089 (Lee Tickett)
- Remove var from project_select.js. !20091 (Lee Tickett)
- Remove var from new_commit_form.js. !20095 (Lee Tickett)
- Remove var from issue.js. !20098 (Lee Tickett)
- Remove var from new_branch_form.js. !20099 (Lee Tickett)
- Remove var from tree.js. !20103 (Lee Tickett)
- Remove var from line_highlighter.js. !20108 (Lee Tickett)
- Remove var from preview_markdown.js. !20115 (Lee Tickett)
- remove all references of BoardService in boards_selector.vue. !20147 (nuwe1)
- Remove all references to BoardsService in index.vue. !20152 (nuwe1)
- Remove var from labels_select.js. !20153 (Lee Tickett)
- Remove all reference to BoardService in board_form.vue. !20158 (nuwe1)
- Remove calendar icon from personal access tokens. !20183
- Move margin-top from flash container to flash. !20211
- Bump Auto DevOps deploy image to v0.7.0. !20250
- Make 'Sidekiq::Testing.fake!' mode as default. !31662 (@blackst0ne)
- Replace task-done icon with list-task icon to better align with other toolbar list icons.
- Dependency Scanning template that doesn't rely on Docker-in-Docker.
- Adding dropdown arrow icon and updated text alignment.
- Change selects from default browser style to custom style.
## 12.4.2

View file

@ -1 +1 @@
1.67.1
1.72.1

View file

@ -1 +1 @@
1.4.0
1.5.0

View file

@ -1 +1 @@
1.11.0
1.12.0

47
Gemfile
View file

@ -8,12 +8,12 @@ gem 'bootsnap', '~> 1.4'
gem 'nakayoshi_fork', '~> 0.0.4'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
gem 'responders', '~> 3.0'
gem 'sprockets', '~> 3.7.0'
# Default values for AR models
gem 'default_value_for', '~> 3.2.0'
gem 'default_value_for', '~> 3.3.0'
# Supported DBs
gem 'pg', '~> 1.1'
@ -42,7 +42,7 @@ gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3'
gem 'omniauth_openid_connect', '~> 0.3.1'
gem 'omniauth_openid_connect', '~> 0.3.3'
gem "omniauth-ultraauth", '~> 0.0.2'
gem 'omniauth-salesforce', '~> 1.0.5'
gem 'rack-oauth2', '~> 1.9.3'
@ -64,7 +64,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.2.2', require: 'zip'
gem 'rubyzip', '~> 1.3.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.2'
@ -72,7 +72,7 @@ gem 'acme-client', '~> 2.0.2'
gem 'browser', '~> 2.5'
# GPG
gem 'gpgme', '~> 2.0.18'
gem 'gpgme', '~> 2.0.19'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@ -136,7 +136,7 @@ gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.2.0'
gem 'deckar01-task_list', '2.2.1'
gem 'gitlab-markup', '~> 1.7.0'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.17'
@ -151,7 +151,7 @@ gem 'asciidoctor-plantuml', '0.0.9'
gem 'rouge', '~> 3.11.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.4'
gem 'nokogiri', '~> 1.10.5'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
@ -159,6 +159,7 @@ gem 'icalendar'
# Diffs
gem 'diffy', '~> 3.1.0'
gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.7'
@ -175,7 +176,7 @@ group :puma do
end
# State machine
gem 'state_machines-activerecord', '~> 0.5.1'
gem 'state_machines-activerecord', '~> 0.6.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 6.0'
@ -259,9 +260,6 @@ gem 'loofah', '~> 2.2'
# Working with license
gem 'licensee', '~> 8.9'
# Protect against bruteforcing
gem 'rack-attack', '~> 4.4.1'
# Ace editor
gem 'ace-rails-ap', '~> 4.1.0'
@ -293,10 +291,13 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0"
# Protect against bruteforcing
gem 'rack-attack', '~> 6.2.0'
# Sentry integration
gem 'sentry-raven', '~> 2.9'
gem 'premailer-rails', '~> 1.9.7'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '~> 0.5'
@ -331,7 +332,6 @@ group :metrics do
end
group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
gem 'danger', '~> 6.0', require: false
@ -388,7 +388,6 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 5.4', require: false
gem 'knapsack', '~> 1.17'
gem 'stackprof', '~> 0.2.10', require: false
@ -398,6 +397,11 @@ group :development, :test do
gem 'timecop', '~> 0.8.0'
end
# Gems required in omnibus-gitlab pipeline
group :development, :test, :omnibus do
gem 'license_finder', '~> 5.4', require: false
end
group :test do
gem 'shoulda-matchers', '~> 4.0.1', require: false
gem 'email_spec', '~> 2.2.0'
@ -407,6 +411,7 @@ group :test do
gem 'concurrent-ruby', '~> 1.1'
gem 'test-prof', '~> 0.10.0'
gem 'rspec_junit_formatter'
gem 'guard-rspec'
end
gem 'octokit', '~> 4.9'
@ -446,18 +451,18 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.65.0'
gem 'gitaly', '~> 1.70.0'
gem 'grpc', '~> 1.19.0'
gem 'grpc', '~> 1.24.0'
gem 'google-protobuf', '~> 3.7.1'
gem 'google-protobuf', '~> 3.8.0'
gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles
gem 'flipper', '~> 0.13.0'
gem 'flipper-active_record', '~> 0.13.0'
gem 'flipper-active_support_cache_store', '~> 0.13.0'
gem 'flipper', '~> 0.17.1'
gem 'flipper-active_record', '~> 0.17.1'
gem 'flipper-active_support_cache_store', '~> 0.17.1'
gem 'unleash', '~> 0.1.5'
# Structured logging
@ -469,3 +474,5 @@ gem 'gitlab-net-dns', '~> 0.9.1'
# Countries list
gem 'countries', '~> 3.0'
gem 'retriable', '~> 3.1.2'

View file

@ -50,8 +50,8 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
acts-as-taggable-on (6.0.0)
activerecord (~> 5.0)
acts-as-taggable-on (6.5.0)
activerecord (>= 5.0, < 6.1)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
@ -80,14 +80,16 @@ GEM
encryptor (~> 3.0.0)
attr_required (1.0.1)
awesome_print (1.8.0)
aws-sdk (2.9.32)
aws-sdk-resources (= 2.9.32)
aws-sdk-core (2.9.32)
aws-eventstream (1.0.3)
aws-sdk (2.11.374)
aws-sdk-resources (= 2.11.374)
aws-sdk-core (2.11.374)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.9.32)
aws-sdk-core (= 2.9.32)
aws-sigv4 (1.0.0)
aws-sdk-resources (2.11.374)
aws-sdk-core (= 2.11.374)
aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@ -171,9 +173,9 @@ GEM
unicode_utils (~> 1.4)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
crass (1.0.5)
creole (0.5.0)
css_parser (1.5.0)
css_parser (1.7.0)
addressable
daemons (1.2.6)
danger (6.0.9)
@ -192,12 +194,12 @@ GEM
database_cleaner (1.7.0)
debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.2.0)
deckar01-task_list (2.2.1)
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
default_value_for (3.2.0)
activerecord (>= 3.2.0, < 6.0)
default_value_for (3.3.0)
activerecord (>= 3.2.0, < 6.1)
derailed_benchmarks (1.3.5)
benchmark-ips (~> 2)
get_process_mem (~> 0)
@ -222,6 +224,7 @@ GEM
railties
rotp (~> 2.0)
diff-lcs (1.3)
diff_match_patch (0.1.0)
diffy (3.1.0)
discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0)
@ -285,13 +288,13 @@ GEM
fast_gettext (1.6.0)
ffaker (2.10.0)
ffi (1.11.1)
flipper (0.13.0)
flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6)
flipper (~> 0.13.0)
flipper-active_support_cache_store (0.13.0)
activesupport (>= 3.2, < 6)
flipper (~> 0.13.0)
flipper (0.17.1)
flipper-active_record (0.17.1)
activerecord (>= 4.2, < 7)
flipper (~> 0.17.1)
flipper-active_support_cache_store (0.17.1)
activesupport (>= 4.2, < 7)
flipper (~> 0.17.1)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
@ -332,10 +335,8 @@ GEM
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.4)
railties (>= 3.2, < 6.0)
foreman (0.84.0)
thor (~> 0.19.1)
font-awesome-rails (4.7.0.5)
railties (>= 3.2, < 6.1)
formatador (0.2.5)
fugit (1.2.1)
et-orbi (~> 1.1, >= 1.1.8)
@ -358,12 +359,12 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
gitaly (1.65.0)
gitaly (1.70.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-labkit (0.5.2)
actionpack (~> 5)
activesupport (~> 5)
gitlab-labkit (0.7.0)
actionpack (>= 5.0.0, < 6.1.0)
activesupport (>= 5.0.0, < 6.1.0)
grpc (~> 1.19)
jaeger-client (~> 0.10)
opentracing (~> 0.4)
@ -400,7 +401,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.7.1)
google-protobuf (3.8.0)
googleapis-common-protos-types (1.0.4)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@ -410,7 +411,7 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
gpgme (2.0.18)
gpgme (2.0.19)
mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
@ -440,11 +441,25 @@ GEM
graphql (~> 1.6)
html-pipeline (~> 2.8)
sass (~> 3.4)
grpc (1.19.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
grpc (1.24.0)
google-protobuf (~> 3.8)
googleapis-common-protos-types (~> 1.0)
gssapi (1.2.0)
ffi (>= 1.0.1)
guard (2.15.1)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
guard-rspec (4.7.3)
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
haml (5.0.4)
temple (>= 0.8.0)
tilt
@ -508,7 +523,7 @@ GEM
atlassian-jwt
multipart-post
oauth (~> 0.5, >= 0.5.0)
jmespath (1.3.1)
jmespath (1.4.0)
js_regex (3.1.1)
character_set (~> 1.1)
regexp_parser (~> 1.1)
@ -560,15 +575,20 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
locale (2.1.2)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.3.0)
loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
mail (2.7.1)
mini_mime (>= 0.1.1)
mail_room (0.9.1)
@ -584,7 +604,7 @@ GEM
mime-types-data (3.2019.0331)
mimemagic (0.3.2)
mini_magick (4.9.5)
mini_mime (1.0.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
msgpack (1.3.1)
@ -597,16 +617,20 @@ GEM
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
nap (1.1.0)
nenv (0.3.0)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (5.2.0)
netrc (0.11.0)
nio4r (2.3.1)
no_proxy_fix (0.1.2)
nokogiri (1.10.4)
nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
nokogiri
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.1)
@ -675,12 +699,12 @@ GEM
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
omniauth_openid_connect (0.3.1)
omniauth_openid_connect (0.3.3)
addressable (~> 2.5)
omniauth (~> 1.3)
omniauth (~> 1.9)
openid_connect (~> 1.1)
open4 (1.3.4)
openid_connect (1.1.6)
openid_connect (1.1.8)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.5.0)
@ -703,12 +727,12 @@ GEM
pg (1.1.4)
po_to_json (1.0.1)
json (>= 1.6.0)
premailer (1.10.4)
premailer (1.11.1)
addressable
css_parser (>= 1.4.10)
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer-rails (1.10.3)
actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
@ -724,7 +748,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.1.0)
public_suffix (3.1.1)
puma (3.12.0)
puma_worker_killer (0.1.0)
get_process_mem (~> 0.2)
@ -734,8 +758,8 @@ GEM
rack (2.0.7)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
rack
rack-attack (6.2.0)
rack (>= 1.0, < 3)
rack-cors (1.0.2)
rack-oauth2 (1.9.3)
activesupport
@ -763,10 +787,10 @@ GEM
bundler (>= 1.3.0)
railties (= 5.2.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
actionview (~> 5.x, >= 5.0.1)
activesupport (~> 5.x)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
actionview (>= 5.0.1.x)
activesupport (>= 5.0.1.x)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
@ -798,25 +822,25 @@ GEM
recaptcha (4.13.1)
json
recursive-open-struct (1.1.0)
redis (4.1.2)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis (4.1.3)
redis-actionpack (5.1.0)
actionpack (>= 4.0, < 7)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.7)
activesupport (>= 3, < 6)
redis-activesupport (5.2.0)
activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2)
redis-namespace (1.6.0)
redis (>= 3.0.4)
redis-rack (2.0.5)
redis-rack (2.0.6)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.6.0)
redis (>= 2.2, < 5)
redis-store (1.8.1)
redis (>= 4, < 5)
regexp_parser (1.5.1)
regexp_property_values (0.3.4)
representable (3.0.4)
@ -824,9 +848,9 @@ GEM
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.1)
responders (2.4.1)
actionpack (>= 4.2.0, < 6.0)
railties (>= 4.2.0, < 6.0)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@ -897,11 +921,12 @@ GEM
ruby-progressbar (1.10.1)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
ruby_dep (1.5.0)
ruby_parser (3.13.1)
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.2.2)
rubyzip (1.3.0)
rugged (0.28.3.1)
safe_yaml (1.0.4)
sanitize (4.6.6)
@ -938,6 +963,7 @@ GEM
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.12.0)
shellany (0.0.1)
shoulda-matchers (4.0.1)
activesupport (>= 4.2.0)
sidekiq (5.2.7)
@ -978,11 +1004,11 @@ GEM
sshkey (2.0.0)
stackprof (0.2.10)
state_machines (0.5.0)
state_machines-activemodel (0.5.1)
activemodel (>= 4.1, < 6.0)
state_machines-activemodel (0.7.1)
activemodel (>= 4.1)
state_machines (>= 0.5.0)
state_machines-activerecord (0.5.1)
activerecord (>= 4.1, < 6.0)
state_machines-activerecord (0.6.0)
activerecord (>= 4.1)
state_machines-activemodel (>= 0.5.0)
swd (1.1.2)
activesupport (>= 3)
@ -1127,12 +1153,13 @@ DEPENDENCIES
creole (~> 0.5.0)
danger (~> 6.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.2.0)
default_value_for (~> 3.2.0)
deckar01-task_list (= 2.2.1)
default_value_for (~> 3.3.0)
derailed_benchmarks
device_detector
devise (~> 4.6)
devise-two-factor (~> 3.0.0)
diff_match_patch (~> 0.1.0)
diffy (~> 3.1.0)
discordrb-webhooks-blackst0ne (~> 3.3)
doorkeeper (~> 4.3)
@ -1149,9 +1176,9 @@ DEPENDENCIES
faraday_middleware-aws-signers-v4
fast_blank
ffaker (~> 2.10)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
flipper (~> 0.17.1)
flipper-active_record (~> 0.17.1)
flipper-active_support_cache_store (~> 0.17.1)
flowdock (~> 0.7)
fog-aliyun (~> 0.3)
fog-aws (~> 3.5)
@ -1161,14 +1188,13 @@ DEPENDENCIES
fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7)
foreman (~> 0.84.0)
fugit (~> 1.2.1)
fuubar (~> 2.2.0)
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.65.0)
gitaly (~> 1.70.0)
github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
@ -1181,8 +1207,8 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.7.1)
gpgme (~> 2.0.18)
google-protobuf (~> 3.8.0)
gpgme (~> 2.0.19)
grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.1)
@ -1190,8 +1216,9 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.9.11)
graphql-docs (~> 1.6.0)
grpc (~> 1.19.0)
grpc (~> 1.24.0)
gssapi
guard-rspec
haml_lint (~> 0.31.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
@ -1226,7 +1253,7 @@ DEPENDENCIES
net-ldap
net-ntp
net-ssh (~> 5.2)
nokogiri (~> 1.10.4)
nokogiri (~> 1.10.5)
oauth2 (~> 1.4)
octokit (~> 4.9)
omniauth (~> 1.8)
@ -1246,17 +1273,17 @@ DEPENDENCIES
omniauth-twitter (~> 1.4)
omniauth-ultraauth (~> 0.0.2)
omniauth_crowd (~> 2.2.0)
omniauth_openid_connect (~> 0.3.1)
omniauth_openid_connect (~> 0.3.3)
org-ruby (~> 0.9.12)
pg (~> 1.1)
premailer-rails (~> 1.9.7)
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.9.10)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
rack (~> 2.0.7)
rack-attack (~> 4.4.1)
rack-attack (~> 6.2.0)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
@ -1275,7 +1302,8 @@ DEPENDENCIES
redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
responders (~> 3.0)
retriable (~> 3.1.2)
rouge (~> 3.11.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
@ -1291,7 +1319,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0)
ruby-progressbar
ruby_parser (~> 3.8)
rubyzip (~> 1.2.2)
rubyzip (~> 1.3.0)
rugged (~> 0.28)
sanitize (~> 4.6)
sassc-rails (~> 2.1.0)
@ -1312,7 +1340,7 @@ DEPENDENCIES
sprockets (~> 3.7.0)
sshkey (~> 2.0)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.5.1)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.10.0)
thin (~> 1.7.0)

43
Guardfile Normal file
View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
# More info at https://github.com/guard/guard#readme
cmd = ENV['SPRING'] ? 'spring rspec' : 'bundle exec rspec'
guard :rspec, cmd: cmd do
require "guard/rspec/dsl"
dsl = Guard::RSpec::Dsl.new(self)
directories %w(app ee lib spec)
# RSpec files
rspec = dsl.rspec
watch(rspec.spec_helper) { rspec.spec_dir }
watch(rspec.spec_support) { rspec.spec_dir }
watch(rspec.spec_files)
# Ruby files
ruby = dsl.ruby
dsl.watch_spec_files_for(ruby.lib_files)
# Rails files
rails = dsl.rails(view_extensions: %w(erb haml slim))
dsl.watch_spec_files_for(rails.app_files)
dsl.watch_spec_files_for(rails.views)
watch(rails.controllers) do |m|
[
rspec.spec.call("routing/#{m[1]}_routing"),
rspec.spec.call("controllers/#{m[1]}_controller")
]
end
# Rails config changes
watch(rails.spec_helper) { rspec.spec_dir }
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
# Capybara features specs
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
end

View file

@ -79,7 +79,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our
Overview and details of feature flag processes in development of GitLab itself is described in [feature flags process documentation](https://docs.gitlab.com/ee/development/feature_flags/process.html).
Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/developing.html).
Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/development.html).
Getting access and how to expose the feature to users is detailed in [controlling feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/controls.html).

View file

@ -1 +1 @@
12.4.6
12.5.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -2,6 +2,8 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
import flash from '~/flash';
import { __ } from '~/locale';
const Api = {
groupsPath: '/api/:version/groups.json',
@ -29,6 +31,7 @@ const Api = {
usersPath: '/api/:version/users.json',
userPath: '/api/:version/users/:id',
userStatusPath: '/api/:version/users/:id/status',
userProjectsPath: '/api/:version/users/:id/projects',
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
applySuggestionPath: '/api/:version/suggestions/:id/apply',
@ -110,10 +113,9 @@ const Api = {
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data }) => {
.then(({ data, headers }) => {
callback(data);
return data;
return { data, headers };
});
},
@ -239,7 +241,8 @@ const Api = {
.get(url, {
params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data));
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
},
commitMultiple(id, data) {
@ -348,6 +351,20 @@ const Api = {
});
},
userProjects(userId, query, options, callback) {
const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
const defaults = {
search: query,
per_page: 20,
};
return axios
.get(url, {
params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
},
branches(id, query = '', options = {}) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));

View file

@ -1,4 +1,4 @@
/* eslint-disable func-names, no-var */
/* eslint-disable func-names */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
@ -12,11 +12,8 @@ import { __ } from '~/locale';
// more than `x` users are referenced.
//
var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
let lastTextareaHeight;
let lastTextareaPreviewed;
function MarkdownPreview() {}
@ -27,14 +24,13 @@ MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.');
MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function($form) {
var mdText;
var preview = $form.find('.js-md-preview');
var url = preview.data('url');
const preview = $form.find('.js-md-preview');
const url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
const mdText = $form.find('textarea.markdown-area').val();
if (mdText === undefined) {
return;
@ -46,7 +42,7 @@ MarkdownPreview.prototype.showPreview = function($form) {
} else {
preview.addClass('md-preview-loading').text(__('Loading...'));
this.fetchMarkdownPreview(mdText, url, response => {
var body;
let body;
if (response.body.length > 0) {
({ body } = response);
} else {
@ -91,8 +87,7 @@ MarkdownPreview.prototype.hideReferencedUsers = function($form) {
};
MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) {
var referencedUsers;
referencedUsers = $form.find('.referenced-users');
const referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) {
referencedUsers.show();
@ -108,8 +103,7 @@ MarkdownPreview.prototype.hideReferencedCommands = function($form) {
};
MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
var referencedCommands;
referencedCommands = $form.find('.referenced-commands');
const referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) {
referencedCommands.html(commands);
referencedCommands.show();
@ -119,15 +113,15 @@ MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
}
};
markdownPreview = new MarkdownPreview();
const markdownPreview = new MarkdownPreview();
previewButtonSelector = '.js-md-preview-button';
writeButtonSelector = '.js-md-write-button';
const previewButtonSelector = '.js-md-preview-button';
const writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function() {
var $form = $(this);
const $form = $(this);
$form.find('textarea.markdown-area').on('input', () => {
markdownPreview.hideReferencedUsers($form);
});
@ -188,7 +182,7 @@ $(document).on('markdown-preview:hide', (e, $form) => {
});
$(document).on('markdown-preview:toggle', (e, keyboardEvent) => {
var $target;
let $target;
$target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
@ -201,16 +195,14 @@ $(document).on('markdown-preview:toggle', (e, keyboardEvent) => {
});
$(document).on('click', previewButtonSelector, function(e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
const $form = $(this).closest('form');
$(document).triggerHandler('markdown-preview:show', [$form]);
});
$(document).on('click', writeButtonSelector, function(e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
const $form = $(this).closest('form');
$(document).triggerHandler('markdown-preview:hide', [$form]);
});

View file

@ -118,8 +118,6 @@ export default class FileTemplateMediator {
}
});
this.setFilename(item.name);
if (this.editor.getValue() !== '') {
this.setTypeSelectorToggleText(item.name);
}
@ -133,14 +131,16 @@ export default class FileTemplateMediator {
selectTemplateFile(selector, query, data) {
const self = this;
const { name } = selector.config;
selector.renderLoading();
this.fetchFileTemplate(selector.config.type, query, data)
.then(file => {
this.setEditorContent(file);
this.setFilename(name);
selector.renderLoaded();
this.typeSelector.setToggleText(selector.config.name);
this.typeSelector.setToggleText(name);
toast(__(`${query} template applied`), {
action: {
text: __('Undo'),

View file

@ -133,7 +133,7 @@ export default {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
gl.boardService
boardsStore
.deleteBoard(this.currentBoard)
.then(() => {
visitUrl(boardsStore.rootPath);
@ -143,7 +143,7 @@ export default {
this.isLoading = false;
});
} else {
gl.boardService
boardsStore
.createBoard(this.board)
.then(resp => resp.data)
.then(data => {

View file

@ -84,7 +84,8 @@ export default {
this.$nextTick(() => {
if (
this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length
this.list.issuesSize > this.list.issues.length &&
this.list.isExpanded
) {
this.list.page += 1;
this.list.getIssues(false).catch(() => {

View file

@ -168,7 +168,7 @@ export default {
}
const recentBoardsPromise = new Promise((resolve, reject) =>
gl.boardService
boardsStore
.recentBoards()
.then(resolve)
.catch(err => {
@ -184,7 +184,7 @@ export default {
}),
);
Promise.all([gl.boardService.allBoards(), recentBoardsPromise])
Promise.all([boardsStore.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;

View file

@ -1,5 +1,6 @@
<script>
import _ from 'underscore';
import { mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@ -63,6 +64,7 @@ export default {
};
},
computed: {
...mapState(['isShowingLabels']),
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
@ -92,7 +94,7 @@ export default {
return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
return this.isShowingLabels && this.issue.labels.find(this.showLabel);
},
issueReferencePath() {
const { referencePath, groupId } = this.issue;

View file

@ -1,6 +1,7 @@
<script>
/* global ListIssue */
import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '~/boards/stores/boards_store';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
import ModalFooter from './footer.vue';
@ -109,7 +110,7 @@ export default {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService
return boardsStore
.getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,

View file

@ -13,6 +13,7 @@ import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import '~/boards/models/milestone';
import '~/boards/models/project';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
import BoardService from 'ee_else_ce/boards/services/board_service';
@ -29,6 +30,7 @@ import {
} from '~/lib/utils/common_utils';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import {
setPromotionState,
setWeigthFetchingState,
@ -67,6 +69,7 @@ export default () => {
BoardSidebar,
BoardAddIssuesModal,
},
store,
data: {
state: boardsStore.state,
loading: true,
@ -314,5 +317,6 @@ export default () => {
}
toggleFocusMode(ModalStore, boardsStore, $boardApp);
toggleLabels();
mountMultipleBoardsSwitcher();
};

View file

@ -50,8 +50,8 @@ class List {
this.page = 1;
this.loading = true;
this.loadingMore = false;
this.issues = [];
this.issuesSize = 0;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) {

View file

@ -0,0 +1,3 @@
export default {
getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
};

View file

@ -1,14 +1,18 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from 'ee_else_ce/boards/stores/state';
import getters from 'ee_else_ce/boards/stores/getters';
import actions from 'ee_else_ce/boards/stores/actions';
import mutations from 'ee_else_ce/boards/stores/mutations';
Vue.use(Vuex);
export default () =>
export const createStore = () =>
new Vuex.Store({
state,
getters,
actions,
mutations,
});
export default createStore();

View file

@ -1,3 +1,3 @@
export default () => ({
// ...
isShowingLabels: true,
});

View file

@ -0,0 +1 @@
export default () => {};

View file

@ -8,11 +8,12 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select';
const Environments = () => import('ee_component/clusters/components/environments.vue');
@ -37,6 +38,8 @@ export default class Clusters {
installJupyterPath,
installKnativePath,
updateKnativePath,
installElasticStackPath,
installCrossplanePath,
installPrometheusPath,
managePrometheusPath,
clusterEnvironmentsPath,
@ -81,11 +84,13 @@ export default class Clusters {
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installCertManagerEndpoint: installCertManagerPath,
installCrossplaneEndpoint: installCrossplanePath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
});
@ -108,8 +113,10 @@ export default class Clusters {
this.ingressDomainHelpText &&
this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');
initProjectSelectDropdown();
Clusters.initDismissableCallout();
initSettingsPanels();
const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area');
if (toggleButtonsContainer) {
setupToggleButtons(toggleButtonsContainer);
@ -222,6 +229,7 @@ export default class Clusters {
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
@ -233,6 +241,7 @@ export default class Clusters {
eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication');
}
@ -399,12 +408,14 @@ export default class Clusters {
}
installApplication({ id: appId, params }) {
return Clusters.validateInstallation(appId, params)
.then(() => {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId);
return this.service.installApplication(appId, params).catch(() => {
// eslint-disable-next-line promise/no-nesting
this.service.installApplication(appId, params).catch(() => {
this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
@ -412,6 +423,19 @@ export default class Clusters {
s__('ClusterIntegration|Request to begin installing failed'),
);
});
})
.catch(error => this.store.updateAppProperty(appId, 'validationError', error));
}
static validateInstallation(appId, params) {
return new Promise((resolve, reject) => {
if (appId === CROSSPLANE && !params.stack) {
reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
return;
}
resolve();
});
}
uninstallApplication({ id: appId }) {
@ -458,6 +482,12 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'hostname', data.hostname);
}
setCrossplaneProviderStack(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'stack', data.stack.code);
this.store.updateAppProperty(appId, 'validationError', null);
}
destroy() {
this.destroyed = true;

View file

@ -9,9 +9,11 @@ import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
@ -19,6 +21,7 @@ import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
export default {
components: {
@ -27,6 +30,7 @@ export default {
LoadingButton,
GlLoadingIcon,
KnativeDomainEditor,
CrossplaneProviderStack,
},
props: {
type: {
@ -88,9 +92,11 @@ export default {
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
crossplaneLogo,
knativeLogo,
meltanoLogo,
prometheusLogo,
elasticStackLogo,
}),
computed: {
isProjectCluster() {
@ -114,6 +120,15 @@ export default {
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
crossplaneInstalled() {
return this.applications.crossplane.status === APPLICATION_STATUS.INSTALLED;
},
enableClusterApplicationCrossplane() {
return gon.features && gon.features.enableClusterApplicationCrossplane;
},
enableClusterApplicationElasticStack() {
return gon.features && gon.features.enableClusterApplicationElasticStack;
},
ingressDescription() {
return sprintf(
_.escape(
@ -146,6 +161,24 @@ export default {
false,
);
},
crossplaneDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{kubectl} or %{gitlabIntegrationLink}.
Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.`,
),
),
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|Gitlab Integration'))}</a>`,
kubectl: `<code>kubectl</code>`,
},
false,
);
},
prometheusDescription() {
return sprintf(
_.escape(
@ -168,9 +201,18 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
elasticStackInstalled() {
return this.applications.elastic_stack.status === APPLICATION_STATUS.INSTALLED;
},
elasticStackKibanaHostname() {
return this.applications.elastic_stack.kibana_hostname;
},
knative() {
return this.applications.knative;
},
crossplane() {
return this.applications.crossplane;
},
cloudRun() {
return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative;
},
@ -207,6 +249,12 @@ export default {
hostname,
});
},
setCrossplaneProviderStack(stack) {
eventHub.$emit('setCrossplaneProviderStack', {
id: 'crossplane',
stack,
});
},
},
};
</script>
@ -430,6 +478,34 @@ export default {
}}
</div>
</application-row>
<application-row
v-if="enableClusterApplicationCrossplane"
id="crossplane"
:logo-url="crossplaneLogo"
:title="applications.crossplane.title"
:status="applications.crossplane.status"
:status-reason="applications.crossplane.statusReason"
:request-status="applications.crossplane.requestStatus"
:request-reason="applications.crossplane.requestReason"
:installed="applications.crossplane.installed"
:install-failed="applications.crossplane.installFailed"
:uninstallable="applications.crossplane.uninstallable"
:uninstall-successful="applications.crossplane.uninstallSuccessful"
:uninstall-failed="applications.crossplane.uninstallFailed"
:install-application-request-params="{ stack: applications.crossplane.stack }"
:disabled="!helmInstalled"
title-link="https://crossplane.io"
>
<template>
<div slot="description">
<p v-html="crossplaneDescription"></p>
<div class="form-group">
<CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
</div>
</div>
</template>
</application-row>
<application-row
id="jupyter"
:logo-url="jupyterhubLogo"
@ -542,6 +618,75 @@ export default {
/>
</div>
</application-row>
<application-row
v-if="enableClusterApplicationElasticStack"
id="elastic_stack"
:logo-url="elasticStackLogo"
:title="applications.elastic_stack.title"
:status="applications.elastic_stack.status"
:status-reason="applications.elastic_stack.statusReason"
:request-status="applications.elastic_stack.requestStatus"
:request-reason="applications.elastic_stack.requestReason"
:version="applications.elastic_stack.version"
:chart-repo="applications.elastic_stack.chartRepo"
:update-available="applications.elastic_stack.updateAvailable"
:installed="applications.elastic_stack.installed"
:install-failed="applications.elastic_stack.installFailed"
:update-successful="applications.elastic_stack.updateSuccessful"
:update-failed="applications.elastic_stack.updateFailed"
:uninstallable="applications.elastic_stack.uninstallable"
:uninstall-successful="applications.elastic_stack.uninstallSuccessful"
:uninstall-failed="applications.elastic_stack.uninstallFailed"
:disabled="!helmInstalled"
:install-application-request-params="{
kibana_hostname: applications.elastic_stack.kibana_hostname,
}"
title-link="https://github.com/helm/charts/tree/master/stable/elastic-stack"
>
<div slot="description">
<p>
{{
s__(
`ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
)
}}
</p>
<template v-if="ingressExternalEndpoint">
<div class="form-group">
<label for="elastic-stack-kibana-hostname">{{
s__('ClusterIntegration|Kibana Hostname')
}}</label>
<div class="input-group">
<input
v-model="applications.elastic_stack.kibana_hostname"
:readonly="elasticStackInstalled"
type="text"
class="form-control js-hostname"
/>
<span class="input-group-btn">
<clipboard-button
:text="elasticStackKibanaHostname"
:title="s__('ClusterIntegration|Copy Kibana Hostname')"
class="js-clipboard-btn"
/>
</span>
</div>
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p>
</div>
</template>
</div>
</application-row>
</div>
</section>
</template>

View file

@ -0,0 +1,93 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '../../locale';
export default {
name: 'CrossplaneProviderStack',
components: {
GlDropdown,
GlDropdownItem,
Icon,
},
props: {
stacks: {
type: Array,
required: false,
default: () => [
{
name: s__('Google Cloud Platform'),
code: 'gcp',
},
{
name: s__('Amazon Web Services'),
code: 'aws',
},
{
name: s__('Microsoft Azure'),
code: 'azure',
},
{
name: s__('Rook'),
code: 'rook',
},
],
},
crossplane: {
type: Object,
required: true,
},
},
computed: {
dropdownText() {
const result = this.stacks.reduce((map, obj) => {
// eslint-disable-next-line no-param-reassign
map[obj.code] = obj.name;
return map;
}, {});
const { stack } = this.crossplane;
if (stack !== '') {
return result[stack];
}
return s__('Select Stack');
},
validationError() {
return this.crossplane.validationError;
},
},
methods: {
selectStack(stack) {
this.$emit('set', stack);
},
},
};
</script>
<template>
<div>
<label>
{{ s__('ClusterIntegration|Enabled stack') }}
</label>
<gl-dropdown
:disabled="crossplane.installed"
:text="dropdownText"
toggle-class="dropdown-menu-toggle gl-field-error-outline"
class="w-100"
:class="{ 'gl-show-field-errors': validationError }"
>
<gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
<span class="ml-1">{{ stack.name }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
<p class="form-text text-muted">
{{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
<a
href="https://crossplane.io/docs/master/stacks-guide.html"
target="_blank"
rel="noopener noreferrer"
>{{ __('Crossplane') }}</a
>
</p>
</div>
</template>

View file

@ -2,7 +2,16 @@
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
import {
HELM,
INGRESS,
CERT_MANAGER,
PROMETHEUS,
RUNNER,
KNATIVE,
JUPYTER,
ELASTIC_STACK,
} from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
@ -28,6 +37,7 @@ const CUSTOM_APP_WARNING_TEXT = {
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
[ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
};
export default {

View file

@ -50,8 +50,19 @@ export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
export const ELASTIC_STACK = 'elastic_stack';
export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
export const APPLICATIONS = [
HELM,
INGRESS,
JUPYTER,
KNATIVE,
RUNNER,
CERT_MANAGER,
PROMETHEUS,
ELASTIC_STACK,
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';

View file

@ -7,10 +7,12 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
cert_manager: this.options.installCertManagerEndpoint,
crossplane: this.options.installCrossplaneEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
elastic_stack: this.options.installElasticStackEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,

View file

@ -5,6 +5,8 @@ import {
JUPYTER,
KNATIVE,
CERT_MANAGER,
ELASTIC_STACK,
CROSSPLANE,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
@ -25,6 +27,7 @@ const applicationInitialState = {
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
validationError: null,
};
export default class ClusterStore {
@ -57,6 +60,11 @@ export default class ClusterStore {
title: s__('ClusterIntegration|Cert-Manager'),
email: null,
},
crossplane: {
...applicationInitialState,
title: s__('ClusterIntegration|Crossplane'),
stack: null,
},
runner: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
@ -85,6 +93,11 @@ export default class ClusterStore {
updateSuccessful: false,
updateFailed: false,
},
elastic_stack: {
...applicationInitialState,
title: s__('ClusterIntegration|Elastic Stack'),
kibana_hostname: null,
},
},
environments: [],
fetchingEnvironments: false,
@ -197,13 +210,15 @@ export default class ClusterStore {
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === CROSSPLANE) {
this.state.applications.crossplane.stack =
this.state.applications.crossplane.stack || serverAppEntry.stack;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname =
this.state.applications.jupyter.hostname ||
serverAppEntry.hostname ||
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
this.state.applications.jupyter.hostname,
serverAppEntry.hostname,
'jupyter',
);
} else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
this.state.applications.knative.hostname =
@ -216,10 +231,26 @@ export default class ClusterStore {
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.updateAvailable = updateAvailable;
} else if (appId === ELASTIC_STACK) {
this.state.applications.elastic_stack.kibana_hostname = this.updateHostnameIfUnset(
this.state.applications.elastic_stack.kibana_hostname,
serverAppEntry.kibana_hostname,
'kibana',
);
}
});
}
updateHostnameIfUnset(current, updated, fallback) {
return (
current ||
updated ||
(this.state.applications.ingress.externalIp
? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
: '')
);
}
toggleFetchEnvironments(isFetching) {
this.state.fetchingEnvironments = isFetching;
}

View file

@ -1,4 +1,4 @@
/* eslint-disable func-names, no-var, no-else-return, consistent-return, one-var, no-return-assign, no-unused-expressions, no-sequences */
/* eslint-disable func-names, no-var, no-else-return, consistent-return, one-var, no-return-assign */
import $ from 'jquery';
@ -9,40 +9,29 @@ const viewModes = ['two-up', 'swipe'];
export default class ImageFile {
constructor(file) {
this.file = file;
this.requestImageInfo(
$('.two-up.view .frame.deleted img', this.file),
(function(_this) {
return function() {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), () => {
_this.initViewModes();
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), () =>
this.requestImageInfo($('.two-up.view .frame.added img', this.file), () => {
this.initViewModes();
// Load two-up view after images are loaded
// so that we can display the correct width and height information
const $images = $('.two-up.view img', _this.file);
const $images = $('.two-up.view img', this.file);
$images.waitForImages(() => {
_this.initView('two-up');
this.initView('two-up');
});
});
};
})(this),
}),
);
}
initViewModes() {
const viewMode = viewModes[0];
$('.view-modes', this.file).removeClass('hide');
$('.view-modes-menu', this.file).on(
'click',
'li',
(function(_this) {
return function(event) {
$('.view-modes-menu', this.file).on('click', 'li', event => {
if (!$(event.currentTarget).hasClass('active')) {
return _this.activateViewMode(event.currentTarget.className);
return this.activateViewMode(event.currentTarget.className);
}
};
})(this),
);
});
return this.activateViewMode(viewMode);
}
@ -51,15 +40,10 @@ export default class ImageFile {
.removeClass('active')
.filter(`.${viewMode}`)
.addClass('active');
return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(
200,
(function(_this) {
return function() {
$(`.view.${viewMode}`, _this.file).fadeIn(200);
return _this.initView(viewMode);
};
})(this),
);
return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(200, () => {
$(`.view.${viewMode}`, this.file).fadeIn(200);
return this.initView(viewMode);
});
}
initView(viewMode) {
@ -103,22 +87,18 @@ export default class ImageFile {
.on('touchmove', dragMove);
}
prepareFrames(view) {
static prepareFrames(view) {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
$('.frame', view)
.each(
(function() {
return function(index, frame) {
.each((index, frame) => {
var height, width;
width = $(frame).width();
height = $(frame).height();
maxWidth = width > maxWidth ? width : maxWidth;
return (maxHeight = height > maxHeight ? height : maxHeight);
};
})(this),
)
})
.css({
width: maxWidth,
height: maxHeight,
@ -128,9 +108,7 @@ export default class ImageFile {
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each(
(function(_this) {
return function(index, wrap) {
return $('.two-up.view .wrap', this.file).each((index, wrap) => {
$('img', wrap).each(function() {
var currentWidth;
currentWidth = $(this).width();
@ -138,24 +116,21 @@ export default class ImageFile {
return $(this).width(availWidth / 2);
}
});
return _this.requestImageInfo($('img', wrap), (width, height) => {
return this.requestImageInfo($('img', wrap), (width, height) => {
$('.image-info .meta-width', wrap).text(`${width}px`);
$('.image-info .meta-height', wrap).text(`${height}px`);
return $('.image-info', wrap).removeClass('hide');
});
};
})(this),
);
});
},
swipe() {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
return $('.swipe.view', this.file).each(
(function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
(ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
return $('.swipe.view', this.file).each((index, view) => {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding;
const ref = ImageFile.prepareFrames(view);
[maxWidth, maxHeight] = ref;
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
@ -175,26 +150,24 @@ export default class ImageFile {
wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
_this.initDraggable($swipeBar, wrapPadding, (e, left) => {
this.initDraggable($swipeBar, wrapPadding, (e, left) => {
if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
$swipeWrap.width(maxWidth + 1 - left);
$swipeBar.css('left', left);
}
});
};
})(this),
);
});
},
'onion-skin': function() {
var dragTrackWidth, maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each(
(function(_this) {
return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref;
(ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
return $('.onion-skin.view', this.file).each((index, view) => {
var $frame, $track, $dragger, $frameAdded, framePadding;
const ref = ImageFile.prepareFrames(view);
[maxWidth, maxHeight] = ref;
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
@ -215,7 +188,7 @@ export default class ImageFile {
$frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, (e, left) => {
this.initDraggable($dragger, framePadding, (e, left) => {
var opacity = left / dragTrackWidth;
if (opacity >= 0 && opacity <= 1) {
@ -223,9 +196,7 @@ export default class ImageFile {
$frameAdded.css('opacity', opacity);
}
});
};
})(this),
);
});
},
};
@ -235,14 +206,7 @@ export default class ImageFile {
if (domImg.complete) {
return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
} else {
return img.on(
'load',
(function(_this) {
return function() {
return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
};
})(this),
);
return img.on('load', () => callback.call(this, domImg.naturalWidth, domImg.naturalHeight));
}
}
}

View file

@ -1,4 +1,4 @@
/* eslint-disable func-names, one-var, no-var, no-else-return */
/* eslint-disable func-names, no-else-return */
import $ from 'jquery';
import { __ } from './locale';
@ -8,9 +8,8 @@ import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
const $dropdown = $(this);
const selected = $dropdown.data('selected');
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
@ -44,17 +43,16 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow(ref) {
var link;
const link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', ref);
if (ref.header != null) {
return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else {
link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', ref);
return $('<li />').append(link);
}
},

View file

@ -41,7 +41,7 @@ export default {
noForkText() {
return sprintf(
__(
"To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.",
"To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private.",
),
{ link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
false,

View file

@ -0,0 +1,227 @@
<script>
import { __ } from '~/locale';
import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
export default {
components: {
GlAreaChart,
GlLoadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
branch: {
type: String,
required: true,
},
},
data() {
return {
masterChart: null,
individualCharts: [],
svgs: {},
masterChartHeight: 264,
individualChartHeight: 216,
};
},
computed: {
...mapState(['chartData', 'loading']),
...mapGetters(['showChart', 'parsedData']),
masterChartData() {
const data = {};
this.xAxisRange.forEach(date => {
data[date] = this.parsedData.total[date] || 0;
});
return [
{
name: __('Commits'),
data: Object.entries(data),
},
];
},
masterChartOptions() {
return {
...this.getCommonChartOptions(true),
yAxis: {
name: __('Number of commits'),
},
grid: {
bottom: 64,
left: 64,
right: 20,
top: 20,
},
};
},
individualChartsData() {
const maxNumberOfIndividualContributorsCharts = 100;
return Object.keys(this.parsedData.byAuthor)
.map(name => {
const author = this.parsedData.byAuthor[name];
return {
name,
email: author.email,
commits: author.commits,
dates: [
{
name: __('Commits'),
data: this.xAxisRange.map(date => [date, author.dates[date] || 0]),
},
],
};
})
.sort((a, b) => b.commits - a.commits)
.slice(0, maxNumberOfIndividualContributorsCharts);
},
individualChartOptions() {
return {
...this.getCommonChartOptions(false),
yAxis: {
name: __('Commits'),
max: this.individualChartYAxisMax,
},
grid: {
bottom: 27,
left: 64,
right: 20,
top: 8,
},
};
},
individualChartYAxisMax() {
return this.individualChartsData.reduce((acc, item) => {
const values = item.dates[0].data.map(value => value[1]);
return Math.max(acc, ...values);
}, 0);
},
xAxisRange() {
const dates = Object.keys(this.parsedData.total).sort((a, b) => new Date(a) - new Date(b));
const firstContributionDate = new Date(dates[0]);
const lastContributionDate = new Date(dates[dates.length - 1]);
return getDatesInRange(firstContributionDate, lastContributionDate, dateFormatter);
},
firstContributionDate() {
return this.xAxisRange[0];
},
lastContributionDate() {
return this.xAxisRange[this.xAxisRange.length - 1];
},
charts() {
return _.uniq(this.individualCharts);
},
},
mounted() {
this.fetchChartData(this.endpoint);
},
methods: {
...mapActions(['fetchChartData']),
getCommonChartOptions(isMasterChart) {
return {
xAxis: {
type: 'time',
name: '',
data: this.xAxisRange,
axisLabel: {
formatter: xAxisLabelFormatter,
showMaxLabel: false,
showMinLabel: false,
},
boundaryGap: false,
splitNumber: isMasterChart ? 24 : 18,
// 28 days
minInterval: 28 * 86400 * 1000,
min: this.firstContributionDate,
max: this.lastContributionDate,
},
};
},
setSvg(name) {
return getSvgIconPathContent(name)
.then(path => {
if (path) {
this.$set(this.svgs, name, `path://${path}`);
}
})
.catch(() => {});
},
onMasterChartCreated(chart) {
this.masterChart = chart;
this.setSvg('scroll-handle')
.then(() => {
this.masterChart.setOption({
dataZoom: [
{
type: 'slider',
handleIcon: this.svgs['scroll-handle'],
},
],
});
})
.catch(() => {});
this.masterChart.on('datazoom', _.debounce(this.setIndividualChartsZoom, 200));
},
onIndividualChartCreated(chart) {
this.individualCharts.push(chart);
},
setIndividualChartsZoom(options) {
this.charts.forEach(chart =>
chart.setOption(
{
dataZoom: {
start: options.start,
end: options.end,
show: false,
},
},
{ lazyUpdate: true },
),
);
},
},
};
</script>
<template>
<div>
<div v-if="loading" class="contributors-loader text-center">
<gl-loading-icon :inline="true" :size="4" />
</div>
<div v-else-if="showChart" class="contributors-charts">
<h4>{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<div>
<gl-area-chart
:data="masterChartData"
:option="masterChartOptions"
:height="masterChartHeight"
@created="onMasterChartCreated"
/>
</div>
<div class="row">
<div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6">
<h4>{{ contributor.name }}</h4>
<p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
<gl-area-chart
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,23 @@
import Vue from 'vue';
import ContributorsGraphs from './components/contributors.vue';
import store from './stores';
export default () => {
const el = document.querySelector('.js-contributors-graph');
if (!el) return null;
return new Vue({
el,
store,
render(createElement) {
return createElement(ContributorsGraphs, {
props: {
endpoint: el.dataset.projectGraphPath,
branch: el.dataset.projectBranch,
},
});
},
});
};

View file

@ -0,0 +1,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
fetchChartData(endpoint) {
return axios.get(endpoint);
},
};

View file

@ -0,0 +1,20 @@
import flash from '~/flash';
import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, true);
return service
.fetchChartData(endpoint)
.then(res => res.data)
.then(data => {
commit(types.SET_CHART_DATA, data);
commit(types.SET_LOADING_STATE, false);
})
.catch(() => flash(__('An error occurred while loading chart data')));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,33 @@
export const showChart = state => Boolean(!state.loading && state.chartData);
export const parsedData = state => {
const byAuthor = {};
const total = {};
state.chartData.forEach(({ date, author_name, author_email }) => {
total[date] = total[date] ? total[date] + 1 : 1;
const authorData = byAuthor[author_name];
if (!authorData) {
byAuthor[author_name] = {
email: author_email.toLowerCase(),
commits: 1,
dates: {
[date]: 1,
},
};
} else {
authorData.commits += 1;
authorData.dates[date] = authorData.dates[date] ? authorData.dates[date] + 1 : 1;
}
});
return {
total,
byAuthor,
};
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,18 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
export default createStore();

View file

@ -0,0 +1,3 @@
export const SET_CHART_DATA = 'SET_CHART_DATA';
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
export const SET_ACTIVE_BRANCH = 'SET_ACTIVE_BRANCH';

View file

@ -0,0 +1,17 @@
import * as types from './mutation_types';
export default {
[types.SET_LOADING_STATE](state, value) {
state.loading = value;
},
[types.SET_CHART_DATA](state, chartData) {
Object.assign(state, {
chartData,
});
},
[types.SET_ACTIVE_BRANCH](state, branch) {
Object.assign(state, {
branch,
});
},
};

View file

@ -0,0 +1,5 @@
export default () => ({
loading: false,
chartData: null,
branch: 'master',
});

View file

@ -0,0 +1,30 @@
import { getMonthNames } from '~/lib/utils/datetime_utility';
/**
* Converts provided string to date and returns formatted value as a year for date in January and month name for the rest
* @param {String}
* @returns {String} - formatted value
*
* xAxisLabelFormatter('01-12-2019') will return '2019'
* xAxisLabelFormatter('02-12-2019') will return 'Feb'
* xAxisLabelFormatter('07-12-2019') will return 'Jul'
*/
export const xAxisLabelFormatter = val => {
const date = new Date(val);
const month = date.getUTCMonth();
const year = date.getUTCFullYear();
return month === 0 ? `${year}` : getMonthNames(true)[month];
};
/**
* Formats provided date to YYYY-MM-DD format
* @param {Date}
* @returns {String} - formatted value
*/
export const dateFormatter = date => {
const year = date.getUTCFullYear();
const month = date.getUTCMonth();
const day = date.getUTCDate();
return `${year}-${`0${month + 1}`.slice(-2)}-${`0${day}`.slice(-2)}`;
};

View file

@ -2,14 +2,19 @@
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { GlIcon } from '@gitlab/ui';
const findItem = (items, valueProp, value) => items.find(item => item[valueProp] === value);
const toArray = value => [].concat(value);
const itemsProp = (items, prop) => items.map(item => item[prop]);
const defaultSearchFn = (searchQuery, labelProp) => item =>
item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
export default {
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
GlIcon,
},
props: {
fieldName: {
@ -28,7 +33,7 @@ export default {
default: '',
},
value: {
type: [Object, String],
type: [Object, Array, String],
required: false,
default: () => null,
},
@ -72,6 +77,11 @@ export default {
required: false,
default: false,
},
multiple: {
type: Boolean,
required: false,
default: false,
},
errorMessage: {
type: String,
required: false,
@ -90,12 +100,11 @@ export default {
searchFn: {
type: Function,
required: false,
default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1,
default: defaultSearchFn,
},
},
data() {
return {
selectedItem: findItem(this.items, this.value),
searchQuery: '',
};
},
@ -109,36 +118,52 @@ export default {
return this.disabledText;
}
if (!this.selectedItem) {
if (!this.selectedItems.length) {
return this.placeholder;
}
return this.selectedItemLabel;
return this.selectedItemsLabels;
},
results() {
if (!this.items) {
return [];
}
return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
},
selectedItems() {
const valueProp = this.valueProperty;
const valueList = toArray(this.value);
const items = this.getItemsOrEmptyList();
return this.items.filter(this.searchFn(this.searchQuery));
return items.filter(item => valueList.some(value => item[valueProp] === value));
},
selectedItemLabel() {
return this.selectedItem && this.selectedItem[this.labelProperty];
selectedItemsLabels() {
return itemsProp(this.selectedItems, this.labelProperty).join(', ');
},
selectedItemValue() {
return (this.selectedItem && this.selectedItem[this.valueProperty]) || '';
},
},
watch: {
value(value) {
this.selectedItem = findItem(this.items, this.valueProperty, value);
selectedItemsValues() {
return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
methods: {
select(item) {
this.selectedItem = item;
getItemsOrEmptyList() {
return this.items || [];
},
selectSingle(item) {
this.$emit('input', item[this.valueProperty]);
},
selectMultiple(item) {
const value = toArray(this.value);
const itemValue = item[this.valueProperty];
const itemValueIndex = value.indexOf(itemValue);
if (itemValueIndex > -1) {
value.splice(itemValueIndex, 1);
} else {
value.push(itemValue);
}
this.$emit('input', value);
},
isSelected(item) {
return this.selectedItems.includes(item);
},
},
};
</script>
@ -146,7 +171,7 @@ export default {
<template>
<div>
<div class="js-gcp-machine-type-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemValue" />
<dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="disabled"
@ -158,15 +183,28 @@ export default {
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
<span class="js-empty-text menu-item">
{{ emptyText }}
</span>
<span class="js-empty-text menu-item">{{ emptyText }}</span>
</li>
<li v-for="item in results" :key="item.id">
<button class="js-dropdown-item" type="button" @click.prevent="select(item)">
<slot name="item" :item="item">
{{ item.name }}
</slot>
<button
v-if="multiple"
class="js-dropdown-item d-flex align-items-center"
type="button"
@click.stop.prevent="selectMultiple(item)"
>
<gl-icon
:class="[{ invisible: !isSelected(item) }, 'mr-1']"
name="mobile-issue-close"
/>
<slot name="item" :item="item">{{ item.name }}</slot>
</button>
<button
v-else
class="js-dropdown-item"
type="button"
@click.prevent="selectSingle(item)"
>
<slot name="item" :item="item">{{ item.name }}</slot>
</button>
</li>
</ul>
@ -182,8 +220,7 @@ export default {
'text-muted': !hasErrors,
},
]"
>{{ errorMessage }}</span
>
{{ errorMessage }}
</span>
</div>
</template>

View file

@ -1,4 +1,5 @@
<script>
import { mapState } from 'vuex';
import ServiceCredentialsForm from './service_credentials_form.vue';
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
@ -16,14 +17,37 @@ export default {
type: String,
required: true,
},
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
computed: {
...mapState(['hasCredentials']),
},
};
</script>
<template>
<div class="js-create-eks-cluster">
<eks-cluster-configuration-form
v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
:external-link-icon="externalLinkIcon"
/>
<service-credentials-form
v-else
:create-role-arn-help-path="createRoleArnHelpPath"
:account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
:external-link-icon="externalLinkIcon"
/>
</div>
</template>

View file

@ -4,8 +4,8 @@ import { sprintf, s__ } from '~/locale';
import _ from 'underscore';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
import RegionDropdown from './region_dropdown.vue';
import { KUBERNETES_VERSIONS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
const { mapState: mapRegionsState, mapActions: mapRegionsActions } = createNamespacedHelpers(
@ -22,13 +22,17 @@ const {
mapState: mapSecurityGroupsState,
mapActions: mapSecurityGroupsActions,
} = createNamespacedHelpers('securityGroups');
const {
mapState: mapInstanceTypesState,
mapActions: mapInstanceTypesActions,
} = createNamespacedHelpers('instanceTypes');
export default {
components: {
ClusterFormDropdown,
RegionDropdown,
GlFormInput,
GlFormCheckbox,
LoadingButton,
},
props: {
gitlabManagedClusterHelpPath: {
@ -39,6 +43,10 @@ export default {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
computed: {
...mapState([
@ -51,7 +59,10 @@ export default {
'selectedSubnet',
'selectedRole',
'selectedSecurityGroup',
'selectedInstanceType',
'nodeCount',
'gitlabManagedCluster',
'isCreatingCluster',
]),
...mapRolesState({
roles: 'items',
@ -83,6 +94,11 @@ export default {
isLoadingSecurityGroups: 'isLoadingItems',
loadingSecurityGroupsError: 'loadingItemsError',
}),
...mapInstanceTypesState({
instanceTypes: 'items',
isLoadingInstanceTypes: 'isLoadingItems',
loadingInstanceTypesError: 'loadingItemsError',
}),
kubernetesVersions() {
return KUBERNETES_VERSIONS;
},
@ -98,6 +114,27 @@ export default {
securityGroupDropdownDisabled() {
return !this.selectedVpc;
},
createClusterButtonDisabled() {
return (
!this.clusterName ||
!this.environmentScope ||
!this.kubernetesVersion ||
!this.selectedRegion ||
!this.selectedKeyPair ||
!this.selectedVpc ||
!this.selectedSubnet ||
!this.selectedRole ||
!this.selectedSecurityGroup ||
!this.selectedInstanceType ||
!this.nodeCount ||
this.isCreatingCluster
);
},
createClusterButtonLabel() {
return this.isCreatingCluster
? s__('ClusterIntegration|Creating Kubernetes cluster')
: s__('ClusterIntegration|Create Kubernetes cluster');
},
kubernetesIntegrationHelpText() {
const escapedUrl = _.escape(this.kubernetesIntegrationHelpPath);
@ -115,11 +152,26 @@ export default {
roleDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}.',
'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
'<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#role-create" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
regionsDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}.',
),
{
startLink:
'<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@ -128,11 +180,12 @@ export default {
keyPairDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services%{endLink}.',
'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}.',
),
{
startLink:
'<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@ -141,11 +194,12 @@ export default {
vpcDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services%{endLink}.',
'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#vpc" target="_blank" rel="noopener noreferrer">',
'<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@ -154,11 +208,12 @@ export default {
subnetDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Choose the %{startLink}subnets%{endLink} in your VPC where your worker nodes will run.',
'ClusterIntegration|Choose the %{startLink}subnets %{externalLinkIcon} %{endLink} in your VPC where your worker nodes will run.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#subnets" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@ -167,11 +222,26 @@ export default {
securityGroupDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Choose the %{startLink}security groups%{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
'ClusterIntegration|Choose the %{startLink}security group %{externalLinkIcon} %{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#securityGroups" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
instanceTypesDropdownHelpText() {
return sprintf(
s__(
'ClusterIntegration|Choose the worker node %{startLink}instance type %{externalLinkIcon} %{endLink}.',
),
{
startLink:
'<a href="https://aws.amazon.com/ec2/instance-types" target="_blank" rel="noopener noreferrer">',
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@ -195,9 +265,12 @@ export default {
mounted() {
this.fetchRegions();
this.fetchRoles();
this.fetchInstanceTypes();
},
methods: {
...mapActions([
'createCluster',
'signOut',
'setClusterName',
'setEnvironmentScope',
'setKubernetesVersion',
@ -207,6 +280,8 @@ export default {
'setRole',
'setKeyPair',
'setSecurityGroup',
'setInstanceType',
'setNodeCount',
'setGitlabManagedCluster',
]),
...mapRegionsActions({ fetchRegions: 'fetchItems' }),
@ -215,15 +290,22 @@ export default {
...mapRolesActions({ fetchRoles: 'fetchItems' }),
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
...mapInstanceTypesActions({ fetchInstanceTypes: 'fetchItems' }),
setRegionAndFetchVpcsAndKeyPairs(region) {
this.setRegion({ region });
this.setVpc({ vpc: null });
this.setKeyPair({ keyPair: null });
this.setSubnet({ subnet: null });
this.setSecurityGroup({ securityGroup: null });
this.fetchVpcs({ region });
this.fetchKeyPairs({ region });
},
setVpcAndFetchSubnets(vpc) {
this.setVpc({ vpc });
this.fetchSubnets({ vpc });
this.fetchSecurityGroups({ vpc });
this.setSubnet({ subnet: null });
this.setSecurityGroup({ securityGroup: null });
this.fetchSubnets({ vpc, region: this.selectedRegion });
this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
},
},
};
@ -233,7 +315,12 @@ export default {
<h2>
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h2>
<p v-html="kubernetesIntegrationHelpText"></p>
<div class="mb-3" v-html="kubernetesIntegrationHelpText"></div>
<div class="mb-3">
<button class="btn btn-link js-sign-out" @click.prevent="signOut()">
{{ s__('ClusterIntegration|Select a different AWS role') }}
</button>
</div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
s__('ClusterIntegration|Kubernetes cluster name')
@ -273,7 +360,7 @@ export default {
<cluster-form-dropdown
field-id="eks-role"
field-name="eks-role"
:input="selectedRole"
:value="selectedRole"
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
@ -288,13 +375,21 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Region') }}</label>
<region-dropdown
<cluster-form-dropdown
field-id="eks-region"
field-name="eks-region"
:value="selectedRegion"
:regions="regions"
:error="loadingRegionsError"
:items="regions"
:loading="isLoadingRegions"
:loading-text="s__('ClusterIntegration|Loading Regions')"
:placeholder="s__('ClusterIntergation|Select a region')"
:search-field-placeholder="s__('ClusterIntegration|Search regions')"
:empty-text="s__('ClusterIntegration|No region found')"
:has-errors="Boolean(loadingRegionsError)"
:error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
@input="setRegionAndFetchVpcsAndKeyPairs($event)"
/>
<p class="form-text text-muted" v-html="regionsDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-key-pair">{{
@ -303,7 +398,7 @@ export default {
<cluster-form-dropdown
field-id="eks-key-pair"
field-name="eks-key-pair"
:input="selectedKeyPair"
:value="selectedKeyPair"
:items="keyPairs"
:disabled="keyPairDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
@ -323,7 +418,7 @@ export default {
<cluster-form-dropdown
field-id="eks-vpc"
field-name="eks-vpc"
:input="selectedVpc"
:value="selectedVpc"
:items="vpcs"
:loading="isLoadingVpcs"
:disabled="vpcDropdownDisabled"
@ -339,11 +434,12 @@ export default {
<p class="form-text text-muted" v-html="vpcDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnet') }}</label>
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
<cluster-form-dropdown
field-id="eks-subnet"
field-name="eks-subnet"
:input="selectedSubnet"
multiple
:value="selectedSubnet"
:items="subnets"
:loading="isLoadingSubnets"
:disabled="subnetDropdownDisabled"
@ -360,12 +456,12 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-security-group">{{
s__('ClusterIntegration|Security groups')
s__('ClusterIntegration|Security group')
}}</label>
<cluster-form-dropdown
field-id="eks-security-group"
field-name="eks-security-group"
:input="selectedSecurityGroup"
:value="selectedSecurityGroup"
:items="securityGroups"
:loading="isLoadingSecurityGroups"
:disabled="securityGroupDropdownDisabled"
@ -382,6 +478,39 @@ export default {
/>
<p class="form-text text-muted" v-html="securityGroupDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-instance-type">{{
s__('ClusterIntegration|Instance type')
}}</label>
<cluster-form-dropdown
field-id="eks-instance-type"
field-name="eks-instance-type"
:value="selectedInstanceType"
:items="instanceTypes"
:loading="isLoadingInstanceTypes"
:loading-text="s__('ClusterIntegration|Loading instance types')"
:placeholder="s__('ClusterIntergation|Select an instance type')"
:search-field-placeholder="s__('ClusterIntegration|Search instance types')"
:empty-text="s__('ClusterIntegration|No instance type found')"
:has-errors="Boolean(loadingInstanceTypesError)"
:error-message="s__('ClusterIntegration|Could not load instance types')"
@input="setInstanceType({ instanceType: $event })"
/>
<p class="form-text text-muted" v-html="instanceTypesDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-node-count">{{
s__('ClusterIntegration|Number of nodes')
}}</label>
<gl-form-input
id="eks-node-count"
type="number"
min="1"
step="1"
:value="nodeCount"
@input="setNodeCount({ nodeCount: $event })"
/>
</div>
<div class="form-group">
<gl-form-checkbox
:checked="gitlabManagedCluster"
@ -390,5 +519,14 @@ export default {
>
<p class="form-text text-muted" v-html="gitlabManagedHelpText"></p>
</div>
<div class="form-group">
<loading-button
class="js-create-cluster btn-success"
:disabled="createClusterButtonDisabled"
:loading="isCreatingCluster"
:label="createClusterButtonLabel"
@click="createCluster()"
/>
</div>
</form>
</template>

View file

@ -1,63 +0,0 @@
<script>
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
export default {
components: {
ClusterFormDropdown,
},
props: {
regions: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
default: null,
},
},
computed: {
hasErrors() {
return Boolean(this.error);
},
helpText() {
return sprintf(
s__('ClusterIntegration|Learn more about %{startLink}Regions%{endLink}.'),
{
startLink:
'<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div>
<cluster-form-dropdown
field-id="eks-region"
field-name="eks-region"
:items="regions"
:loading="loading"
:loading-text="s__('ClusterIntegration|Loading Regions')"
:placeholder="s__('ClusterIntergation|Select a region')"
:search-field-placeholder="s__('ClusterIntegration|Search regions')"
:empty-text="s__('ClusterIntegration|No region found')"
:has-errors="hasErrors"
:error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
v-bind="$attrs"
v-on="$listeners"
/>
<p class="form-text text-muted" v-html="helpText"></p>
</div>
</template>

View file

@ -1,3 +1,141 @@
<script>
import { GlFormInput } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
components: {
GlFormInput,
LoadingButton,
ClipboardButton,
},
props: {
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
data() {
return {
roleArn: '',
};
},
computed: {
...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
submitButtonDisabled() {
return this.isCreatingRole || !this.roleArn;
},
submitButtonLabel() {
return this.isCreatingRole
? __('Authenticating')
: s__('ClusterIntegration|Authenticate with AWS');
},
accountAndExternalIdsHelpText() {
const escapedUrl = _.escape(this.accountAndExternalIdsHelpPath);
return sprintf(
s__(
'ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}',
),
{
startAwsLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
provisionRoleArnHelpText() {
const escapedUrl = _.escape(this.createRoleArnHelpPath);
return sprintf(
s__(
'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}',
),
{
startAwsLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
},
methods: {
...mapActions(['createRole']),
},
};
</script>
<template>
<form name="service-credentials-form"></form>
<form name="service-credentials-form" @submit.prevent="createRole({ roleArn, externalId })">
<h2>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h2>
<p>
{{
s__(
'ClusterIntegration|You must grant access to your organizations AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN.',
)
}}
</p>
<div v-if="createRoleError" class="js-invalid-credentials bs-callout bs-callout-danger">
{{ createRoleError }}
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
<div class="input-group">
<gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
<div class="input-group-append">
<clipboard-button
:text="accountId"
:title="__('Copy Account ID to clipboard')"
class="input-group-text js-copy-account-id-button"
/>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="eks-external-id">{{ __('External ID') }}</label>
<div class="input-group">
<gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
<div class="input-group-append">
<clipboard-button
:text="externalId"
:title="__('Copy External ID to clipboard')"
class="input-group-text js-copy-external-id-button"
/>
</div>
</div>
</div>
<div class="col-12 mb-3 mt-n3">
<p class="form-text text-muted" v-html="accountAndExternalIdsHelpText"></p>
</div>
</div>
<div class="form-group">
<label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div>
<loading-button
class="js-submit-service-credentials btn-success"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
:label="submitButtonLabel"
/>
</form>
</template>

View file

@ -1,7 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export const KUBERNETES_VERSIONS = [
{ name: '1.14', value: '1.14' },
{ name: '1.13', value: '1.13' },
{ name: '1.12', value: '1.12' },
{ name: '1.11', value: '1.11' },
];
export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];

View file

@ -1,16 +1,54 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import CreateEksCluster from './components/create_eks_cluster.vue';
import createStore from './store';
Vue.use(Vuex);
export default el => {
const { gitlabManagedClusterHelpPath, kubernetesIntegrationHelpPath } = el.dataset;
const {
gitlabManagedClusterHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
getRolesPath,
getRegionsPath,
getKeyPairsPath,
getVpcsPath,
getSubnetsPath,
getSecurityGroupsPath,
getInstanceTypesPath,
externalId,
accountId,
hasCredentials,
createRolePath,
createClusterPath,
signOutPath,
externalLinkIcon,
} = el.dataset;
return new Vue({
el,
store: createStore(),
store: createStore({
initialState: {
hasCredentials: parseBoolean(hasCredentials),
externalId,
accountId,
createRolePath,
createClusterPath,
signOutPath,
},
apiPaths: {
getRolesPath,
getRegionsPath,
getKeyPairsPath,
getVpcsPath,
getSubnetsPath,
getSecurityGroupsPath,
getInstanceTypesPath,
},
}),
components: {
CreateEksCluster,
},
@ -19,6 +57,9 @@ export default el => {
props: {
gitlabManagedClusterHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalLinkIcon,
},
});
},

View file

@ -1,84 +1,58 @@
import EC2 from 'aws-sdk/clients/ec2';
import IAM from 'aws-sdk/clients/iam';
import axios from '~/lib/utils/axios_utils';
export const fetchRoles = () => {
const iam = new IAM();
return iam
.listRoles()
.promise()
.then(({ Roles: roles }) => roles.map(({ RoleName: name }) => ({ name })));
};
export const fetchKeyPairs = () => {
const ec2 = new EC2();
return ec2
.describeKeyPairs()
.promise()
.then(({ KeyPairs: keyPairs }) => keyPairs.map(({ RegionName: name }) => ({ name })));
};
export const fetchRegions = () => {
const ec2 = new EC2();
return ec2
.describeRegions()
.promise()
.then(({ Regions: regions }) =>
regions.map(({ RegionName: name }) => ({
name,
value: name,
export default apiPaths => ({
fetchRoles() {
return axios
.get(apiPaths.getRolesPath)
.then(({ data: { roles } }) =>
roles.map(({ role_name: name, arn: value }) => ({ name, value })),
);
},
fetchKeyPairs({ region }) {
return axios
.get(apiPaths.getKeyPairsPath, { params: { region } })
.then(({ data: { key_pairs: keyPairs } }) =>
keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })),
);
},
fetchRegions() {
return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) =>
regions.map(({ region_name }) => ({
name: region_name,
value: region_name,
})),
);
};
export const fetchVpcs = () => {
const ec2 = new EC2();
return ec2
.describeVpcs()
.promise()
.then(({ Vpcs: vpcs }) =>
vpcs.map(({ VpcId: id }) => ({
value: id,
name: id,
},
fetchVpcs({ region }) {
return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) =>
vpcs.map(({ vpc_id }) => ({
value: vpc_id,
name: vpc_id,
})),
);
};
export const fetchSubnets = ({ vpc }) => {
const ec2 = new EC2();
return ec2
.describeSubnets({
Filters: [
{
Name: 'vpc-id',
Values: [vpc],
},
],
})
.promise()
.then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ id, name: id })));
};
export const fetchSecurityGroups = ({ vpc }) => {
const ec2 = new EC2();
return ec2
.describeSecurityGroups({
Filters: [
{
Name: 'vpc-id',
Values: [vpc],
},
],
})
.promise()
.then(({ SecurityGroups: securityGroups }) =>
securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
fetchSubnets({ vpc, region }) {
return axios
.get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } })
.then(({ data: { subnets } }) =>
subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })),
);
};
export default () => {};
},
fetchSecurityGroups({ vpc, region }) {
return axios
.get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } })
.then(({ data: { security_groups: securityGroups } }) =>
securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })),
);
},
fetchInstanceTypes() {
return axios
.get(apiPaths.getInstanceTypesPath)
.then(({ data: { instance_types: instanceTypes } }) =>
instanceTypes.map(({ instance_type_name }) => ({
name: instance_type_name,
value: instance_type_name,
})),
);
},
});

View file

@ -1,4 +1,12 @@
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
const getErrorMessage = data => {
const errorKey = Object.keys(data)[0];
return data[errorKey][0];
};
export const setClusterName = ({ commit }, payload) => {
commit(types.SET_CLUSTER_NAME, payload);
@ -12,6 +20,68 @@ export const setKubernetesVersion = ({ commit }, payload) => {
commit(types.SET_KUBERNETES_VERSION, payload);
};
export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
dispatch('requestCreateRole');
return axios
.post(createRolePath, {
role_arn: payload.roleArn,
role_external_id: payload.externalId,
})
.then(() => dispatch('createRoleSuccess'))
.catch(error => dispatch('createRoleError', { error }));
};
export const requestCreateRole = ({ commit }) => {
commit(types.REQUEST_CREATE_ROLE);
};
export const createRoleSuccess = ({ commit }) => {
commit(types.CREATE_ROLE_SUCCESS);
};
export const createRoleError = ({ commit }, payload) => {
commit(types.CREATE_ROLE_ERROR, payload);
};
export const createCluster = ({ dispatch, state }) => {
dispatch('requestCreateCluster');
return axios
.post(state.createClusterPath, {
name: state.clusterName,
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
provider_aws_attributes: {
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
role_arn: state.selectedRole,
key_name: state.selectedKeyPair,
security_group_id: state.selectedSecurityGroup,
instance_type: state.selectedInstanceType,
num_nodes: state.nodeCount,
},
})
.then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
.catch(({ response: { data } }) => {
dispatch('createClusterError', data);
});
};
export const requestCreateCluster = ({ commit }) => {
commit(types.REQUEST_CREATE_CLUSTER);
};
export const createClusterSuccess = (_, location) => {
window.location.assign(location);
};
export const createClusterError = ({ commit }, error) => {
commit(types.CREATE_CLUSTER_ERROR, error);
createFlash(getErrorMessage(error));
};
export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload);
};
@ -40,4 +110,16 @@ export const setGitlabManagedCluster = ({ commit }, payload) => {
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
};
export default () => {};
export const setInstanceType = ({ commit }, payload) => {
commit(types.SET_INSTANCE_TYPE, payload);
};
export const setNodeCount = ({ commit }, payload) => {
commit(types.SET_NODE_COUNT, payload);
};
export const signOut = ({ commit, state: { signOutPath } }) =>
axios
.delete(signOutPath)
.then(() => commit(types.SIGN_OUT))
.catch(({ response: { data } }) => createFlash(getErrorMessage(data)));

View file

@ -6,14 +6,16 @@ import state from './state';
import clusterDropdownStore from './cluster_dropdown';
import * as awsServices from '../services/aws_services_facade';
import awsServicesFactory from '../services/aws_services_facade';
const createStore = () =>
new Vuex.Store({
const createStore = ({ initialState, apiPaths }) => {
const awsServices = awsServicesFactory(apiPaths);
return new Vuex.Store({
actions,
getters,
mutations,
state: state(),
state: Object.assign(state(), initialState),
modules: {
roles: {
namespaced: true,
@ -39,7 +41,12 @@ const createStore = () =>
namespaced: true,
...clusterDropdownStore(awsServices.fetchSecurityGroups),
},
instanceTypes: {
namespaced: true,
...clusterDropdownStore(awsServices.fetchInstanceTypes),
},
},
});
};
export default createStore;

View file

@ -7,4 +7,13 @@ export const SET_KEY_PAIR = 'SET_KEY_PAIR';
export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE';
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
export const SET_NODE_COUNT = 'SET_NODE_COUNT';
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
export const SIGN_OUT = 'SIGN_OUT';
export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';

View file

@ -28,7 +28,39 @@ export default {
[types.SET_SECURITY_GROUP](state, { securityGroup }) {
state.selectedSecurityGroup = securityGroup;
},
[types.SET_INSTANCE_TYPE](state, { instanceType }) {
state.selectedInstanceType = instanceType;
},
[types.SET_NODE_COUNT](state, { nodeCount }) {
state.nodeCount = nodeCount;
},
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
state.gitlabManagedCluster = gitlabManagedCluster;
},
[types.REQUEST_CREATE_ROLE](state) {
state.isCreatingRole = true;
state.createRoleError = null;
state.hasCredentials = false;
},
[types.CREATE_ROLE_SUCCESS](state) {
state.isCreatingRole = false;
state.createRoleError = null;
state.hasCredentials = true;
},
[types.CREATE_ROLE_ERROR](state, { error }) {
state.isCreatingRole = false;
state.createRoleError = error;
state.hasCredentials = false;
},
[types.REQUEST_CREATE_CLUSTER](state) {
state.isCreatingCluster = true;
state.createClusterError = null;
},
[types.CREATE_CLUSTER_ERROR](state, { error }) {
state.isCreatingCluster = false;
state.createClusterError = error;
},
[types.SIGN_OUT](state) {
state.hasCredentials = false;
},
};

View file

@ -1,18 +1,31 @@
import { KUBERNETES_VERSIONS } from '../constants';
const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
export default () => ({
isValidatingCredentials: false,
validCredentials: false,
createRolePath: null,
isCreatingRole: false,
roleCreated: false,
createRoleError: false,
accountId: '',
externalId: '',
clusterName: '',
environmentScope: '*',
kubernetesVersion: [KUBERNETES_VERSIONS].value,
kubernetesVersion,
selectedRegion: '',
selectedRole: '',
selectedKeyPair: '',
selectedVpc: '',
selectedSubnet: '',
selectedSecurityGroup: '',
selectedInstanceType: 'm5.large',
nodeCount: '3',
isCreatingCluster: false,
createClusterError: false,
gitlabManagedCluster: true,
});

View file

@ -0,0 +1,37 @@
import initGkeDropdowns from './gke_cluster';
import initGkeNamespace from './gke_cluster_namespace';
import PersistentUserCallout from '~/persistent_user_callout';
const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
const isProjectLevelCluster = page => page.startsWith('project:clusters');
export default (document, gon) => {
const { page } = document.body.dataset;
const isNewClusterView = newClusterViews.some(view => page.endsWith(view));
if (!isNewClusterView) {
return;
}
const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout);
initGkeDropdowns();
if (gon.features.createEksClusters) {
import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
.then(({ default: initCreateEKSCluster }) => {
const el = document.querySelector('.js-create-eks-cluster-form-container');
if (el) {
initCreateEKSCluster(el);
}
})
.catch(() => {});
}
if (isProjectLevelCluster(page)) {
initGkeNamespace();
}
};

View file

@ -1,44 +0,0 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { GlButton } from '@gitlab/ui';
export default {
name: 'StageCardListItem',
components: {
Icon,
GlButton,
},
props: {
isActive: {
type: Boolean,
required: true,
},
canEdit: {
type: Boolean,
default: false,
required: false,
},
},
};
</script>
<template>
<div
:class="{ active: isActive }"
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
>
<slot></slot>
<div v-if="canEdit" class="dropdown">
<gl-button
:title="__('More actions')"
class="more-actions-toggle btn btn-transparent p-0"
data-toggle="dropdown"
>
<icon class="icon" name="ellipsis_v" />
</gl-button>
<ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
<slot name="dropdown-options"></slot>
</ul>
</div>
</div>
</template>

View file

@ -1,11 +1,6 @@
<script>
import StageCardListItem from './stage_card_list_item.vue';
export default {
name: 'StageNavItem',
components: {
StageCardListItem,
},
props: {
isDefaultStage: {
type: Boolean,
@ -40,16 +35,16 @@ export default {
hasValue() {
return this.value && this.value.length > 0;
},
editable() {
return this.isUserAllowed && this.canEdit;
},
},
};
</script>
<template>
<li @click="$emit('select')">
<stage-card-list-item :is-active="isActive" :can-edit="editable">
<div
:class="{ active: isActive }"
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
>
<div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
{{ title }}
</div>
@ -62,27 +57,6 @@ export default {
<span class="not-available">{{ __('Not available') }}</span>
</template>
</div>
<template v-slot:dropdown-options>
<template v-if="isDefaultStage">
<li>
<button type="button" class="btn-default btn-transparent">
{{ __('Hide stage') }}
</button>
</li>
</template>
<template v-else>
<li>
<button type="button" class="btn-default btn-transparent">
{{ __('Edit stage') }}
</button>
</li>
<li>
<button type="button" class="btn-danger danger">
{{ __('Remove stage') }}
</button>
</li>
</template>
</template>
</stage-card-list-item>
</div>
</li>
</template>

View file

@ -124,8 +124,10 @@ export default {
:diff-viewer-mode="diffViewerMode"
:new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha"
:new-size="diffFile.new_size"
:old-path="diffFile.old_path"
:old-sha="diffFile.diff_refs.base_sha"
:old-size="diffFile.old_size"
:file-hash="diffFileHash"
:project-path="projectPath"
:a-mode="diffFile.a_mode"

View file

@ -54,11 +54,12 @@ export default {
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
hasDiffLines() {
hasDiff() {
return (
this.file.highlighted_diff_lines &&
(this.file.highlighted_diff_lines &&
this.file.parallel_diff_lines &&
this.file.parallel_diff_lines.length > 0
this.file.parallel_diff_lines.length > 0) ||
!this.file.blob.readable_text
);
},
isFileTooLarge() {
@ -82,7 +83,7 @@ export default {
},
watch: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
if (!newVal && oldVal && !this.hasDiffLines) {
if (!newVal && oldVal && !this.hasDiff) {
this.handleLoadCollapsedDiff();
}
@ -103,7 +104,7 @@ export default {
'setFileCollapsed',
]),
handleToggle() {
if (!this.hasDiffLines) {
if (!this.hasDiff) {
this.handleLoadCollapsedDiff();
} else {
this.isCollapsed = !this.isCollapsed;

View file

@ -290,5 +290,5 @@ export default function dropzoneInput(form) {
formTextarea.focus();
});
return Dropzone.forElement($formDropzone.get(0));
return $formDropzone.get(0) ? Dropzone.forElement($formDropzone.get(0)) : null;
}

View file

@ -0,0 +1,141 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
export default {
components: {
GlButton,
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
Icon,
Stacktrace,
},
directives: {
TrackEvent: TrackEventDirective,
},
mixins: [timeagoMixin],
props: {
issueDetailsPath: {
type: String,
required: true,
},
issueStackTracePath: {
type: String,
required: true,
},
},
computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
timeAgo: this.timeFormated(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() {
return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
},
lastReleaseLink() {
return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
},
showDetails() {
return Boolean(!this.loading && this.error && this.error.id);
},
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
},
mounted() {
this.startPollingDetails(this.issueDetailsPath);
this.startPollingStacktrace(this.issueStackTracePath);
},
methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
trackClickErrorLinkToSentryOptions,
formatDate(date) {
return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
},
};
</script>
<template>
<div>
<div v-if="loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
<div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<!-- <gl-button class="my-3 ml-auto" variant="success">
{{ __('Create Issue') }}
</gl-button>-->
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
<h2 class="text-truncate">{{ error.title }}</h2>
</tooltip-on-truncate>
<h3>{{ __('Error details') }}</h3>
<ul>
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
:href="error.external_url"
target="_blank"
>
<span class="text-truncate">{{ error.external_url }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
<li v-if="error.first_release_short_version">
<span class="bold">{{ __('First seen') }}:</span>
{{ formatDate(error.first_seen) }}
<gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
</gl-link>
</li>
<li v-if="error.last_release_short_version">
<span class="bold">{{ __('Last seen') }}:</span>
{{ formatDate(error.last_seen) }}
<gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
</gl-link>
</li>
<li>
<span class="bold">{{ __('Events') }}:</span>
<span>{{ error.count }}</span>
</li>
<li>
<span class="bold">{{ __('Users') }}:</span>
<span>{{ error.user_count }}</span>
</li>
</ul>
<div v-if="loadingStacktrace" class="py-3">
<gl-loading-icon :size="3" />
</div>
<template v-if="showStacktrace">
<h3 class="my-4">{{ __('Stack trace') }}</h3>
<stacktrace :entries="stacktrace" />
</template>
</div>
</div>
</div>
</template>

View file

@ -1,11 +1,19 @@
<script>
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlEmptyState,
GlButton,
GlLink,
GlLoadingIcon,
GlTable,
GlSearchBoxByType,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils';
import { trackViewInSentryOptions } from '../utils';
export default {
fields: [
@ -20,6 +28,7 @@ export default {
GlLink,
GlLoadingIcon,
GlTable,
GlSearchBoxByType,
Icon,
TimeAgo,
},
@ -48,8 +57,17 @@ export default {
required: true,
},
},
data() {
return {
errorSearchQuery: '',
};
},
computed: {
...mapState(['errors', 'externalUrl', 'loading']),
...mapState('list', ['errors', 'externalUrl', 'loading']),
...mapGetters('list', ['filterErrorsByTitle']),
filteredErrors() {
return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors;
},
},
created() {
if (this.errorTrackingEnabled) {
@ -57,9 +75,11 @@ export default {
}
},
methods: {
...mapActions(['startPolling', 'restartPolling']),
...mapActions('list', ['startPolling', 'restartPolling']),
trackViewInSentryOptions,
trackClickErrorLinkToSentryOptions,
viewDetails(errorId) {
visitUrl(`error_tracking/${errorId}/details`);
},
},
};
</script>
@ -71,10 +91,17 @@ export default {
<gl-loading-icon :size="3" />
</div>
<div v-else>
<div class="d-flex justify-content-end">
<div class="d-flex flex-row justify-content-around bg-secondary border">
<gl-search-box-by-type
v-model="errorSearchQuery"
class="col-lg-10 m-3 p-0"
:placeholder="__('Search or filter results...')"
type="search"
autofocus
/>
<gl-button
v-track-event="trackViewInSentryOptions(externalUrl)"
class="my-3 ml-auto"
class="m-3"
variant="primary"
:href="externalUrl"
target="_blank"
@ -84,7 +111,14 @@ export default {
</gl-button>
</div>
<gl-table :items="errors" :fields="$options.fields" :show-empty="true" fixed stacked="sm">
<gl-table
class="mt-3"
:items="filteredErrors"
:fields="$options.fields"
:show-empty="true"
fixed
stacked="sm"
>
<template slot="HEAD_events" slot-scope="data">
<div class="text-md-right">{{ data.label }}</div>
</template>
@ -94,13 +128,11 @@ export default {
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)"
:href="errors.item.externalUrl"
class="d-flex text-dark"
target="_blank"
@click="viewDetails(errors.item.id)"
>
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
<span class="text-secondary text-truncate">
{{ errors.item.culprit }}

View file

@ -0,0 +1,33 @@
<script>
import StackTraceEntry from './stacktrace_entry.vue';
export default {
components: {
StackTraceEntry,
},
props: {
entries: {
type: Array,
required: true,
},
},
methods: {
isFirstEntry(index) {
return index === 0;
},
},
};
</script>
<template>
<div class="stacktrace">
<stack-trace-entry
v-for="(entry, index) in entries"
:key="`stacktrace-entry-${index}`"
:lines="entry.context"
:file-path="entry.filename"
:error-line="entry.lineNo"
:expanded="isFirstEntry(index)"
/>
</div>
</template>

View file

@ -0,0 +1,110 @@
<script>
import { GlTooltip } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
FileIcon,
Icon,
},
directives: {
GlTooltip,
},
props: {
lines: {
type: Array,
required: true,
},
filePath: {
type: String,
required: true,
},
errorLine: {
type: Number,
required: true,
},
expanded: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isExpanded: this.expanded,
};
},
computed: {
linesLength() {
return this.lines.length;
},
collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
},
methods: {
isHighlighted(lineNum) {
return lineNum === this.errorLine;
},
toggle() {
this.isExpanded = !this.isExpanded;
},
lineNum(line) {
return line[0];
},
lineCode(line) {
return line[1];
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
<div class="file-header-content ">
<div class="d-inline-block cursor-pointer" @click="toggle()">
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
</div>
<div class="d-inline-block append-right-4">
<file-icon
:file-name="filePath"
:size="18"
aria-hidden="true"
css-classes="append-right-5"
/>
<strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
{{ filePath }}
</strong>
</div>
<clipboard-button
:title="__('Copy file path')"
:text="filePath"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
</div>
<table v-if="isExpanded" :class="$options.userColorScheme" class="code js-syntax-highlight">
<tbody>
<template v-for="(line, index) in lines">
<tr :key="`stacktrace-line-${index}`" class="line_holder">
<td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
{{ lineNum(line) }}
</td>
<td
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
v-html="lineCode(line)"
></td>
</tr>
</template>
</tbody>
</table>
</div>
</template>

View file

@ -0,0 +1,25 @@
import Vue from 'vue';
import store from './store';
import ErrorDetails from './components/error_details.vue';
export default () => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_details',
components: {
ErrorDetails,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { issueDetailsPath, issueStackTracePath } = domEl.dataset;
return createElement('error-details', {
props: {
issueDetailsPath,
issueStackTracePath,
},
});
},
});
};

View file

@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
getErrorList({ endpoint }) {
getSentryData({ endpoint }) {
return axios.get(endpoint);
},
};

View file

@ -0,0 +1,63 @@
import service from '../../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
let stackTracePoll;
let detailPoll;
const stopPolling = poll => {
if (poll) poll.stop();
};
export function startPollingDetails({ commit }, endpoint) {
detailPoll = new Poll({
resource: service,
method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
detailPoll.restart();
return;
}
commit(types.SET_ERROR, data.error);
commit(types.SET_LOADING, false);
stopPolling(detailPoll);
},
errorCallback: () => {
commit(types.SET_LOADING, false);
createFlash(__('Failed to load error details from Sentry.'));
},
});
detailPoll.makeRequest();
}
export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll = new Poll({
resource: service,
method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
stackTracePoll.restart();
return;
}
commit(types.SET_STACKTRACE_DATA, data.error);
commit(types.SET_LOADING_STACKTRACE, false);
stopPolling(stackTracePoll);
},
errorCallback: () => {
commit(types.SET_LOADING_STACKTRACE, false);
createFlash(__('Failed to load stacktrace.'));
},
});
stackTracePoll.makeRequest();
}
export default () => {};

View file

@ -0,0 +1,3 @@
export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
export default () => {};

View file

@ -0,0 +1,4 @@
export const SET_ERROR = 'SET_ERRORS';
export const SET_LOADING = 'SET_LOADING';
export const SET_LOADING_STACKTRACE = 'SET_LOADING_STACKTRACE';
export const SET_STACKTRACE_DATA = 'SET_STACKTRACE_DATA';

View file

@ -0,0 +1,16 @@
import * as types from './mutation_types';
export default {
[types.SET_ERROR](state, data) {
state.error = data;
},
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
[types.SET_LOADING_STACKTRACE](state, data) {
state.loadingStacktrace = data;
},
[types.SET_STACKTRACE_DATA](state, data) {
state.stacktraceData = data;
},
};

Some files were not shown because too many files have changed in this diff Show more