Update upstream source from tag 'upstream/12.1.11'

Update to upstream version '12.1.11'
with Debian dir 6183fb855f
This commit is contained in:
Sruthi Chandran 2019-09-30 21:11:34 +05:30
commit da6367271f
3552 changed files with 62667 additions and 52118 deletions

View file

@ -5,6 +5,7 @@ globals:
gl: false gl: false
gon: false gon: false
localStorage: false localStorage: false
IS_EE: false
plugins: plugins:
- import - import
- html - html

View file

@ -1,7 +1,6 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.29"
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
RAILS_ENV: "test" RAILS_ENV: "test"
NODE_ENV: "test" NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
@ -37,6 +36,7 @@ include:
- local: .gitlab/ci/cng.gitlab-ci.yml - local: .gitlab/ci/cng.gitlab-ci.yml
- local: .gitlab/ci/docs.gitlab-ci.yml - local: .gitlab/ci/docs.gitlab-ci.yml
- local: .gitlab/ci/frontend.gitlab-ci.yml - local: .gitlab/ci/frontend.gitlab-ci.yml
- local: .gitlab/ci/memory.gitlab-ci.yml
- local: .gitlab/ci/pages.gitlab-ci.yml - local: .gitlab/ci/pages.gitlab-ci.yml
- local: .gitlab/ci/qa.gitlab-ci.yml - local: .gitlab/ci/qa.gitlab-ci.yml
- local: .gitlab/ci/reports.gitlab-ci.yml - local: .gitlab/ci/reports.gitlab-ci.yml

View file

@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis /doc/ @axil @marcia @eread @mikelewis
# Frontend maintainers should see everything in `app/assets/` # Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya *.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
# Someone from the database team should review changes in `db/` # Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS db/ @abrandl @NikolayS
@ -19,3 +19,5 @@ db/ @abrandl @NikolayS
/lib/gitlab/ci/templates/ @nolith @zj /lib/gitlab/ci/templates/ @nolith @zj
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah /lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
/lib/gitlab/ci/templates/Security/ @plafoucriere @gonzoyumo @twoodham /lib/gitlab/ci/templates/Security/ @plafoucriere @gonzoyumo @twoodham
/ee/app/models/project_alias.rb @patrickbajao
/ee/lib/api/project_aliases.rb @patrickbajao

View file

@ -12,7 +12,9 @@
# Trigger a manual docs build in gitlab-docs only on non docs-only branches. # Trigger a manual docs build in gitlab-docs only on non docs-only branches.
# Useful to preview the docs changes live. # Useful to preview the docs changes live.
review-docs-deploy-manual: review-docs-deploy-manual:
<<: *review-docs extends:
- .review-docs
- .no-docs-and-no-qa
stage: build stage: build
script: script:
- gem install gitlab --no-document - gem install gitlab --no-document
@ -21,9 +23,6 @@ review-docs-deploy-manual:
only: only:
- branches@gitlab-org/gitlab-ce - branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
# Always trigger a docs build in gitlab-docs only on docs-only branches. # Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live. # Useful to preview the docs changes live.
@ -66,6 +65,8 @@ docs lint:
- scripts/lint-changelog-yaml - scripts/lint-changelog-yaml
- mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX - mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX
- cd /tmp/gitlab-docs - cd /tmp/gitlab-docs
# Lint Markdown
- bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX -c $CI_PROJECT_DIR/.mdlrc
# Build HTML from Markdown # Build HTML from Markdown
- bundle exec nanoc - bundle exec nanoc
# Check the internal links # Check the internal links

View file

@ -1,26 +1,25 @@
.assets-compile-cache: &assets-compile-cache .assets-compile-cache: &assets-compile-cache
cache: cache:
key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v5" key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6"
paths: paths:
- vendor/ruby/ - vendor/ruby/
- .yarn-cache/ - .yarn-cache/
- tmp/cache/assets/sprockets - tmp/cache/assets/sprockets
policy: pull-push
.use-pg: &use-pg .use-pg: &use-pg
services: services:
- name: postgres:9.6 - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
gitlab:assets:compile: .gitlab:assets:compile-metadata:
<<: *assets-compile-cache <<: *assets-compile-cache
extends: .dedicated-no-docs-pull-cache-job extends: .dedicated-no-docs-pull-cache-job
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.21-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.29-docker-18.06.1 image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.21-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.29-docker-18.06.1
dependencies: dependencies:
- setup-test-env - setup-test-env
services: services:
- docker:stable-dind - docker:19.03.0-dind
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
RAILS_ENV: "production" RAILS_ENV: "production"
@ -33,7 +32,7 @@ gitlab:assets:compile:
DOCKER_HOST: tcp://docker:2375 DOCKER_HOST: tcp://docker:2375
script: script:
- node --version - node --version
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache - retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache --prefer-offline
- free -m - free -m
- retry bundle exec rake gitlab:assets:compile - retry bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image - time scripts/build_assets_image
@ -58,14 +57,32 @@ gitlab:assets:compile:
- docker - docker
- gitlab-org - gitlab-org
compile-assets: gitlab:assets:compile:
extends: .gitlab:assets:compile-metadata
cache:
policy: pull-push
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
gitlab:assets:compile pull-cache:
extends: .gitlab:assets:compile-metadata
cache:
policy: pull
except:
refs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/
.compile-assets-metadata:
extends: .dedicated-runner extends: .dedicated-runner
<<: *use-pg <<: *use-pg
<<: *assets-compile-cache <<: *assets-compile-cache
stage: prepare stage: prepare
script: script:
- node --version - node --version
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache - retry yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- free -m - free -m
- retry bundle exec rake gitlab:assets:compile - retry bundle exec rake gitlab:assets:compile
- scripts/clean-old-cached-assets - scripts/clean-old-cached-assets
@ -77,8 +94,23 @@ compile-assets:
paths: paths:
- node_modules - node_modules
- public/assets - public/assets
compile-assets:
extends: .compile-assets-metadata
cache:
policy: pull-push
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
compile-assets pull-cache:
extends: .compile-assets-metadata
cache:
policy: pull
except: except:
refs: refs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
gitlab:ui:visual: gitlab:ui:visual:
@ -87,6 +119,7 @@ gitlab:ui:visual:
allow_failure: true allow_failure: true
dependencies: dependencies:
- compile-assets - compile-assets
- compile-assets pull-cache
script: script:
# Remove node modules from GitLab that may conflict with gitlab-ui # Remove node modules from GitLab that may conflict with gitlab-ui
- rm -r node_modules - rm -r node_modules
@ -116,6 +149,7 @@ karma:
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- compile-assets - compile-assets
- compile-assets pull-cache
- setup-test-env - setup-test-env
variables: variables:
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
@ -134,14 +168,15 @@ karma:
paths: paths:
- chrome_debug.log - chrome_debug.log
- coverage-javascript/ - coverage-javascript/
reports: # reports:
junit: junit_karma.xml # junit: junit_karma.xml
jest: jest:
extends: .dedicated-no-docs-and-no-qa-pull-cache-job extends: .dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- compile-assets - compile-assets
- compile-assets pull-cache
- setup-test-env - setup-test-env
script: script:
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
@ -156,8 +191,8 @@ jest:
paths: paths:
- coverage-frontend/ - coverage-frontend/
- junit_jest.xml - junit_jest.xml
reports: # reports:
junit: junit_jest.xml # junit: junit_jest.xml
cache: cache:
key: jest key: jest
paths: paths:
@ -196,7 +231,7 @@ qa:selectors:
before_script: [] before_script: []
script: script:
- date - date
- yarn install --frozen-lockfile --cache-folder .yarn-cache - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- date - date
- yarn run webpack-prod - yarn run webpack-prod
@ -232,6 +267,7 @@ jsdoc:
stage: post-test stage: post-test
dependencies: dependencies:
- compile-assets - compile-assets
- compile-assets pull-cache
before_script: [] before_script: []
script: script:
- date - date

View file

@ -28,16 +28,26 @@
policy: pull policy: pull
stage: test stage: test
.dedicated-no-docs-pull-cache-job: .no-docs:
extends: .dedicated-pull-cache-job
except: except:
- /(^docs[\/-].*|.*-docs$)/ refs:
- /(^docs[\/-].*|.*-docs$)/
.no-docs-and-no-qa:
except:
refs:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
.dedicated-no-docs-pull-cache-job:
extends:
- .dedicated-pull-cache-job
- .no-docs
.dedicated-no-docs-and-no-qa-pull-cache-job: .dedicated-no-docs-and-no-qa-pull-cache-job:
extends: .dedicated-pull-cache-job extends:
except: - .dedicated-pull-cache-job
- /(^docs[\/-].*|.*-docs$)/ - .no-docs-and-no-qa
- /(^qa[\/-].*|.*-qa$)/
# Jobs that do not need a DB # Jobs that do not need a DB
.dedicated-no-docs-no-db-pull-cache-job: .dedicated-no-docs-no-db-pull-cache-job:
@ -45,6 +55,12 @@
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
# Jobs that need a dedicated runner, with no cache
.dedicated-no-docs:
extends:
- .dedicated-runner
- .no-docs
.single-script-job-dedicated-runner: .single-script-job-dedicated-runner:
extends: .dedicated-runner extends: .dedicated-runner
image: ruby:2.6-alpine image: ruby:2.6-alpine

View file

@ -0,0 +1,42 @@
memory-static:
extends: .dedicated-no-docs-no-db-pull-cache-job
script:
# Uses two different reports from the 'derailed_benchmars' gem.
# Loads each of gems in the Gemfile and checks how much memory they consume when they are required.
# 'derailed_benchmarks' internally uses 'get_process_mem'
- bundle exec derailed bundle:mem > tmp/memory_bundle_mem.txt
- scripts/generate-gems-size-metrics-static tmp/memory_bundle_mem.txt >> 'tmp/memory_metrics.txt'
# Outputs detailed information about objects created while gems are loaded.
# 'derailed_benchmarks' internally uses 'memory_profiler'
- bundle exec derailed bundle:objects > tmp/memory_bundle_objects.txt
- scripts/generate-gems-memory-metrics-static tmp/memory_bundle_objects.txt >> 'tmp/memory_metrics.txt'
artifacts:
paths:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_metrics.txt
# Show memory usage caused by invoking require per gem.
# Unlike `memory-static`, it hits the app with one request to ensure that any last minute require-s have been called.
# The application is booted in `production` environment.
# All tests are run without a webserver (directly using Rack::Mock by default).
memory-on-boot:
extends: .rspec-metadata-pg-10
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "true"
SKIP_STORAGE_VALIDATION: "true"
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
script:
# Both bootsnap and derailed monkey-patch Kernel#require, which leads to circular dependency
- DISABLE_BOOTSNAP=true PATH_TO_HIT="/users/sign_in" CUT_OFF=0.3 bundle exec derailed exec perf:mem >> 'tmp/memory_on_boot.txt'
- scripts/generate-memory-metrics-on-boot tmp/memory_on_boot.txt >> 'tmp/memory_on_boot_metrics.txt'
artifacts:
paths:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_on_boot_metrics.txt

View file

@ -1,6 +1,6 @@
.use-pg: &use-pg .use-pg: &use-pg
services: services:
- name: postgres:9.6 - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
@ -10,11 +10,6 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
.use-mysql: &use-mysql
services:
- mysql:5.7
- redis:alpine
.only-schedules-master: &only-schedules-master .only-schedules-master: &only-schedules-master
only: only:
- schedules@gitlab-org/gitlab-ce - schedules@gitlab-org/gitlab-ce
@ -25,8 +20,9 @@
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup .gitlab-setup: &gitlab-setup
extends: .dedicated-no-docs-and-no-qa-pull-cache-job extends:
<<: *use-pg - .dedicated-no-docs-and-no-qa-pull-cache-job
- .use-pg
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
@ -48,7 +44,9 @@
- bundle exec rake $CI_JOB_NAME - bundle exec rake $CI_JOB_NAME
.rspec-metadata: &rspec-metadata .rspec-metadata: &rspec-metadata
extends: .dedicated-pull-cache-job extends:
- .dedicated-pull-cache-job
- .no-docs-and-no-qa
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
@ -68,6 +66,8 @@
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- date - date
- 'export KNAPSACK_TEST_FILE_PATTERN=$(ruby -r./lib/quality/test_level.rb -e "puts Quality::TestLevel.new.pattern(:${TEST_LEVEL})")' - 'export KNAPSACK_TEST_FILE_PATTERN=$(ruby -r./lib/quality/test_level.rb -e "puts Quality::TestLevel.new.pattern(:${TEST_LEVEL})")'
- mkdir -p tmp/memory_test
- export MEMORY_TEST_PATH="tmp/memory_test/${TEST_TOOL}_${TEST_LEVEL}_${DATABASE}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_memory.csv"
- knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml --tag level:${TEST_LEVEL} --tag ~geo" - knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml --tag level:${TEST_LEVEL} --tag ~geo"
- date - date
artifacts: artifacts:
@ -79,11 +79,9 @@
- rspec_flaky/ - rspec_flaky/
- rspec_profiling/ - rspec_profiling/
- tmp/capybara/ - tmp/capybara/
reports: - tmp/memory_test/
junit: junit_rspec.xml # reports:
except: # junit: junit_rspec.xml
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
.rspec-metadata-pg: &rspec-metadata-pg .rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata <<: *rspec-metadata
@ -94,10 +92,6 @@
<<: *use-pg-10 <<: *use-pg-10
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.29"
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
extends: .dedicated-no-docs-and-no-qa-pull-cache-job extends: .dedicated-no-docs-and-no-qa-pull-cache-job
@ -131,8 +125,10 @@
- setup-test-env - setup-test-env
setup-test-env: setup-test-env:
extends: .dedicated-runner-default-cache extends:
<<: *use-pg - .dedicated-runner-default-cache
- .no-docs
- .use-pg
stage: prepare stage: prepare
script: script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
@ -143,8 +139,6 @@ setup-test-env:
- tmp/tests - tmp/tests
- config/secrets.yml - config/secrets.yml
- vendor/gitaly-ruby - vendor/gitaly-ruby
except:
- /(^docs[\/-].*|.*-docs$)/
rspec unit pg: rspec unit pg:
<<: *rspec-metadata-pg <<: *rspec-metadata-pg
@ -173,42 +167,6 @@ rspec system pg-10:
<<: *only-schedules-master <<: *only-schedules-master
parallel: 24 parallel: 24
rspec unit mysql:
<<: *rspec-metadata-mysql
<<: *only-schedules-master
parallel: 20
rspec integration mysql:
<<: *rspec-metadata-mysql
<<: *only-schedules-master
parallel: 6
rspec system mysql:
<<: *rspec-metadata-mysql
<<: *only-schedules-master
parallel: 24
.rspec-mysql-on-demand: &rspec-mysql-on-demand
only:
variables:
- $CI_COMMIT_MESSAGE =~ /\[run mysql\]/i
- $CI_COMMIT_REF_NAME =~ /mysql/
rspec unit mysql on-demand:
<<: *rspec-metadata-mysql
<<: *rspec-mysql-on-demand
parallel: 20
rspec integration mysql on-demand:
<<: *rspec-metadata-mysql
<<: *rspec-mysql-on-demand
parallel: 6
rspec system mysql on-demand:
<<: *rspec-metadata-mysql
<<: *rspec-mysql-on-demand
parallel: 24
rspec-fast-spec-helper: rspec-fast-spec-helper:
<<: *rspec-metadata-pg <<: *rspec-metadata-pg
script: script:
@ -226,16 +184,11 @@ rspec quarantine pg:
<<: *rspec-quarantine <<: *rspec-quarantine
allow_failure: true allow_failure: true
rspec quarantine mysql:
<<: *rspec-metadata-mysql
<<: *rspec-quarantine
<<: *only-schedules-master
allow_failure: true
static-analysis: static-analysis:
extends: .dedicated-no-docs-no-db-pull-cache-job extends: .dedicated-no-docs-no-db-pull-cache-job
dependencies: dependencies:
- compile-assets - compile-assets
- compile-assets pull-cache
- setup-test-env - setup-test-env
script: script:
- scripts/static-analysis - scripts/static-analysis
@ -250,11 +203,12 @@ static-analysis:
downtime_check: downtime_check:
<<: *rake-exec <<: *rake-exec
except: except:
- master refs:
- tags - master
- /^[\d-]+-stable(-ee)?$/ - tags
- /(^docs[\/-].*|.*-docs$)/ - /^[\d-]+-stable(-ee)?$/
- /(^qa[\/-].*|.*-qa$)/ - /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
dependencies: dependencies:
- setup-test-env - setup-test-env
@ -262,12 +216,13 @@ ee_compat_check:
<<: *rake-exec <<: *rake-exec
dependencies: [] dependencies: []
except: except:
- master refs:
- tags - master
- /[\d-]+-stable(-ee)?/ - tags
- /^security-/ - /[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee - /^security-/
- branches@gitlab/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
retry: 0 retry: 0
artifacts: artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
@ -280,10 +235,6 @@ db:migrate:reset-pg:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
db:migrate:reset-mysql:
<<: *db-migrate-reset
<<: *use-mysql
db:check-schema-pg: db:check-schema-pg:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
@ -294,15 +245,11 @@ migration:path-pg:
<<: *migration-paths <<: *migration-paths
<<: *use-pg <<: *use-pg
migration:path-mysql:
<<: *migration-paths
<<: *use-mysql
.db-rollback: &db-rollback .db-rollback: &db-rollback
extends: .dedicated-no-docs-and-no-qa-pull-cache-job extends: .dedicated-no-docs-and-no-qa-pull-cache-job
script: script:
- bundle exec rake db:migrate VERSION=20170523121229 - bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate - bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
dependencies: dependencies:
- setup-test-env - setup-test-env
@ -310,26 +257,18 @@ db:rollback-pg:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
gitlab:setup-pg: gitlab:setup-pg:
<<: *gitlab-setup <<: *gitlab-setup
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- setup-test-env - setup-test-env
gitlab:setup-mysql:
<<: *gitlab-setup
<<: *use-mysql
dependencies:
- setup-test-env
coverage: coverage:
# Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to # Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
# download artifacts from all the rspec jobs instead of from setup-test-env only # download artifacts from all the rspec jobs instead of from setup-test-env only
extends: .dedicated-runner-default-cache extends:
- .dedicated-runner-default-cache
- .no-docs-and-no-qa
cache: cache:
policy: pull policy: pull
variables: variables:
@ -337,6 +276,7 @@ coverage:
stage: post-test stage: post-test
script: script:
- bundle exec scripts/merge-simplecov - bundle exec scripts/merge-simplecov
- bundle exec scripts/gather-test-memory-data
coverage: '/LOC \((\d+\.\d+%)\) covered.$/' coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts: artifacts:
name: coverage name: coverage
@ -344,6 +284,4 @@ coverage:
paths: paths:
- coverage/index.html - coverage/index.html
- coverage/assets/ - coverage/assets/
except: - tmp/memory_test/
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/

View file

@ -1,98 +1,26 @@
include: include:
- template: Code-Quality.gitlab-ci.yml - template: Code-Quality.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
code_quality: code_quality:
extends: .dedicated-no-docs-no-db-pull-cache-job extends: .dedicated-no-docs
# gitlab-org runners set `privileged: false` but we need to have it set to true # gitlab-org runners set `privileged: false` but we need to have it set to true
# since we're using Docker in Docker # since we're using Docker in Docker
tags: [] tags: []
before_script: [] before_script: []
cache: {} cache: {}
dependencies: []
variables:
SETUP_DB: "false"
sast: sast:
extends: .dedicated-no-docs-no-db-pull-cache-job extends: .dedicated-no-docs
image: docker:stable
variables:
SAST_CONFIDENCE_LEVEL: 2
DOCKER_DRIVER: overlay2
allow_failure: true
tags: [] tags: []
before_script: [] before_script: []
cache: {} cache: {}
dependencies: [] variables:
services: SAST_BRAKEMAN_LEVEL: 2
- docker:stable-dind
script:
- | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
function propagate_env_vars() {
CURRENT_ENV=$(printenv)
for VAR_NAME; do
echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
done
}
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- |
docker run \
$(propagate_env_vars \
SAST_ANALYZER_IMAGES \
SAST_ANALYZER_IMAGE_PREFIX \
SAST_ANALYZER_IMAGE_TAG \
SAST_DEFAULT_ANALYZERS \
SAST_BRAKEMAN_LEVEL \
SAST_GOSEC_LEVEL \
SAST_FLAWFINDER_LEVEL \
SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
SAST_RUN_ANALYZER_TIMEOUT \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
reports:
sast: gl-sast-report.json
dependency_scanning: dependency_scanning:
extends: .dedicated-no-docs-no-db-pull-cache-job extends: .dedicated-no-docs
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
tags: [] tags: []
before_script: [] before_script: []
cache: {} cache: {}
dependencies: []
services:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
function propagate_env_vars() {
CURRENT_ENV=$(printenv)
for VAR_NAME; do
echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
done
}
- |
docker run \
$(propagate_env_vars \
DS_ANALYZER_IMAGES \
DS_ANALYZER_IMAGE_PREFIX \
DS_ANALYZER_IMAGE_TAG \
DS_DEFAULT_ANALYZERS \
DEP_SCAN_DISABLE_REMOTE_CHECKS \
DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
DS_PULL_ANALYZER_IMAGE_TIMEOUT \
DS_RUN_ANALYZER_TIMEOUT \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json

View file

@ -35,7 +35,7 @@
<<: *review-base <<: *review-base
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
services: services:
- docker:stable-dind - docker:19.03.0-dind
tags: tags:
- gitlab-org - gitlab-org
- docker - docker
@ -77,6 +77,7 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base .review-deploy-base: &review-deploy-base
<<: *review-base <<: *review-base
allow_failure: true allow_failure: true
retry: 1
stage: review stage: review
variables: variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
@ -95,10 +96,16 @@ schedule:review-build-cng:
- install_api_client_dependencies_with_apk - install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh - source scripts/review_apps/review-apps.sh
script: script:
- perform_review_app_deployment - check_kube_domain
- ensure_namespace
- install_tiller
- install_external_dns
- download_chart
- deploy || display_deployment_debug
- wait_for_review_app_to_be_accessible
- add_license
artifacts: artifacts:
paths: paths: [review_app_url.txt]
- review_app_url.txt
expire_in: 2 days expire_in: 2 days
when: always when: always
@ -108,8 +115,6 @@ review-deploy:
schedule:review-deploy: schedule:review-deploy:
<<: *review-deploy-base <<: *review-deploy-base
<<: *review-schedules-only <<: *review-schedules-only
script:
- perform_review_app_deployment
review-stop: review-stop:
<<: *review-base <<: *review-base
@ -124,11 +129,11 @@ review-stop:
script: script:
- source scripts/review_apps/review-apps.sh - source scripts/review_apps/review-apps.sh
- delete - delete
- cleanup
.review-qa-base: &review-qa-base .review-qa-base: &review-qa-base
<<: *review-docker <<: *review-docker
allow_failure: true allow_failure: true
retry: 2
stage: qa stage: qa
variables: variables:
<<: *review-docker-variables <<: *review-docker-variables
@ -169,7 +174,38 @@ review-qa-all:
script: script:
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json
- export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" - 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
parallel-spec-reports:
extends: .dedicated-runner
dependencies:
- review-qa-all
image: ruby:2.6-alpine
services: []
before_script: []
variables:
SETUP_DB: "false"
NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
stage: post-test
allow_failure: true
when: manual
retry: 0
artifacts:
when: always
paths:
- qa/report-new.html
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
script:
- apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
- gem install nokogiri
- cd qa/gitlab-qa-run-*/gitlab-*
- ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
- cd ../../..
- '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
- scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
.review-performance-base: &review-performance-base .review-performance-base: &review-performance-base
<<: *review-qa-base <<: *review-qa-base
@ -225,11 +261,12 @@ danger-review:
except: except:
refs: refs:
- master - master
- /^[\d-]+-stable(-ee)?$/
variables: variables:
- $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/ - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
- $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/ - $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/
script: script:
- git version - git version
- node --version - node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- danger --fail-on-errors=true - danger --fail-on-errors=true

View file

@ -15,7 +15,9 @@ cache gems:
- setup-test-env - setup-test-env
gitlab_git_test: gitlab_git_test:
extends: .dedicated-runner extends:
- .dedicated-runner
- .no-docs-and-no-qa
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
before_script: [] before_script: []
@ -23,12 +25,11 @@ gitlab_git_test:
cache: {} cache: {}
script: script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
no_ee_check: no_ee_check:
extends: .dedicated-runner extends:
- .dedicated-runner
- .no-docs-and-no-qa
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
before_script: [] before_script: []
@ -38,6 +39,3 @@ no_ee_check:
- scripts/no-ee-check - scripts/no-ee-check
only: only:
- /.+/@gitlab-org/gitlab-ce - /.+/@gitlab-org/gitlab-ce
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/

View file

@ -12,7 +12,9 @@
- rspec_profiling/ - rspec_profiling/
retrieve-tests-metadata: retrieve-tests-metadata:
<<: *tests-metadata-state extends:
- .tests-metadata-state
- .no-docs-and-no-qa
stage: prepare stage: prepare
cache: cache:
key: tests_metadata key: tests_metadata
@ -25,9 +27,6 @@ retrieve-tests-metadata:
- mkdir -p rspec_profiling/ - mkdir -p rspec_profiling/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
update-tests-metadata: update-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
@ -69,9 +68,10 @@ flaky-examples-check:
only: only:
- branches - branches
except: except:
- master refs:
- /(^docs[\/-].*|.*-docs$)/ - master
- /(^qa[\/-].*|.*-qa$)/ - /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
artifacts: artifacts:
expire_in: 30d expire_in: 30d
paths: paths:

View file

@ -1,34 +0,0 @@
#### Database Reviewer Checklist
Thank you for becoming a ~database reviewer! Please work on the list
below to complete your setup. For any question, reach out to #database
an mention `@abrandl`.
- [ ] Change issue title to include your name: `Database Reviewer Checklist: Your Name`
- [ ] Review general [code review guide](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Review [database review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html)
- [ ] Familiarize with [migration helpers](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/database/migration_helpers.rb) and review usage in existing migrations
- [ ] Read [database migration style guide](https://docs.gitlab.com/ee/development/migration_style_guide.html)
- [ ] Familiarize with best practices in [database guides](https://docs.gitlab.com/ee/development/#database-guides)
- [ ] Watch [Optimising Rails Database Queries: Episode 1](https://www.youtube.com/watch?v=79GurlaxhsI)
- [ ] Read [Understanding EXPLAIN plans](https://docs.gitlab.com/ee/development/understanding_explain_plans.html)
- [ ] Review [database best practices](https://docs.gitlab.com/ee/development/#best-practices)
- [ ] Review how we use [database instances restored from a backup](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd) for testing and make sure you're set up to execute pipelines (check [README.md](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd/blob/master/README.md) and reach out to @abrandl since this is currently subject to being changed)
- [ ] Get yourself added to [`@gl-database`](https://gitlab.com/groups/gl-database/-/group_members) group and respond to @-mentions to the group (reach out to any maintainer on the group to get added). You will get TODOs on gitlab.com for group mentions.
- [ ] Make sure you have proper access to at least a read-only replica in staging and production
- [ ] Indicate in `data/team.yml` your role as a database reviewer ([example MR](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/19600/diffs)). Assign MR to your manager for merge.
- [ ] Send one MR to improve the [review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html) or the [issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Database%20Reviewer.md)
Note that *approving and accepting* merge requests is *restricted* to
Database Maintainers only. As a reviewer, pass the MR to a maintainer
for approval.
You're all set! Watch out for TODOs on GitLab.com.
###### Where to go for questions?
Reach out to `#database` on Slack and mention `@abrandl` for any questions.
cc @abrandl
/label ~meta ~database

View file

@ -4,7 +4,7 @@
Note: Doc work as part of feature development is covered in the Feature Request template. Note: Doc work as part of feature development is covered in the Feature Request template.
* For issues related to features of the docs.gitlab.com site, see * For issues related to features of the docs.gitlab.com site, see
https://gitlab.com/gitlab-com/gitlab-docs/issues/ https://gitlab.com/gitlab-org/gitlab-docs/issues/
* For information about documentation content and process, see * For information about documentation content and process, see
https://docs.gitlab.com/ee/development/documentation/ --> https://docs.gitlab.com/ee/development/documentation/ -->

View file

@ -22,7 +22,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen
- [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, - [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) - [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread. to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable) - [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE

7
.mdlrc Normal file
View file

@ -0,0 +1,7 @@
# This is the options file for mdl, configured in .gitlab/ci/docs.gitlab-ci.yml,
# and related to the style file ./mdlrc.style
# See https://github.com/markdownlint/markdownlint/blob/master/docs/configuration.md
ignore_front_matter true
style File.expand_path('.mdlrc.style', __dir__)

21
.mdlrc.style Normal file
View file

@ -0,0 +1,21 @@
# This is the style file for mdl, configured in .gitlab/ci/docs.gitlab-ci.yml,
# and related to the options file ./mdlrc
# See https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md
# for more detailed information on the rules and styles.
rule "MD001"
rule "MD003", :style => :atx
rule "MD011"
rule "MD023"
rule "MD032"
rule "MD034"
rule "MD037"
# Should not be used currently:
# rule "MD004", :style => :dash # unordered list style - dash
# False positives, see https://github.com/markdownlint/markdownlint/issues/261
# rule "MD039" # Spaces inside link text
# Crashes when link text has certain punctuation

View file

@ -466,12 +466,10 @@ Rails/LinkToBlank:
Rails/Presence: Rails/Presence:
Exclude: Exclude:
- 'app/models/ci/pipeline.rb' - 'app/models/ci/pipeline.rb'
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb' - 'app/models/concerns/mentionable.rb'
- 'app/models/project_services/hipchat_service.rb' - 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_service.rb' - 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/jira_service.rb' - 'app/models/project_services/jira_service.rb'
- 'app/models/project_services/kubernetes_service.rb'
- 'app/models/project_services/packagist_service.rb' - 'app/models/project_services/packagist_service.rb'
- 'app/models/wiki_page.rb' - 'app/models/wiki_page.rb'
- 'lib/gitlab/github_import/importer/releases_importer.rb' - 'lib/gitlab/github_import/importer/releases_importer.rb'
@ -514,7 +512,6 @@ Security/YAMLLoad:
- 'spec/config/mail_room_spec.rb' - 'spec/config/mail_room_spec.rb'
- 'spec/initializers/secret_token_spec.rb' - 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb' - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- 'spec/models/project_services/kubernetes_service_spec.rb'
# Offense count: 34 # Offense count: 34
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.

View file

@ -2,20 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.0.9 ## 12.1.11
- No changes.
## 12.1.10
- No changes.
## 12.1.9
### Security (1 change) ### Security (1 change)
- Upgrade pages to 1.6.3. - Upgrade pages to 1.7.2.
## 12.0.8 ## 12.1.8
### Security (22 changes) ### Security (21 changes)
- Ensure only authorised users can create notes on Merge Requests and Issues. - Ensure only authorised users can create notes on Merge Requests and Issues.
- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. - Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
- Queries for Upload should be scoped by model.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. - Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments. - Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly. - Send TODOs for comments on commits correctly.
@ -37,36 +44,349 @@ entry.
- Fix SSRF via DNS rebinding in Kubernetes Integration. - Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.0.7 ## 12.1.7
- Unreleased due to QA failure. - Unreleased due to QA failure.
## 12.0.6 ## 12.1.6
### Security (2 changes) ### Security (2 changes)
- Upgrade Gitaly to 1.47.2 to prevent revision flag injection exploits. - Upgrade Gitaly to 1.53.2 to prevent revision flag injection exploits.
- Upgrade pages to 1.6.2 to prevent gitlab api token recovery from cookie. - Upgrade pages to 1.7.1 to prevent gitlab api token recovery from cookie.
## 12.0.5 ## 12.1.5
- No changes. - No changes.
## 12.0.4 ## 12.1.4
### Fixed (3 changes, 1 of them is from the community)
- Properly translate term in projects list. !30958
- Add exclusive lease to mergeability check process. !31082
- Fix Docker in Docker (DIND) listen port behavior change by adding DOCKER_TLS_CERTDIR in CI job templates. !31201 (Cameron Boulton)
### Performance (1 change)
- Improve job log rendering performance. !31262
## 12.1.3
### Fixed (11 changes)
- Prevent multiple confirmation modals from opening when deleting a repository. !30532
- Fix the project auto devops API. !30946
- Fix "Certificate misses intermediates" UI error when enabling Let's Encrypt integration for pages domain. !30995
- Fix xterm css not loading for environment terminal. !31023
- Set DOCKER_TLS_CERTDIR in Auto Dev-Ops CI template to fix jobs using Docker-in-Docker. !31078
- Set DOCKER_TLS_CERTDIR in CI job templates to fix Docker-in-Docker service. !31080
- Support Docker OCI images. !31127
- Fix error rendering submodules in MR diffs when there is no .gitmodules. !31162
- Fix pdf.js rendering pages in the wrong order. !31222
- Fix exception handling in Gitaly autodetection. !31285
- Fix bug that caused diffs not to show on MRs with changes to submodules.
### Performance (1 change)
- Optimise import performance. !31045
## 12.1.2
### Security (1 change)
- Use source project as permissions reference for MergeRequestsController#pipelines.
### Security (9 changes) ### Security (9 changes)
- Restrict slash commands to users who can log in. - Restrict slash commands to users who can log in.
- Patch XSS issue in wiki links. - Patch XSS issue in wiki links.
- Queries for Upload should be scoped by model.
- Filter merge request params on the new merge request page. - Filter merge request params on the new merge request page.
- Fix Server Side Request Forgery mitigation bypass. - Fix Server Side Request Forgery mitigation bypass.
- Show badges if pipelines are public otherwise default to project permissions. - Show badges if pipelines are public otherwise default to project permissions.
- Do not allow localhost url redirection in GitHub Integration. - Do not allow localhost url redirection in GitHub Integration.
- Do not show moved issue id for users that cannot read issue. - Do not show moved issue id for users that cannot read issue.
- Use source project as permissions reference for MergeRequestsController#pipelines.
- Drop feature to take ownership of trigger token. - Drop feature to take ownership of trigger token.
## 12.1.1
- No changes.
## 12.1.0
### Security (11 changes, 2 of them are from the community)
- Update tar to 2.2.2. !29949 (Takuya Noguchi)
- Update lodash to 4.7.14 and lodash.mergewith to 4.6.2. !30602 (Takuya Noguchi)
- Correctly check permissions when creating snippet notes.
- Gate MR head_pipeline behind read_pipeline ability.
- Prevent Billion Laughs attack.
- Add missing authorizations in GraphQL.
- Fix Denial of Service for comments when rendering issues/MR comments.
- Expose merge requests count based on user access.
- Fix DoS vulnerability in color validation regex.
- Prevent the detection of merge request templates by unauthorized users.
- Persist tmp snippet uploads at users.
### Removed (7 changes)
- Disable Kubernetes credential passthrough for managed project-level clusters. !29262
- Remove deprecated group routes. !29351
- Remove support for creating non-RBAC kubernetes clusters. !29614
- Remove Kubernetes service integration and Kubernetes service template from available deployment platforms. !29786
- Remove MySQL support. !29790
- Remove depreated /u/:username routing. !30044
- Remove support for legacy pipeline triggers. !30133
### Fixed (84 changes, 14 of them are from the community)
- Update a user's routes after updating their name. !23272
- Show poper panel when validation error occurs in admin settings panels. !25434
- Expect bytes from Gitaly RPC GetRawChanges. !28164
- Sanitize LDAP output in Rake tasks. !28427
- Left align mr widget icons and text. !28561
- Keep the empty folders in the tree. !29196
- Fix incorrect emoji placement in commit diff discussion. !29445
- Fix favicon path with uploads of object store. !29482 (Roger Meier)
- Remove duplicate trailing +/- char in merge request discussions. !29518
- Fix the signup form's username validation messages not displaying. !29678 (Jiaan Louw)
- Fix broken environment selector and always display it on monitoring dashboard. !29705
- Fix Container Scanning job timeout when using the kubernetes executor. !29706
- Look for new branches more carefully. !29761
- Fix nested lists unnecessary margin. !29775 (Kuba Kopeć)
- Fix reports jobs timing out because of cache. !29780
- Fix Double Border in Profile Page. !29784 (Yoginth <@yo>)
- Remove minimum character limits for fuzzy searches when using a CTE. !29810
- Set default sort method for dashboard projects list. !29830 (David Palubin)
- Protect TeamCity builds from triggering when a branch has been deleted. And a MR-option. !29836 (Nikolay Novikov, Raphael Tweitmann)
- Fix pipeline schedule does not run correctly when it's scheduled at the same time with the cron worker. !29848
- Always shows author of created issue/started discussion/comment in HTML body and text of email. !29886 (Frank van Rest)
- Build correct basenames for title search results. !29898
- Resolve "500 error when forking via the web IDE button". !29909
- Turn commit sha in monitor charts popover to link. !29914
- Fix broken URLs for uploads with a plus in the filename. !29915
- Retry fetching Kubernetes Secret#token (#63507). !29922
- Enforce presence of pipeline when "Pipeline must succeed" project setting is enabled. !29926
- Fix unresponsive reply button in discussions. !29936
- Allow asynchronous rebase operations to be monitored. !29940
- Resolve Avatar in Please sign in pattern too large. !29944
- Persist the cluster a deployment was deployed to. !29960
- Fix runner tags search dropdown being empty when there are tags. !29985
- Display the correct amount of projects being migrated/rolled-back to Hashed Storage when specifying ranges. !29996
- Resolve Environment details header border misaligned. !30011
- Correct link to docs for External Dashboard. !30019
- Fix Jupyter-Git integration. !30020 (Amit Rathi)
- Update Mermaid to 8.1.0. !30036
- Fix background migrations failing with unused replication slot. !30042
- Disable Rails SQL query cache when applying service templates. !30060
- Set higher TTL for write lock of trace to prevent concurrent archiving. !30064
- Fix charts on Cluster health page. !30073
- Display boards filter bar on mobile. !30120
- Fix IDE editor not showing when switching back from preview. !30135
- Support note position tracing on an image. !30158
- Replace slugifyWithHyphens with improved slugify function. !30172 (Luke Ward)
- 'Open' and 'Closed' issue board lists no longer display a redundant tooltip. !30187
- Fix pipelines table to update without refreshing after action. !30190
- Change ruby_process_start_time_seconds metric to unix timestamp instead of seconds from boot. !30195
- Fix attachments using the wrong URLs in e-mails. !30197
- Make sure UnicornSampler is started only in master process. !30215
- Don't show image diff note on text file. !30221
- Fix median counting for cycle analytics. !30229
- In WebIDE allow adding new entries of the same name as deleted entry. !30239
- Don't let logged out user do manual order. !30264
- Skip spam check for task list updates. !30279
- Make Housekeeping button do a full garbage collection. !30289
- Removing an image should not output binary data. !30314
- Fix spacing issues for toasts. !30345
- Fix race in forbid_sidekiq_in_transactions.rb. !30359
- Fixed back navigation for projects filter. !30373
- Fix environments broken terminal. !30401
- Fix invalid SSL certificate errors on Drone CI service. !30422
- Fix subgroup url in search drop down. !30457
- Make unicorn_workers to return meaningful results. !30506
- Fix wrong URL when creating milestones from instance milestones dashboard. !30512
- Fixed incorrect line wrap for assignee label in issues. !30523 (Marc Schwede)
- Improves section header whitespace on the CI/CD Charts page. !30531
- Prevent multiple confirmation modals from opening when deleting a repository. !30532
- Aligns CI icon in Merge Request dashboard. !30558
- Add text-secondary to controls in project list. !30567
- Review Tools: Add large z-index to toolbar. !30583
- Hide restricted and disallowed visibility radios. !30590
- Resolve Label picker: Line break on long label titles. !30610
- Fix a bug that prevented projects containing merge request diff comments from being imported. !30630
- I fixed z index bug in diff page. !30657 (Faruk Can)
- Allow client authentication method to be configured for OpenID Connect. !30683 (Vincent Fazio)
- Fix commenting before discussions are loaded. !30724
- Fix linebreak rendering in Mermaid flowcharts. !30730
- Make httpclient respect system SSL configuration. !30749
- Bump fog-aws to v3.5.2. !30803
- API: Allow changing only ci_default_git_depth. !30888 (Mathieu Parent)
- Search issuables by iids. (Riccardo Padovani)
- Fix broken warnings while Editing Issues and Edit File on MR.
- Make sure we are receiving the proper information on the MR Popover by updating the IID in the graphql query.
### Changed (39 changes, 8 of them are from the community)
- Improve group list UI. !26542
- Backport and Docs for Paginate license management and add license search. !27602
- Update merge requests section description text on project settings page. !27838
- Knative version bump 0.5 -> 0.6. !28798 (Chris Baumbauer)
- Add salesforce logo for salesforce SSO. !28857
- Enforced requirements for UltraAuth users. !28941 (Kartikey Tanna)
- Return 400 when deleting tags more often than once per hour. !29448
- Add identity information to external authorization requests. !29461
- Enable just-in-time Kubernetes resource creation for project-level clusters. !29515
- renamed discussion to thread in merge-request and issue timeline. !29553 (Michel Engelen)
- Changed HTTP Status Code for disabled repository on /branches and /commits to 404. !29585 (Sam Battalio)
- Enable Git object pools. !29595 (jramsay)
- Updated container registry to display error message when special characters in path. Documentation has also been updated. !29616
- Allow developers to delete tags. !29668
- Will not update issue timestamps when changing positions in a list. !29677
- Include a link back to the MR for Visual Review feedback form. !29719
- Improve discussion reply buttons layout and how jump to next discussion button appears. !29779
- Renders a pre-release tag for releases. !29797
- Migrate NULL values for users.private_profile column and update users API to reject null value for private_profile. !29888
- Re-name files in Web IDE in a more natural way. !29948
- Include events from subgroups in group's activity. !29953 (Fabian Schneider @fabsrc)
- Upgrade to Gitaly v1.49.0. !29990
- Remove group and instance clusters feature flag. !30124
- Add support for creating random passwords in user creation API. !30138
- Support CIDR notation in IP rate limiter. !30146
- Add Redis call details in Peek performance bar. !30191
- Create Knative role and binding with service account. !30235
- Add cleanup migration for MR's multiple assignees. !30261
- Updates PHP template to php:latest to ensure always targeting latest stable. !30319 (Paul Giberson)
- Format `from` and `to` fields in JSON audit log. !30333
- Upgrade to Gitaly v1.51.0. !30353
- Modify cycle analytics on project level. !30356
- Extract clair version as CLAIR_EXECUTABLE_VERSION variable and update clair executable from v8 to v11. !30396
- Upgrade Rouge to 3.5.1. !30431
- Move multiple issue boards to core. !30503
- Upgrade to Gitaly v1.52.0. !30568
- Upgrade to Gitaly v1.53.0. !30614
- Open WebIDE in fork when user doesn't have access. !30642
- Propagate python version variable. (Can Eldem)
### Performance (25 changes, 1 of them is from the community)
- Remove tooltip directive on project avatar image component. !29631 (George Tsiolis)
- Use Rugged if we detect storage is NFS and we can access the disk. !29725
- Add endpoint for fetching diverging commit counts. !29802
- Cache feature flag names in Redis for a minute. !29816
- Avoid storing backtraces from Bitbucket Cloud imports in the database. !29862
- Remove import columns from projects table. !29863
- Enable Gitaly ref name caching for discussions.json. !29951
- Allow caching of negative FindCommit matches. !29952
- Eliminate N+1 queries in Dashboard::TodosController. !29954
- Memoize non-existent custom appearances. !29957
- Add a separate endpoint for fetching MRs serialized as widgets. !29979
- Use CTE to fetch clusters hierarchy in single query. !30063
- Enable Gitaly ref caching for SearchController. !30105
- Avoid loading pipeline status in search results. !30111
- Improve performance of MergeRequestsController#ci_environment_status endpoint. !30224
- Add a memory cache local to the thread to reduce Redis load. !30233
- Cache Flipper persisted names directly to local memory storage. !30265
- Limit amount of JUnit tests returned. !30274
- Cache Flipper feature flags in L1 and L2 caches. !30276
- Prevent amplification of ReactiveCachingWorker jobs upon failures. !30432
- Allow ReactiveCaching to support nil value. !30456
- Improve performance of fetching environments statuses. !30560
- Do Redis lookup in batches in ActiveSession.sessions_from_ids. !30561
- Remove catfile cache feature flag. !30750
- Fix Gitaly auto-detection caching. !30954
### Added (46 changes, 12 of them are from the community)
- Document the negative commit message push rule for the API. !14004 (Maikel Vlasman)
- Expose saml_provider_id in the users API. !14045
- Improve Project API. !28327 (Mathieu Parent)
- Remove Sentry from application settings. !28447 (Roger Meier)
- Implement borderless discussion design with new reply field. !28580
- Enable terminals for instance and group clusters. !28613
- Resolve Multiple discussions per line in merge request diffs. !28748
- Adds link to Grafana in Admin > Monitoring settings when grafana is enabled in config. !28937 (Romain Maneschi)
- Bring Manual Ordering on Issue List. !29410
- Added commit type to tree GraphQL response. !29412
- New API for User Counts, updates on success of an MR the count on top and in other tabs. !29441
- Add option to limit time tracking units to hours. !29469 (Jon Kolb)
- Add confirmation for registry image deletion. !29505
- Sync merge ref upon mergeability check. !29569
- Show an Upcoming Status for Releases. !29577
- Add order_by and sort params to list runner jobs api. !29629 (Sujay Patel)
- Allow custom username for deploy tokens. !29639
- Add a verified pill next to email addresses under the admin users section. !29669
- Add rake task to clean orphan artifact files. !29681
- Render GFM in GraphQL. !29700
- Upgrade asciidoctor version to 2.0.10. !29741 (Rajendra Kadam)
- Allow auto-completing scoped labels. !29749
- Enable syntax highlighting for AsciiDoc. !29835 (Guillaume Grossetie)
- Expose placeholder element for metrics charts in GFM. !29861
- Added a min schema version check to db:migrate. !29882
- Extract zoom link from issue and pass to frontend. !29910 (raju249)
- GraphQL mutations for add, remove and toggle emoji. !29919
- Labeled issue boards can now collapse. !29955
- Allow Ingress to be uninstalled from the UI. !29977
- Add permission check to metrics dashboards endpoint. !30017
- Allow JupyterHub to be uninstalled from the UI. !30097
- Allow GitLab Runner to be uninstalled from the UI. !30176
- GraphQL mutations for managing Notes. !30210
- Add API for CRUD group clusters. !30213
- Add endpoint to move multiple issues in boards. !30216
- Enable terminals button for group clusters. !30255
- Prevent excessive sanitization of AsciiDoc ouptut. !30290 (Guillaume Grossetie)
- Extend `MergeToRefService` to create merge ref from an arbitrary ref. !30361
- Add CI variable to provide GitLab HOST. !30417
- Add migration for adding rule_type to approval_project_rules. !30575
- Enable section anchors in Asciidoctor. !30666 (Guillaume Grossetie)
- Preserve footnote link ids in Asciidoctor. !30790 (Guillaume Grossetie)
- Add support for generating SSL certificates for custon pages domains through Let's Encrypt.
- Introduce default: for gitlab-ci.yml.
- Move Multiple Issue Boards for Projects to Core.
- Add Gitaly data to the usage ping.
### Other (35 changes, 15 of them are from the community)
- Remove unresolved class and fixed height in discussion header. !28440 (David Palubin)
- Moved EE/CE code differences for file `app/views/search/_category.html.haml` into CE. !28755 (Michel Engelen)
- Changes "Todo" to "To Do" in the UI for clarity. !28844
- Migrate GitLab managed project-level clusters to unmanaged if a Kubernetes namespace was unable to be created. !29251
- Migrate GitLab managed project-level clusters to unmanaged if they are missing a Kubernetes service account token. !29648
- Add strategies column to operations_feature_flag_scopes table. !29808
- Disallow `NULL` values for `geo_nodes.primary` column. !29818 (Arun Kumar Mohan)
- Replace 'JIRA' with 'Jira'. !29849 (Takuya Noguchi)
- Support jsonb default in add_column_with_default migration helper. !29871
- Update pagination prev and next texts. !29911
- Adds metrics to measure cost of expensive operations. !29928
- Always allow access to health endpoints from localhost in dev. !29930
- Update GitLab Runner Helm Chart to 0.6.0. !29982
- Use darker gray color for system note metadata and edited text. !30054
- Fix typo in docs about Elasticsearch. !30162 (Takuya Noguchi)
- Fix typo in code comments about Elasticsearch. !30163 (Takuya Noguchi)
- Update mixin-deep to 1.3.2. !30223 (Takuya Noguchi)
- Migrate markdown header_spec.js to Jest. !30228 (Martin Hobert)
- Remove istanbul JavaScript package. !30232 (Takuya Noguchi)
- Centralize markdownlint configuration. !30263
- Use PostgreSQL 9.6.11 in CI tests. !30270 (Takuya Noguchi)
- Fix typo in updateResolvableDiscussionsCounts action. !30278 (Frank van Rest)
- Change color for namespace in commit search. !30312
- Remove applySuggestion from notes service. !30399 (Frank van Rest)
- Improved readability of storage statistics in group / project admin area. !30406
- Alignign empty container registry message with design guidelines. !30502
- Remove toggleAward from notes service. !30536 (Frank van Rest)
- Remove deleteNote from notes service. !30537 (Frank van Rest)
- change the use of boardService in favor of boardsStore on footer for the board component. !30616 (eduarmreyes)
- Update example Prometheus scrape config. !30739
- Update GitLab Pages to v1.7.0.
- Add token_encrypted column to operations_feature_flags_clients table.
- Removes EE diff for app/views/profiles/preferences/show.html.haml.
- Removes EE differences for app/views/layouts/fullscreen.html.haml.
- Removes EE differences for app/views/admin/users/show.html.haml.
## 12.0.3 (2019-06-27) ## 12.0.3 (2019-06-27)
- No changes. - No changes.
@ -415,6 +735,15 @@ entry.
- Moves snowplow to CE repo. - Moves snowplow to CE repo.
## 11.11.4 (2019-06-26)
### Fixed (3 changes)
- Fix Fogbugz Importer not working. !29383
- Fix scrolling to top on assignee change. !29500
- Fix IDE commit using latest ref in branch and overriding contents. !29769
## 11.11.3 (2019-06-10) ## 11.11.3 (2019-06-10)
### Fixed (5 changes) ### Fixed (5 changes)
@ -628,6 +957,27 @@ entry.
- Add some frozen string to spec/**/*.rb. (gfyoung) - Add some frozen string to spec/**/*.rb. (gfyoung)
## 11.10.8 (2019-06-27)
- No changes.
### Security (10 changes)
- Fix Denial of Service for comments when rendering issues/MR comments.
- Gate MR head_pipeline behind read_pipeline ability.
- Fix DoS vulnerability in color validation regex.
- Expose merge requests count based on user access.
- Persist tmp snippet uploads at users.
- Add missing authorizations in GraphQL.
- Disable Rails SQL query cache when applying service templates.
- Prevent Billion Laughs attack.
- Correctly check permissions when creating snippet notes.
- Prevent the detection of merge request templates by unauthorized users.
### Performance (1 change)
- Add improvements to global search of issues and merge requests. !27817
## 11.10.6 (2019-06-04) ## 11.10.6 (2019-06-04)
### Fixed (7 changes, 1 of them is from the community) ### Fixed (7 changes, 1 of them is from the community)

View file

@ -19,4 +19,5 @@ unless helper.release_automation?
danger.import_dangerfile(path: 'danger/single_codebase') danger.import_dangerfile(path: 'danger/single_codebase')
danger.import_dangerfile(path: 'danger/gitlab_ui_wg') danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
danger.import_dangerfile(path: 'danger/ce_ee_vue_templates') danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
danger.import_dangerfile(path: 'danger/only_documentation')
end end

View file

@ -1 +1 @@
1.47.3 1.53.3

View file

@ -1 +1 @@
1.6.3 1.7.2

28
Gemfile
View file

@ -1,6 +1,6 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'rails', '5.1.7' gem 'rails', '5.2.3'
# Improves copy-on-write performance for MRI # Improves copy-on-write performance for MRI
gem 'nakayoshi_fork', '~> 0.0.4' gem 'nakayoshi_fork', '~> 0.0.4'
@ -11,7 +11,7 @@ gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0' gem 'sprockets', '~> 3.7.0'
# Default values for AR models # Default values for AR models
gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for' gem 'default_value_for', '~> 3.2.0'
# Supported DBs # Supported DBs
gem 'mysql2', '~> 0.4.10', group: :mysql gem 'mysql2', '~> 0.4.10', group: :mysql
@ -84,6 +84,7 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
gem 'graphql', '~> 1.8.0' gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10' gem 'graphiql-rails', '~> 1.4.10'
gem 'apollo_upload_server', '~> 2.0.0.beta3' gem 'apollo_upload_server', '~> 2.0.0.beta3'
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
@ -99,7 +100,7 @@ gem 'carrierwave', '~> 1.3'
gem 'mini_magick' gem 'mini_magick'
# for backups # for backups
gem 'fog-aws', '~> 3.3' gem 'fog-aws', '~> 3.5'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421. # Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb. # Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0' gem 'fog-core', '= 2.1.0'
@ -129,10 +130,10 @@ gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.8' gem 'asciidoctor', '~> 2.0.10'
gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
gem 'asciidoctor-plantuml', '0.0.8' gem 'asciidoctor-plantuml', '0.0.9'
gem 'rouge', '~> 3.1' gem 'rouge', '~> 3.5'
gem 'truncato', '~> 0.7.11' gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0' gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.3' gem 'nokogiri', '~> 1.10.3'
@ -211,7 +212,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
# HipChat integration # HipChat integration
gem 'hipchat', '~> 1.5.0' gem 'hipchat', '~> 1.5.0'
# JIRA integration # Jira integration
gem 'jira-ruby', '~> 1.4' gem 'jira-ruby', '~> 1.4'
# Flowdock integration # Flowdock integration
@ -300,13 +301,16 @@ gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0' gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0' gem 'peek-redis', '~> 1.2.0'
# Memory benchmarks
gem 'derailed_benchmarks', require: false
# Metrics # Metrics
group :metrics do group :metrics do
gem 'method_source', '~> 0.8', require: false gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.9.4' gem 'prometheus-client-mmap', '~> 0.9.8'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
@ -336,7 +340,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0' gem 'database_cleaner', '~> 1.7.0'
gem 'factory_bot_rails', '~> 4.8.2' gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.7.0' gem 'rspec-rails', '~> 3.8.0'
gem 'rspec-retry', '~> 0.6.1' gem 'rspec-retry', '~> 0.6.1'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
@ -365,6 +369,7 @@ group :development, :test do
gem 'haml_lint', '~> 0.31.0', require: false gem 'haml_lint', '~> 0.31.0', require: false
gem 'simplecov', '~> 0.16.1', require: false gem 'simplecov', '~> 0.16.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false
gem 'mdl', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
@ -374,7 +379,6 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '1.0' gem 'activerecord_sane_schema_dumper', '1.0'
gem 'stackprof', '~> 0.2.10', require: false gem 'stackprof', '~> 0.2.10', require: false
gem 'derailed_benchmarks', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false gem 'simple_po_parser', '~> 1.1.2', require: false
@ -417,7 +421,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support # SSH host key support
gem 'net-ssh', '~> 5.0' gem 'net-ssh', '~> 5.2'
gem 'sshkey', '~> 2.0' gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support # Required for ED25519 SSH host key support
@ -427,7 +431,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly' gem 'gitaly-proto', '~> 1.37.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0' gem 'grpc', '~> 1.19.0'

View file

@ -6,44 +6,48 @@ GEM
ace-rails-ap (4.1.2) ace-rails-ap (4.1.2)
acme-client (2.0.2) acme-client (2.0.2)
faraday (~> 0.9, >= 0.9.1) faraday (~> 0.9, >= 0.9.1)
actioncable (5.1.7) actioncable (5.2.3)
actionpack (= 5.1.7) actionpack (= 5.2.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (~> 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.1.7) actionmailer (5.2.3)
actionpack (= 5.1.7) actionpack (= 5.2.3)
actionview (= 5.1.7) actionview (= 5.2.3)
activejob (= 5.1.7) activejob (= 5.2.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.1.7) actionpack (5.2.3)
actionview (= 5.1.7) actionview (= 5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
rack (~> 2.0) rack (~> 2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.7) actionview (5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.1.7) activejob (5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.1.7) activemodel (5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
activerecord (5.1.7) activerecord (5.2.3)
activemodel (= 5.1.7) activemodel (= 5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
arel (~> 8.0) arel (>= 9.0)
activerecord-explain-analyze (0.1.0) activerecord-explain-analyze (0.1.0)
activerecord (>= 4) activerecord (>= 4)
pg pg
activerecord_sane_schema_dumper (1.0) activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6) rails (>= 5, < 6)
activesupport (5.1.7) activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
marcel (~> 0.3.1)
activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -60,17 +64,17 @@ GEM
apollo_upload_server (2.0.0.beta.3) apollo_upload_server (2.0.0.beta.3)
graphql (>= 1.8) graphql (>= 1.8)
rails (>= 4.2) rails (>= 4.2)
arel (8.0.0) arel (9.0.0)
asana (0.8.1) asana (0.8.1)
faraday (~> 0.9) faraday (~> 0.9)
faraday_middleware (~> 0.9) faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0) oauth2 (~> 1.0)
asciidoctor (1.5.8) asciidoctor (2.0.10)
asciidoctor-include-ext (0.3.1) asciidoctor-include-ext (0.3.1)
asciidoctor (>= 1.5.6, < 3.0.0) asciidoctor (>= 1.5.6, < 3.0.0)
asciidoctor-plantuml (0.0.8) asciidoctor-plantuml (0.0.9)
asciidoctor (~> 1.5) asciidoctor (>= 1.5.6, < 3.0.0)
ast (2.4.0) ast (2.4.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
@ -163,6 +167,8 @@ GEM
html-pipeline html-pipeline
declarative (0.0.10) declarative (0.0.10)
declarative-option (0.1.0) declarative-option (0.1.0)
default_value_for (3.2.0)
activerecord (>= 3.2.0, < 6.0)
derailed_benchmarks (1.3.5) derailed_benchmarks (1.3.5)
benchmark-ips (~> 2) benchmark-ips (~> 2)
get_process_mem (~> 0) get_process_mem (~> 0)
@ -214,6 +220,8 @@ GEM
excon (0.62.0) excon (0.62.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extended-markdown-filter (0.6.0)
html-pipeline (~> 2.0)
factory_bot (4.8.2) factory_bot (4.8.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_bot_rails (4.8.2) factory_bot_rails (4.8.2)
@ -245,7 +253,7 @@ GEM
fog-json fog-json
ipaddress (~> 0.8) ipaddress (~> 0.8)
xml-simple (~> 1.1) xml-simple (~> 1.1)
fog-aws (3.3.0) fog-aws (3.5.2)
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (~> 1.1) fog-json (~> 1.1)
fog-xml (~> 0.1) fog-xml (~> 0.1)
@ -288,6 +296,7 @@ GEM
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
gemoji (3.0.1)
gemojione (3.3.0) gemojione (3.3.0)
json json
get_process_mem (0.2.3) get_process_mem (0.2.3)
@ -301,11 +310,9 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (1.32.0) gitaly-proto (1.37.0)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0)
gitlab-labkit (0.3.0) gitlab-labkit (0.3.0)
actionpack (~> 5) actionpack (~> 5)
activesupport (~> 5) activesupport (~> 5)
@ -370,6 +377,14 @@ GEM
railties railties
sprockets-rails sprockets-rails
graphql (1.8.1) graphql (1.8.1)
graphql-docs (1.6.0)
commonmarker (~> 0.16)
escape_utils (~> 1.2)
extended-markdown-filter (~> 0.4)
gemoji (~> 3.0)
graphql (~> 1.6)
html-pipeline (~> 2.8)
sass (~> 3.4)
grpc (1.19.0) grpc (1.19.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
@ -459,6 +474,7 @@ GEM
kgio (2.11.2) kgio (2.11.2)
knapsack (1.17.0) knapsack (1.17.0)
rake rake
kramdown (1.17.0)
kubeclient (4.2.2) kubeclient (4.2.2)
http (~> 3.0) http (~> 3.0)
recursive-open-struct (~> 1.0, >= 1.0.4) recursive-open-struct (~> 1.0, >= 1.0.4)
@ -492,6 +508,12 @@ GEM
mail (2.7.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
mail_room (0.9.1) mail_room (0.9.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
mdl (0.5.0)
kramdown (~> 1.12, >= 1.12.0)
mixlib-cli (~> 1.7, >= 1.7.0)
mixlib-config (~> 2.2, >= 2.2.1)
memoist (0.16.0) memoist (0.16.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
@ -505,6 +527,9 @@ GEM
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
mixlib-cli (1.7.0)
mixlib-config (2.2.18)
tomlrb
msgpack (1.2.10) msgpack (1.2.10)
multi_json (1.13.1) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
@ -515,7 +540,7 @@ GEM
mysql2 (0.4.10) mysql2 (0.4.10)
nakayoshi_fork (0.0.4) nakayoshi_fork (0.0.4)
net-ldap (0.16.0) net-ldap (0.16.0)
net-ssh (5.0.1) net-ssh (5.2.0)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.10.3) nokogiri (1.10.3)
@ -652,7 +677,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.4) prometheus-client-mmap (0.9.8)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
@ -687,17 +712,18 @@ GEM
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-timeout (0.5.1) rack-timeout (0.5.1)
rails (5.1.7) rails (5.2.3)
actioncable (= 5.1.7) actioncable (= 5.2.3)
actionmailer (= 5.1.7) actionmailer (= 5.2.3)
actionpack (= 5.1.7) actionpack (= 5.2.3)
actionview (= 5.1.7) actionview (= 5.2.3)
activejob (= 5.1.7) activejob (= 5.2.3)
activemodel (= 5.1.7) activemodel (= 5.2.3)
activerecord (= 5.1.7) activerecord (= 5.2.3)
activesupport (= 5.1.7) activestorage (= 5.2.3)
activesupport (= 5.2.3)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.1.7) railties (= 5.2.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
@ -711,12 +737,12 @@ GEM
rails-i18n (5.1.1) rails-i18n (5.1.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
railties (5.1.7) railties (5.2.3)
actionpack (= 5.1.7) actionpack (= 5.2.3)
activesupport (= 5.1.7) activesupport (= 5.2.3)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.19.0) raindrops (0.19.0)
rake (12.3.2) rake (12.3.2)
@ -770,41 +796,41 @@ GEM
retriable (3.1.2) retriable (3.1.2)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.3.0) rouge (3.5.1)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2) rqrcode (>= 0.4.2)
rspec (3.7.0) rspec (3.8.0)
rspec-core (~> 3.7.0) rspec-core (~> 3.8.0)
rspec-expectations (~> 3.7.0) rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.7.0) rspec-mocks (~> 3.8.0)
rspec-core (3.7.1) rspec-core (3.8.2)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-expectations (3.7.0) rspec-expectations (3.8.4)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-mocks (3.7.0) rspec-mocks (3.8.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-parameterized (0.4.2) rspec-parameterized (0.4.2)
binding_ninja (>= 0.2.3) binding_ninja (>= 0.2.3)
parser parser
proc_to_ast proc_to_ast
rspec (>= 2.13, < 4) rspec (>= 2.13, < 4)
unparser unparser
rspec-rails (3.7.2) rspec-rails (3.8.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.7.0) rspec-core (~> 3.8.0)
rspec-expectations (~> 3.7.0) rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.7.0) rspec-mocks (~> 3.8.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-retry (0.6.1) rspec-retry (0.6.1)
rspec-core (> 3.3) rspec-core (> 3.3)
rspec-set (0.1.3) rspec-set (0.1.3)
rspec-support (3.7.1) rspec-support (3.8.2)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
@ -943,6 +969,7 @@ GEM
parslet (~> 1.8.0) parslet (~> 1.8.0)
toml-rb (1.0.0) toml-rb (1.0.0)
citrus (~> 3.0, > 3.0) citrus (~> 3.0, > 3.0)
tomlrb (1.2.8)
truncato (0.7.11) truncato (0.7.11)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0) nokogiri (>= 1.7.0, <= 2.0)
@ -999,7 +1026,7 @@ GEM
hashdiff hashdiff
webpack-rails (0.9.11) webpack-rails (0.9.11)
railties (>= 3.2.0) railties (>= 3.2.0)
websocket-driver (0.6.5) websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.3)
wikicloth (0.8.1) wikicloth (0.8.1)
@ -1025,9 +1052,9 @@ DEPENDENCIES
akismet (~> 2.0) akismet (~> 2.0)
apollo_upload_server (~> 2.0.0.beta3) apollo_upload_server (~> 2.0.0.beta3)
asana (~> 0.8.1) asana (~> 0.8.1)
asciidoctor (~> 1.5.8) asciidoctor (~> 2.0.10)
asciidoctor-include-ext (~> 0.3.1) asciidoctor-include-ext (~> 0.3.1)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.9)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
@ -1056,6 +1083,7 @@ DEPENDENCIES
creole (~> 0.5.0) creole (~> 0.5.0)
database_cleaner (~> 1.7.0) database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.2.0) deckar01-task_list (= 2.2.0)
default_value_for (~> 3.2.0)
derailed_benchmarks derailed_benchmarks
device_detector device_detector
devise (~> 4.6) devise (~> 4.6)
@ -1077,7 +1105,7 @@ DEPENDENCIES
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
flowdock (~> 0.7) flowdock (~> 0.7)
fog-aliyun (~> 0.3) fog-aliyun (~> 0.3)
fog-aws (~> 3.3) fog-aws (~> 3.5)
fog-core (= 2.1.0) fog-core (= 2.1.0)
fog-google (~> 1.8) fog-google (~> 1.8)
fog-local (~> 0.6) fog-local (~> 0.6)
@ -1091,9 +1119,8 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 1.32.0) gitaly-proto (~> 1.37.0)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-labkit (~> 0.3.0) gitlab-labkit (~> 0.3.0)
gitlab-markup (~> 1.7.0) gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0) gitlab-sidekiq-fetcher (~> 0.4.0)
@ -1109,6 +1136,7 @@ DEPENDENCIES
grape_logging (~> 1.7) grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10) graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0) graphql (~> 1.8.0)
graphql-docs (~> 1.6.0)
grpc (~> 1.19.0) grpc (~> 1.19.0)
haml_lint (~> 0.31.0) haml_lint (~> 0.31.0)
hamlit (~> 2.8.8) hamlit (~> 2.8.8)
@ -1134,6 +1162,7 @@ DEPENDENCIES
lograge (~> 0.5) lograge (~> 0.5)
loofah (~> 2.2) loofah (~> 2.2)
mail_room (~> 0.9.1) mail_room (~> 0.9.1)
mdl (~> 0.5.0)
memory_profiler (~> 0.9) memory_profiler (~> 0.9)
method_source (~> 0.8) method_source (~> 0.8)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
@ -1142,7 +1171,7 @@ DEPENDENCIES
mysql2 (~> 0.4.10) mysql2 (~> 0.4.10)
nakayoshi_fork (~> 0.0.4) nakayoshi_fork (~> 0.0.4)
net-ldap net-ldap
net-ssh (~> 5.0) net-ssh (~> 5.2)
nokogiri (~> 1.10.3) nokogiri (~> 1.10.3)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.9) octokit (~> 4.9)
@ -1173,7 +1202,7 @@ DEPENDENCIES
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
pg (~> 1.1) pg (~> 1.1)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.4) prometheus-client-mmap (~> 0.9.8)
pry-byebug (~> 3.5.1) pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
puma (~> 3.12) puma (~> 3.12)
@ -1184,7 +1213,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.9.3) rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rack-timeout rack-timeout
rails (= 5.1.7) rails (= 5.2.3)
rails-controller-testing rails-controller-testing
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rainbow (~> 3.0) rainbow (~> 3.0)
@ -1199,10 +1228,10 @@ DEPENDENCIES
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 3.1) rouge (~> 3.5)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized rspec-parameterized
rspec-rails (~> 3.7.0) rspec-rails (~> 3.8.0)
rspec-retry (~> 0.6.1) rspec-retry (~> 0.6.1)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
rspec_junit_formatter rspec_junit_formatter

View file

@ -84,44 +84,29 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html [Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature flags
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).
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).
## Feature proposals from the 22nd to the 1st
To allow the Product and Engineering teams time to discuss issues that will be placed into an upcoming milestone,
Product Managers must have their proposal for that milestone ready by the 22nd of each month.
This proposal will be shared with Engineering for discussion, feedback, and planning.
The plan for the upcoming milestone must be finalized by the 1st of the month, one week before kickoff on the 8th.
## Feature freeze on the 7th for the release on the 22nd ## Feature freeze on the 7th for the release on the 22nd
The feature freeze on the 7th has been discontinued. The [transition period overview](https://gitlab.com/gitlab-org/release/docs/blob/21cbd409dd5f157fe252f254f3e897f01908abe2/general/deploy/auto-deploy-transition.md#transition) The feature freeze on the 7th has been discontinued. [Transition period overview](https://gitlab.com/gitlab-org/release/docs/blob/21cbd409dd5f157fe252f254f3e897f01908abe2/general/deploy/auto-deploy-transition.md#transition)
describes the change to this process. During the transition period, the only guarantee that describes the change to this process. During the transition period, the only guarantee that
a change will be included in the release on the 22nd is if the change has been a change will be included in the release on the 22nd is if the change has been
deployed to GitLab.com prior to this date. deployed to GitLab.com prior to this date.
### Feature flags
Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable, may be merged (and
picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created.
A level of common sense should be applied when deciding whether to have a feature
behind a feature flag off or on by default.
The following guidelines can be applied to help make this decision:
* If the feature is not fully ready or functioning, the feature flag should be disabled by default.
* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but
disabled via chatops before deployment on GitLab.com environments. If the performance concern is confirmed, the final release should have the feature flag disabled by default.
* In most other cases, the feature flag can be enabled by default.
For more information on rolling out changes using feature flags, read [through the documentation](https://docs.gitlab.com/ee/development/rolling_out_changes_using_feature_flags.html).
In order to build the final package and present the feature for self-hosted
customers, the feature flag should be removed. This should happen before the
22nd, ideally _at least_ 2 days before. That means MRs with feature
flags being picked at the 19th would have quite a tight schedule, so picking
these _earlier_ is preferable.
While rare, release managers may decide to reject picking a change into a stable
branch, even when feature flags are used. This might be necessary if the changes
are deemed problematic, too invasive, or there simply isn't enough time to
properly test how the changes behave on GitLab.com.
### Between the 1st and the 7th ### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration: These types of merge requests for the upcoming release need special consideration:

View file

@ -15,7 +15,7 @@ To see how GitLab looks please see the [features page on our website](https://ab
- Manage Git repositories with fine grained access controls that keep your code secure - Manage Git repositories with fine grained access controls that keep your code secure
- Perform code reviews and enhance collaboration with merge requests - Perform code reviews and enhance collaboration with merge requests
- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - Complete continuous integration (CI) and continuous deployment/delivery (CD) pipelines to build, test, and deploy your applications
- Each project can also have an issue tracker, issue board, and a wiki - Each project can also have an issue tracker, issue board, and a wiki
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license) - Completely free and open source (MIT Expat license)

View file

@ -1 +1 @@
12.0.9 12.1.11

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -12,6 +12,7 @@ const Api = {
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels', projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
@ -23,6 +24,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
projectTemplatesPath: '/api/:version/projects/:id/templates/:type', projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
userCountsPath: '/api/:version/user_counts',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
userPath: '/api/:version/users/:id', userPath: '/api/:version/users/:id',
userStatusPath: '/api/:version/users/:id/status', userStatusPath: '/api/:version/users/:id/status',
@ -113,6 +115,21 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
/**
* Get all projects for a forked relationship to a specified project
* @param {string} projectPath - Path or ID of a project
* @param {Object} params - Get request parameters
* @returns {Promise} - Request promise
*/
projectForks(projectPath, params) {
const url = Api.buildUrl(Api.forkedProjectsPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, { params });
},
/** /**
* Get all Merge Requests for a project, eventually filtering based on * Get all Merge Requests for a project, eventually filtering based on
* supplied parameters * supplied parameters
@ -296,6 +313,11 @@ const Api = {
}); });
}, },
userCounts() {
const url = Api.buildUrl(this.userCountsPath);
return axios.get(url);
},
userStatus(id, options) { userStatus(id, options) {
const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
return axios.get(url, { return axios.get(url, {

View file

@ -36,7 +36,8 @@ export default function renderMermaid($els) {
}); });
$els.each((i, el) => { $els.each((i, el) => {
const source = el.textContent; // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
/** /**
* Restrict the rendering to a certain amount of character to * Restrict the rendering to a certain amount of character to
@ -59,6 +60,14 @@ export default function renderMermaid($els) {
mermaid.init(undefined, el, id => { mermaid.init(undefined, el, id => {
const svg = document.getElementById(id); const svg = document.getElementById(id);
// As of https://github.com/knsv/mermaid/commit/57b780a0d,
// Mermaid will make two init callbacks:one to initialize the
// flow charts, and another to initialize the Gannt charts.
// Guard against an error caused by double initialization.
if (svg.classList.contains('mermaid')) {
return;
}
svg.classList.add('mermaid'); svg.classList.add('mermaid');
// pre > code > svg // pre > code > svg

View file

@ -1,6 +1,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import { n__ } from '~/locale'; import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
@ -53,12 +54,19 @@ export default Vue.extend({
const { issuesSize } = this.list; const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`; return `${n__('%d issue', '%d issues', issuesSize)}`;
}, },
caretTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() { isNewIssueShown() {
return ( return (
this.list.type === 'backlog' || this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank') (!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
); );
}, },
uniqueKey() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
}, },
watch: { watch: {
filter: { filter: {
@ -72,31 +80,34 @@ export default Vue.extend({
}, },
}, },
mounted() { mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({ const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
draggable: '.is-draggable', draggable: '.is-draggable',
handle: '.js-board-handle', handle: '.js-board-handle',
onEnd: e => { onEnd(e) {
sortableEnd(); sortableEnd();
const sortable = this;
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(); const order = sortable.toArray();
const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10)); const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => { instance.$nextTick(() => {
boardsStore.moveList(list, order); boardsStore.moveList(list, order);
}); });
} }
}, },
}); });
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); Sortable.create(this.$el.parentNode, sortableOptions);
}, },
created() { created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed; this.list.isExpanded = !isCollapsed;
} }
@ -105,16 +116,17 @@ export default Vue.extend({
showNewIssueForm() { showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}, },
toggleExpanded(e) { toggleExpanded() {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded; this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) { if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem( localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
} }
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
$('.tooltip').tooltip('hide');
} }
}, },
}, },

View file

@ -1,4 +1,5 @@
<script> <script>
import { __ } from '~/locale';
/* global ListLabel */ /* global ListLabel */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
@ -7,8 +8,8 @@ export default {
data() { data() {
return { return {
predefinedLabels: [ predefinedLabels: [
new ListLabel({ title: 'To Do', color: '#F0AD4E' }), new ListLabel({ title: __('To Do'), color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' }), new ListLabel({ title: __('Doing'), color: '#5CB85C' }),
], ],
}; };
}, },
@ -58,7 +59,11 @@ export default {
<template> <template>
<div class="board-blank-state p-3"> <div class="board-blank-state p-3">
<p>Add the following default lists to your Issue Board with one click:</p> <p>
{{
s__('BoardBlankState|Add the following default lists to your Issue Board with one click:')
}}
</p>
<ul class="list-unstyled board-blank-state-list"> <ul class="list-unstyled board-blank-state-list">
<li v-for="(label, index) in predefinedLabels" :key="index"> <li v-for="(label, index) in predefinedLabels" :key="index">
<span <span
@ -70,18 +75,21 @@ export default {
</li> </li>
</ul> </ul>
<p> <p>
Starting out with the default set of lists will get you right on the way to making the most of {{
your board. s__(
'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.',
)
}}
</p> </p>
<button <button
class="btn btn-success btn-inverted btn-block" class="btn btn-success btn-inverted btn-block"
type="button" type="button"
@click.stop="addDefaultLists" @click.stop="addDefaultLists"
> >
Add default lists {{ s__('BoardBlankState|Add default lists') }}
</button> </button>
<button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState"> <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState">
Nevermind, I'll use my own {{ s__("BoardBlankState|Nevermind, I'll use my own") }}
</button> </button>
</div> </div>
</template> </template>

View file

@ -0,0 +1,216 @@
<script>
import Flash from '~/flash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
};
export default {
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
DeprecatedModal,
},
props: {
canAdminBoard: {
type: Boolean,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: {
type: Array,
required: false,
default: () => [],
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
scopedLabelsDocumentationLink: {
type: String,
required: false,
default: '#',
},
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
currentBoard: boardsStore.state.currentBoard,
currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
computed: {
isNewForm() {
return this.currentPage === 'new';
},
isDeleteForm() {
return this.currentPage === 'delete';
},
isEditForm() {
return this.currentPage === 'edit';
},
isVisible() {
return this.currentPage !== '';
},
buttonText() {
if (this.isNewForm) {
return 'Create board';
}
if (this.isDeleteForm) {
return 'Delete';
}
return 'Save changes';
},
buttonKind() {
if (this.isNewForm) {
return 'success';
}
if (this.isDeleteForm) {
return 'danger';
}
return 'info';
},
title() {
if (this.isNewForm) {
return 'Create new board';
}
if (this.isDeleteForm) {
return 'Delete board';
}
if (this.readonly) {
return 'Board scope';
}
return 'Edit board';
},
readonly() {
return !this.canAdminBoard;
},
submitDisabled() {
return this.isLoading || this.board.name.length === 0;
},
},
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
methods: {
submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
gl.boardService
.deleteBoard(this.currentBoard)
.then(() => {
visitUrl(boardsStore.rootPath);
})
.catch(() => {
Flash('Failed to delete board. Please try again.');
this.isLoading = false;
});
} else {
gl.boardService
.createBoard(this.board)
.then(resp => resp.data)
.then(data => {
visitUrl(data.board_path);
})
.catch(() => {
Flash('Unable to save your changes. Please try again.');
this.isLoading = false;
});
}
},
cancel() {
boardsStore.showPage('');
},
resetFormState() {
if (this.isNewForm) {
// Clear the form when we open the "New board" modal
this.board = { ...boardDefaults };
} else if (this.currentBoard && Object.keys(this.currentBoard).length) {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
},
};
</script>
<template>
<deprecated-modal
v-show="isVisible"
:hide-footer="readonly"
:title="title"
:primary-button-label="buttonText"
:kind="buttonKind"
:submit-disabled="submitDisabled"
modal-dialog-class="board-config-modal"
@cancel="cancel"
@submit="submit"
>
<template slot="body">
<p v-if="isDeleteForm">Are you sure you want to delete this board?</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name"> Board name </label>
<input
id="board-new-name"
ref="name"
v-model="board.name"
class="form-control"
type="text"
placeholder="Enter board name"
@keyup.enter="submit"
/>
</div>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
:milestone-path="milestonePath"
:labels-path="labelsPath"
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights"
/>
</form>
</template>
</deprecated-modal>
</template>

View file

@ -227,7 +227,7 @@ export default {
:class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }" :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
class="board-list-component position-relative h-100" class="board-list-component position-relative h-100"
> >
<div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues"> <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon /> <gl-loading-icon />
</div> </div>
<board-new-issue <board-new-issue
@ -257,7 +257,7 @@ export default {
/> />
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
<gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize"> Showing all issues </span> <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span> <span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span>
</li> </li>
</ul> </ul>

View file

@ -102,9 +102,9 @@ export default {
<div class="board-card position-relative p-3 rounded"> <div class="board-card position-relative p-3 rounded">
<form @submit="submit($event)"> <form @submit="submit($event)">
<div v-if="error" class="flash-container"> <div v-if="error" class="flash-container">
<div class="flash-alert">An error occurred. Please try again.</div> <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div>
</div> </div>
<label :for="list.id + '-title'" class="label-bold"> Title </label> <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label>
<input <input
:id="list.id + '-title'" :id="list.id + '-title'"
ref="input" ref="input"
@ -122,12 +122,11 @@ export default {
class="float-left" class="float-left"
variant="success" variant="success"
type="submit" type="submit"
>{{ __('Submit issue') }}</gl-button
> >
Submit issue <gl-button class="float-right" type="button" variant="default" @click="cancel">{{
</gl-button> __('Cancel')
<gl-button class="float-right" type="button" variant="default" @click="cancel"> }}</gl-button>
Cancel
</gl-button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -38,6 +38,7 @@ export default Vue.extend({
issue: {}, issue: {},
list: {}, list: {},
loadingAssignees: false, loadingAssignees: false,
timeTrackingLimitToHours: boardsStore.timeTracking.limitToHours,
}; };
}, },
computed: { computed: {

View file

@ -0,0 +1,334 @@
<script>
import { throttle } from 'underscore';
import {
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
name: 'BoardsSelector',
components: {
Icon,
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
},
props: {
currentBoard: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
throttleDuration: {
type: Number,
default: 200,
},
boardBaseUrl: {
type: String,
required: true,
},
hasMissingBoards: {
type: Boolean,
required: true,
},
canAdminBoard: {
type: Boolean,
required: true,
},
multipleIssueBoardsAvailable: {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
},
weights: {
type: Array,
required: true,
},
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
scopedLabelsDocumentationLink: {
type: String,
required: false,
default: '#',
},
},
data() {
return {
loading: true,
hasScrollFade: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
store: boardsStore,
filterTerm: '',
};
},
computed: {
currentPage() {
return this.state.currentPage;
},
filteredBoards() {
return this.boards.filter(board =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
reload: {
get() {
return this.state.reload;
},
set(newValue) {
this.state.reload = newValue;
},
},
board() {
return this.state.currentBoard;
},
showDelete() {
return this.boards.length > 1;
},
scrollFadeClass() {
return {
'fade-out': !this.hasScrollFade,
};
},
showRecentSection() {
return (
this.recentBoards.length &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
filteredBoards() {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
reload() {
if (this.reload) {
this.boards = [];
this.recentBoards = [];
this.loading = true;
this.reload = false;
this.loadBoards(false);
}
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
},
methods: {
showPage(page) {
boardsStore.showPage(page);
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
}
const recentBoardsPromise = new Promise((resolve, reject) =>
gl.boardService
.recentBoards()
.then(resolve)
.catch(err => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err.response.status === httpStatusCodes.UNAUTHORIZED) {
resolve({ data: [] }); // recent boards are empty
return;
}
reject(err);
}),
);
Promise.all([gl.boardService.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;
this.boards = allBoardsJson;
this.recentBoards = recentBoardsJson;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {
this.loading = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
const currentPosition = this.contentClientHeight + content.scrollTop;
return content && currentPosition < this.maxPosition;
},
initScrollFade() {
this.scrollFadeInitialized = true;
const { content } = this.$refs;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
setScrollFade() {
if (!this.scrollFadeInitialized) this.initScrollFade();
this.hasScrollFade = this.isScrolledUp();
},
},
};
</script>
<template>
<div class="boards-switcher js-boards-selector append-right-10">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-dropdown
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
:text="board.name"
@show="loadBoards"
>
<div>
<div class="dropdown-title mb-0" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</div>
</div>
<gl-dropdown-header class="mt-0">
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" />
</gl-dropdown-header>
<div
v-if="!loading"
ref="content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
<gl-dropdown-item
v-show="filteredBoards.length === 0"
class="no-pointer-events text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</gl-dropdown-item>
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
{{ __('Recent') }}
</h6>
<template v-if="showRecentSection">
<gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
</gl-dropdown-item>
</template>
<hr v-if="showRecentSection" class="my-1" />
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
{{ __('All') }}
</h6>
<gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="hasMissingBoards" class="small unclickable">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
</gl-dropdown-item>
</div>
<div
v-show="filteredBoards.length > 0"
class="dropdown-content-faded-mask"
:class="scrollFadeClass"
></div>
<gl-loading-icon v-if="loading" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
<gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')">
{{ s__('IssueBoards|Create new board') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="showDelete"
class="text-danger"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
</gl-dropdown-item>
</div>
</gl-dropdown>
<board-form
v-if="currentPage"
:milestone-path="milestonePath"
:labels-path="labelsPath"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
/>
</span>
</div>
</template>

View file

@ -124,7 +124,7 @@ export default {
return `${this.rootPath}${assignee.username}`; return `${this.rootPath}${assignee.username}`;
}, },
avatarUrlTitle(assignee) { avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`; return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
}, },
showLabel(label) { showLabel(label) {
if (!label.id) return false; if (!label.id) return false;
@ -160,9 +160,10 @@ export default {
:title="__('Confidential')" :title="__('Confidential')"
class="confidential-icon append-right-4" class="confidential-icon append-right-4"
:aria-label="__('Confidential')" :aria-label="__('Confidential')"
/><a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{ />
issue.title <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>
}}</a> {{ issue.title }}
</a>
</h4> </h4>
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
@ -204,13 +205,13 @@ export default {
placement="bottom" placement="bottom"
class="board-issue-path block-truncated bold" class="board-issue-path block-truncated bold"
>{{ issueReferencePath }}</tooltip-on-truncate >{{ issueReferencePath }}</tooltip-on-truncate
>#{{ issue.iid }} >
#{{ issue.iid }}
</span> </span>
<span class="board-info-items prepend-top-8 d-inline-block"> <span class="board-info-items prepend-top-8 d-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" /><issue-time-estimate <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" />
v-if="issue.timeEstimate" <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
:estimate="issue.timeEstimate" <issue-card-weight
/><issue-card-weight
v-if="issue.weight" v-if="issue.weight"
:weight="issue.weight" :weight="issue.weight"
@click="filterByWeight(issue.weight)" @click="filterByWeight(issue.weight)"
@ -230,7 +231,8 @@ export default {
tooltip-placement="bottom" tooltip-placement="bottom"
> >
<span class="js-assignee-tooltip"> <span class="js-assignee-tooltip">
<span class="bold d-block">Assignee</span> {{ assignee.name }} <span class="bold d-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span> <span class="text-white-50">@{{ assignee.username }}</span>
</span> </span>
</user-avatar-link> </user-avatar-link>
@ -240,9 +242,8 @@ export default {
:title="assigneeCounterTooltip" :title="assigneeCounterTooltip"
class="avatar-counter" class="avatar-counter"
data-placement="bottom" data-placement="bottom"
>{{ assigneeCounterLabel }}</span
> >
{{ assigneeCounterLabel }}
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,6 +2,7 @@
import { GlTooltip } from '@gitlab/ui'; import { GlTooltip } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default { export default {
components: { components: {
@ -14,12 +15,17 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
limitToHours: boardsStore.timeTracking.limitToHours,
};
},
computed: { computed: {
title() { title() {
return stringifyTime(parseSeconds(this.estimate), true); return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
}, },
timeEstimate() { timeEstimate() {
return stringifyTime(parseSeconds(this.estimate)); return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
}, },
}, },
}; };

View file

@ -1,4 +1,5 @@
<script> <script>
import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
@ -20,19 +21,20 @@ export default {
computed: { computed: {
contents() { contents() {
const obj = { const obj = {
title: "You haven't added any issues to your project yet", title: __("You haven't added any issues to your project yet"),
content: ` content: __(
An issue can be a bug, a todo or a feature request that needs to be 'An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable.',
discussed in a project. Besides, issues are searchable and filterable. ),
`,
}; };
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = "You haven't selected any issues yet"; obj.title = __("You haven't selected any issues yet");
obj.content = ` obj.content = sprintf(
Go back to <strong>Open issues</strong> and select some issues __(
to add to your board. 'Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board.',
`; ),
{ startTag: '<strong>', endTag: '</strong>' },
);
} }
return obj; return obj;
@ -51,16 +53,16 @@ export default {
<div class="text-content"> <div class="text-content">
<h4>{{ contents.title }}</h4> <h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p> <p v-html="contents.content"></p>
<a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted"> <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
New issue __('New issue')
</a> }}</a>
<button <button
v-if="activeTab === 'selected'" v-if="activeTab === 'selected'"
class="btn btn-default" class="btn btn-default"
type="button" type="button"
@click="changeTab('all')" @click="changeTab('all')"
> >
Open issues {{ __('Open issues') }}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
<script> <script>
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __, n__ } from '../../../locale';
import ListsDropdown from './lists_dropdown.vue'; import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store'; import boardsStore from '../../stores/boards_store';
@ -11,7 +11,7 @@ export default {
components: { components: {
ListsDropdown, ListsDropdown,
}, },
mixins: [modalMixin], mixins: [modalMixin, footerEEMixin],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
@ -24,8 +24,8 @@ export default {
}, },
submitText() { submitText() {
const count = ModalStore.selectedCount(); const count = ModalStore.selectedCount();
if (!count) return __('Add issues');
return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`; return n__(`Add %d issue`, `Add %d issues`, count);
}, },
}, },
methods: { methods: {
@ -42,7 +42,7 @@ export default {
const req = this.buildUpdateRequest(list); const req = this.buildUpdateRequest(list);
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, req).catch(() => { boardsStore.bulkUpdate(issueIds, req).catch(() => {
Flash(__('Failed to update issues, please try again.')); Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach(issue => { selectedIssues.forEach(issue => {
@ -68,11 +68,11 @@ export default {
<button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues"> <button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues">
{{ submitText }} {{ submitText }}
</button> </button>
<span class="inline add-issues-footer-to-list"> to list </span> <span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown /> <lists-dropdown />
</div> </div>
<button class="btn btn-default float-right" type="button" @click="toggleModal(false)"> <button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
Cancel {{ __('Cancel') }}
</button> </button>
</footer> </footer>
</template> </template>

View file

@ -1,4 +1,5 @@
<script> <script>
import { __ } from '~/locale';
import ModalFilters from './filters'; import ModalFilters from './filters';
import ModalTabs from './tabs.vue'; import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
@ -30,10 +31,10 @@ export default {
computed: { computed: {
selectAllText() { selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all'; return __('Select all');
} }
return 'Deselect all'; return __('Deselect all');
}, },
showSearch() { showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
@ -57,7 +58,7 @@ export default {
type="button" type="button"
class="close" class="close"
data-dismiss="modal" data-dismiss="modal"
aria-label="Close" :aria-label="__('Close')"
@click="toggleModal(false)" @click="toggleModal(false)"
> >
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>

View file

@ -123,7 +123,9 @@ export default {
class="empty-state add-issues-empty-state-filter text-center" class="empty-state add-issues-empty-state-filter text-center"
> >
<div class="svg-content"><img :src="emptyStateSvg" /></div> <div class="svg-content"><img :src="emptyStateSvg" /></div>
<div class="text-content"><h4>There are no issues to show.</h4></div> <div class="text-content">
<h4>{{ __('There are no issues to show.') }}</h4>
</div>
</div> </div>
<div v-for="(group, index) in groupedIssues" :key="index" class="add-issues-list-column"> <div v-for="(group, index) in groupedIssues" :key="index" class="add-issues-list-column">
<div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent"> <div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent">

View file

@ -1,4 +1,5 @@
<script> <script>
import { __ } from '~/locale';
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
@ -27,7 +28,7 @@ export default {
}, },
computed: { computed: {
selectedProjectName() { selectedProjectName() {
return this.selectedProject.name || 'Select a project'; return this.selectedProject.name || __('Select a project');
}, },
}, },
mounted() { mounted() {
@ -81,7 +82,7 @@ export default {
<template> <template>
<div> <div>
<label class="label-bold prepend-top-10"> Project </label> <label class="label-bold prepend-top-10">{{ __('Project') }}</label>
<div ref="projectsDropdown" class="dropdown dropdown-projects"> <div ref="projectsDropdown" class="dropdown dropdown-projects">
<button <button
class="dropdown-menu-toggle wide" class="dropdown-menu-toggle wide"
@ -92,9 +93,9 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" /> {{ selectedProjectName }} <icon name="chevron-down" />
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">Projects</div> <div class="dropdown-title">{{ __('Projects') }}</div>
<div class="dropdown-input"> <div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" /> <input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
<icon name="search" class="dropdown-input-search" data-hidden="true" /> <icon name="search" class="dropdown-input-search" data-hidden="true" />
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>

View file

@ -76,7 +76,7 @@ export default Vue.extend({
<template> <template>
<div class="block list"> <div class="block list">
<button class="btn btn-default btn-block" type="button" @click="removeIssue"> <button class="btn btn-default btn-block" type="button" @click="removeIssue">
Remove from board {{ __('Remove from board') }}
</button> </button>
</div> </div>
</template> </template>

View file

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

View file

@ -2,7 +2,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable
import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager'; import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store'; import boardsStore from './stores/boards_store';
import { isEE } from '~/lib/utils/common_utils';
export default class FilteredSearchBoards extends FilteredSearchManager { export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
@ -10,7 +9,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
page: 'boards', page: 'boards',
isGroupDecendent: true, isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters', stateFiltersSelector: '.issues-state-filters',
isGroup: isEE(), isGroup: IS_EE,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
}); });

View file

@ -6,28 +6,31 @@ import { __ } from '~/locale';
import './models/label'; import './models/label';
import './models/assignee'; import './models/assignee';
import FilteredSearchBoards from './filtered_search_boards'; import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from './eventhub'; import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub'; import sidebarEventHub from '~/sidebar/event_hub';
import './models/issue'; import 'ee_else_ce/boards/models/issue';
import './models/list'; import 'ee_else_ce/boards/models/list';
import './models/milestone'; import '~/boards/models/milestone';
import './models/project'; import '~/boards/models/project';
import boardsStore from './stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import ModalStore from './stores/modal_store'; import ModalStore from '~/boards/stores/modal_store';
import BoardService from './services/board_service'; import BoardService from 'ee_else_ce/boards/services/board_service';
import modalMixin from './mixins/modal_mixins'; import modalMixin from '~/boards/mixins/modal_mixins';
import './filters/due_date_filters'; import '~/boards/filters/due_date_filters';
import Board from './components/board'; import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from './components/board_sidebar'; import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from './components/new_list_dropdown'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue'; import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor'; import '~/vue_shared/vue_resource_interceptor';
import { import {
NavigationType, NavigationType,
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
parseBoolean, parseBoolean,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
let issueBoardsApp; let issueBoardsApp;
@ -49,6 +52,7 @@ export default () => {
} }
boardsStore.create(); boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
@ -77,13 +81,14 @@ export default () => {
}, },
}, },
created() { created() {
gl.boardService = new BoardService({ boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint, boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint, recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint, listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath, bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId, boardId: this.boardId,
}); });
gl.boardService = new BoardService();
boardsStore.rootPath = this.boardsEndpoint; boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
@ -204,6 +209,8 @@ export default () => {
}, },
}); });
boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn'); const issueBoardsModal = document.getElementById('js-add-issues-btn');
if (issueBoardsModal) { if (issueBoardsModal) {
@ -277,4 +284,7 @@ export default () => {
`, `,
}); });
} }
toggleFocusMode(ModalStore, boardsStore);
mountMultipleBoardsSwitcher();
}; };

View file

@ -0,0 +1 @@
export default {};

View file

@ -1,7 +1,7 @@
/* global DocumentTouch */ /* global DocumentTouch */
import $ from 'jquery'; import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config'; import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() { export function sortableStart() {
$('.has-tooltip') $('.has-tooltip')
@ -20,7 +20,7 @@ export function getBoardSortableDefaultOptions(obj) {
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, { const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn', filter: '.no-drag',
delay: touchEnabled ? 100 : 0, delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100, scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,

View file

@ -5,7 +5,7 @@
import Vue from 'vue'; import Vue from 'vue';
import './label'; import './label';
import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssueProject from './project'; import IssueProject from './project';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
@ -91,13 +91,13 @@ class ListIssue {
addMilestone(milestone) { addMilestone(milestone) {
const miletoneId = this.milestone ? this.milestone.id : null; const miletoneId = this.milestone ? this.milestone.id : null;
if (isEE && milestone.id !== miletoneId) { if (IS_EE && milestone.id !== miletoneId) {
this.milestone = new ListMilestone(milestone); this.milestone = new ListMilestone(milestone);
} }
} }
removeMilestone(removeMilestone) { removeMilestone(removeMilestone) {
if (isEE && removeMilestone && removeMilestone.id === this.milestone.id) { if (IS_EE && removeMilestone && removeMilestone.id === this.milestone.id) {
this.milestone = {}; this.milestone = {};
} }
} }

View file

@ -4,7 +4,7 @@
import { __ } from '~/locale'; import { __ } from '~/locale';
import ListLabel from './label'; import ListLabel from './label';
import ListAssignee from './assignee'; import ListAssignee from './assignee';
import { isEE, urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone'; import ListMilestone from './milestone';
@ -26,6 +26,12 @@ const TYPES = {
isExpandable: false, isExpandable: false,
isBlank: true, isBlank: true,
}, },
default: {
// includes label, assignee, and milestone lists
isPreset: false,
isExpandable: true,
isBlank: false,
},
}; };
class List { class List {
@ -52,7 +58,7 @@ class List {
} else if (obj.user) { } else if (obj.user) {
this.assignee = new ListAssignee(obj.user); this.assignee = new ListAssignee(obj.user);
this.title = this.assignee.name; this.title = this.assignee.name;
} else if (isEE && obj.milestone) { } else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title; this.title = this.milestone.title;
} }
@ -79,7 +85,7 @@ class List {
entityType = 'label_id'; entityType = 'label_id';
} else if (this.assignee) { } else if (this.assignee) {
entityType = 'assignee_id'; entityType = 'assignee_id';
} else if (isEE && this.milestone) { } else if (IS_EE && this.milestone) {
entityType = 'milestone_id'; entityType = 'milestone_id';
} }
@ -199,7 +205,7 @@ class List {
issue.addAssignee(this.assignee); issue.addAssignee(this.assignee);
} }
if (isEE && this.milestone) { if (IS_EE && this.milestone) {
if (listFrom && listFrom.type === 'milestone') { if (listFrom && listFrom.type === 'milestone') {
issue.removeMilestone(listFrom.milestone); issue.removeMilestone(listFrom.milestone);
} }
@ -249,7 +255,7 @@ class List {
} }
getTypeInfo(type) { getTypeInfo(type) {
return TYPES[type] || {}; return TYPES[type] || TYPES.default;
} }
onNewIssueResponse(issue, data) { onNewIssueResponse(issue, data) {

View file

@ -1,11 +1,9 @@
import { isEE } from '~/lib/utils/common_utils';
export default class ListMilestone { export default class ListMilestone {
constructor(obj) { constructor(obj) {
this.id = obj.id; this.id = obj.id;
this.title = obj.title; this.title = obj.title;
if (isEE) { if (IS_EE) {
this.path = obj.path; this.path = obj.path;
this.state = obj.state; this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl; this.webUrl = obj.web_url || obj.webUrl;

View file

@ -0,0 +1,35 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
export default () => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
el: boardsSwitcherElement,
components: {
BoardsSelector,
},
data() {
const { dataset } = boardsSwitcherElement;
const boardsSelectorProps = {
...dataset,
currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
projectId: Number(dataset.projectId),
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
};
return { boardsSelectorProps };
},
render(createElement) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
},
});
};

View file

@ -1,106 +1,82 @@
import axios from '../../lib/utils/axios_utils'; /* eslint-disable class-methods-use-this */
import { mergeUrlParams } from '../../lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
export default class BoardService { export default class BoardService {
constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
this.boardsEndpoint = boardsEndpoint;
this.boardId = boardId;
this.listsEndpoint = listsEndpoint;
this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.bulkUpdatePath = bulkUpdatePath;
this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`;
}
generateBoardsPath(id) { generateBoardsPath(id) {
return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`; return boardsStore.generateBoardsPath(id);
} }
generateIssuesPath(id) { generateIssuesPath(id) {
return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`; return boardsStore.generateIssuesPath(id);
} }
static generateIssuePath(boardId, id) { static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ return boardsStore.generateIssuePath(boardId, id);
id ? `/${id}` : ''
}`;
} }
all() { all() {
return axios.get(this.listsEndpoint); return boardsStore.all();
} }
generateDefaultLists() { generateDefaultLists() {
return axios.post(this.listsEndpointGenerate, {}); return boardsStore.generateDefaultLists();
} }
createList(entityId, entityType) { createList(entityId, entityType) {
const list = { return boardsStore.createList(entityId, entityType);
[entityType]: entityId,
};
return axios.post(this.listsEndpoint, {
list,
});
} }
updateList(id, position) { updateList(id, position) {
return axios.put(`${this.listsEndpoint}/${id}`, { return boardsStore.updateList(id, position);
list: {
position,
},
});
} }
destroyList(id) { destroyList(id) {
return axios.delete(`${this.listsEndpoint}/${id}`); return boardsStore.destroyList(id);
} }
getIssuesForList(id, filter = {}) { getIssuesForList(id, filter = {}) {
const data = { id }; return boardsStore.getIssuesForList(id, filter);
Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
} }
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(BoardService.generateIssuePath(this.boardId, id), { return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
} }
newIssue(id, issue) { newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), { return boardsStore.newIssue(id, issue);
issue,
});
} }
getBacklog(data) { getBacklog(data) {
return axios.get( return boardsStore.getBacklog(data);
mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
);
} }
bulkUpdate(issueIds, extraData = {}) { bulkUpdate(issueIds, extraData = {}) {
const data = { return boardsStore.bulkUpdate(issueIds, extraData);
update: Object.assign(extraData, {
issuable_ids: issueIds.join(','),
}),
};
return axios.post(this.bulkUpdatePath, data);
} }
static getIssueInfo(endpoint) { static getIssueInfo(endpoint) {
return axios.get(endpoint); return boardsStore.getIssueInfo(endpoint);
} }
static toggleIssueSubscription(endpoint) { static toggleIssueSubscription(endpoint) {
return axios.post(endpoint); return boardsStore.toggleIssueSubscription(endpoint);
}
allBoards() {
return boardsStore.allBoards();
}
recentBoards() {
return boardsStore.recentBoards();
}
createBoard(board) {
return boardsStore.createBoard(board);
}
deleteBoard({ id }) {
return boardsStore.deleteBoard({ id });
} }
} }

View file

@ -8,10 +8,15 @@ import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
timeTracking: {
limitToHours: false,
},
scopedLabels: { scopedLabels: {
helpLink: '', helpLink: '',
enabled: false, enabled: false,
@ -25,6 +30,7 @@ const boardsStore = {
}, },
currentPage: '', currentPage: '',
reload: false, reload: false,
endpoints: {},
}, },
detail: { detail: {
issue: {}, issue: {},
@ -33,6 +39,19 @@ const boardsStore = {
issue: {}, issue: {},
list: {}, list: {},
}, },
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = {
boardsEndpoint,
boardId,
listsEndpoint,
listsEndpointGenerate,
bulkUpdatePath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
create() { create() {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
@ -222,6 +241,143 @@ const boardsStore = {
setIssueDetail(issueDetail) { setIssueDetail(issueDetail) {
this.detail.issue = issueDetail; this.detail.issue = issueDetail;
}, },
setTimeTrackingLimitToHours(limitToHours) {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
generateBoardsPath(id) {
return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
},
generateIssuesPath(id) {
return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
},
generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
},
all() {
return axios.get(this.state.endpoints.listsEndpoint);
},
generateDefaultLists() {
return axios.post(this.state.endpoints.listsEndpointGenerate, {});
},
createList(entityId, entityType) {
const list = {
[entityType]: entityId,
};
return axios.post(this.state.endpoints.listsEndpoint, {
list,
});
},
updateList(id, position) {
return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
list: {
position,
},
});
},
destroyList(id) {
return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
},
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
},
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
},
newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), {
issue,
});
},
getBacklog(data) {
return axios.get(
mergeUrlParams(
data,
`${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
),
);
},
bulkUpdate(issueIds, extraData = {}) {
const data = {
update: Object.assign(extraData, {
issuable_ids: issueIds.join(','),
}),
};
return axios.post(this.state.endpoints.bulkUpdatePath, data);
},
getIssueInfo(endpoint) {
return axios.get(endpoint);
},
toggleIssueSubscription(endpoint) {
return axios.post(endpoint);
},
allBoards() {
return axios.get(this.generateBoardsPath());
},
recentBoards() {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
createBoard(board) {
const boardPayload = { ...board };
boardPayload.label_ids = (board.labels || []).map(b => b.id);
if (boardPayload.label_ids.length === 0) {
boardPayload.label_ids = [''];
}
if (boardPayload.assignee) {
boardPayload.assignee_id = boardPayload.assignee.id;
}
if (boardPayload.milestone) {
boardPayload.milestone_id = boardPayload.milestone.id;
}
if (boardPayload.id) {
return axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload });
}
return axios.post(this.generateBoardsPath(), { board: boardPayload });
},
deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id));
},
setCurrentBoard(board) {
this.state.currentBoard = board;
},
}; };
BoardsStoreEE.initEESpecific(boardsStore); BoardsStoreEE.initEESpecific(boardsStore);

View file

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

View file

@ -0,0 +1,72 @@
<script>
import { sprintf, __ } from '~/locale';
import GraphBar from './graph_bar.vue';
import { MAX_COMMIT_COUNT } from '../constants';
export default {
components: {
GraphBar,
},
props: {
defaultBranch: {
type: String,
required: true,
},
distance: {
type: Number,
required: false,
default: null,
},
aheadCount: {
type: Number,
required: true,
},
behindCount: {
type: Number,
required: true,
},
maxCommits: {
type: Number,
required: true,
},
},
computed: {
title() {
if (this.distance) {
return sprintf(
__('More than %{number_commits_distance} commits different with %{default_branch}'),
{
number_commits_distance:
this.distance >= MAX_COMMIT_COUNT ? `${MAX_COMMIT_COUNT - 1}+` : this.distance,
default_branch: this.defaultBranch,
},
);
}
return sprintf(
__(
'%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead',
),
{
number_commits_behind: this.behindCount,
number_commits_ahead: this.aheadCount,
default_branch: this.defaultBranch,
},
);
},
},
};
</script>
<template>
<div :title="title" class="divergence-graph px-2 d-none d-md-block">
<template v-if="distance">
<graph-bar :count="distance" :max-commits="maxCommits" position="full" />
</template>
<template v-else>
<graph-bar :count="behindCount" :max-commits="maxCommits" position="left" />
<div class="graph-separator pull-left mt-1"></div>
<graph-bar :count="aheadCount" :max-commits="maxCommits" position="right" />
</template>
</div>
</template>

View file

@ -0,0 +1,69 @@
<script>
import { SIDES, MAX_COMMIT_COUNT } from '../constants';
export default {
props: {
position: {
type: String,
required: true,
},
count: {
type: Number,
required: true,
},
maxCommits: {
type: Number,
required: true,
},
},
computed: {
label() {
if (this.count >= MAX_COMMIT_COUNT) {
return `${MAX_COMMIT_COUNT - 1}+`;
}
return this.count;
},
barGraphWidthFactor() {
return this.maxCommits > 0 ? 100 / this.maxCommits : 0;
},
style() {
return {
width: `${this.count * this.barGraphWidthFactor}%`,
};
},
isFullWidth() {
return this.position === SIDES.full;
},
isLeftSide() {
return this.position === SIDES.left;
},
roundedClass() {
if (this.isFullWidth) return 'rounded';
return `rounded-${this.position}`;
},
textAlignmentClass() {
if (this.isFullWidth) return 'text-center';
return `text-${this.isLeftSide ? SIDES.right : SIDES.left}`;
},
positionSideClass() {
return `position-${this.isLeftSide ? SIDES.right : SIDES.left}-0`;
},
},
};
</script>
<template>
<div :class="{ full: isFullWidth }" class="position-relative pull-left pt-1 graph-side h-100">
<div
:style="style"
:class="[roundedClass, positionSideClass]"
class="position-absolute bar js-graph-bar"
></div>
<span :class="textAlignmentClass" class="d-block pt-1 pr-1 count js-graph-count">
{{ label }}
</span>
</div>
</template>

View file

@ -0,0 +1,6 @@
export const SIDES = {
full: 'full',
left: 'left',
right: 'right',
};
export const MAX_COMMIT_COUNT = 1000;

View file

@ -0,0 +1,51 @@
import Vue from 'vue';
import { __ } from '../locale';
import createFlash from '../flash';
import axios from '../lib/utils/axios_utils';
import DivergenceGraph from './components/divergence_graph.vue';
export function createGraphVueApp(el, data, maxCommits) {
return new Vue({
el,
render(h) {
return h(DivergenceGraph, {
props: {
defaultBranch: 'master',
distance: data.distance ? parseInt(data.distance, 10) : null,
aheadCount: parseInt(data.ahead, 10),
behindCount: parseInt(data.behind, 10),
maxCommits,
},
});
},
});
}
export default endpoint => {
const names = [...document.querySelectorAll('.js-branch-item')].map(
({ dataset }) => dataset.name,
);
return axios
.get(endpoint, {
params: { names },
})
.then(({ data }) => {
const maxCommits = Object.entries(data).reduce((acc, [, val]) => {
const max = Math.max(...Object.values(val));
return max > acc ? max : acc;
}, 100);
Object.entries(data).forEach(([branchName, val]) => {
const el = document.querySelector(
`[data-name="${branchName}"] .js-branch-divergence-graph`,
);
if (!el) return;
createGraphVueApp(el, val, maxCommits);
});
})
.catch(() =>
createFlash(__('Error fetching diverging counts for branches. Please try again.')),
);
};

View file

@ -207,7 +207,7 @@ export default {
return __('Updating'); return __('Updating');
} }
return __('Updated'); return this.updateSuccessful ? __('Updated to') : __('Updated');
}, },
updateFailureDescription() { updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
@ -331,8 +331,6 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-update-details" class="form-text text-muted label p-0 js-cluster-application-update-details"
> >
{{ versionLabel }} {{ versionLabel }}
<span v-if="updateSuccessful">to</span>
<gl-link <gl-link
v-if="updateSuccessful" v-if="updateSuccessful"
:href="chartRepo" :href="chartRepo"

View file

@ -2,7 +2,7 @@
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants'; import { APPLICATION_STATUS } from '~/clusters/constants';
@ -32,7 +32,7 @@ export default {
return [UPDATING].includes(this.knative.status); return [UPDATING].includes(this.knative.status);
}, },
saveButtonLabel() { saveButtonLabel() {
return this.saving ? this.__('Saving') : this.__('Save changes'); return this.saving ? __('Saving') : __('Save changes');
}, },
knativeInstalled() { knativeInstalled() {
return this.knative.installed; return this.knative.installed;
@ -122,9 +122,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
) )
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
{{ __('More information') }} __('More information')
</a> }}</a>
</p> </p>
<p <p

View file

@ -1,6 +1,7 @@
<script> <script>
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants'; import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
@ -22,7 +23,7 @@ export default {
return this.status === UNINSTALLING; return this.status === UNINSTALLING;
}, },
label() { label() {
return this.loading ? this.__('Uninstalling') : this.__('Uninstall'); return this.loading ? __('Uninstalling') : __('Uninstall');
}, },
}, },
}; };

View file

@ -14,7 +14,9 @@ const CUSTOM_APP_WARNING_TEXT = {
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'), [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'), [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'), [KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'),
[JUPYTER]: '', [JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
}; };
export default { export default {

View file

@ -80,6 +80,9 @@ const applicationStateMachine = {
installFailed: false, installFailed: false,
}, },
}, },
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
// This is possible in artificial environments for E2E testing // This is possible in artificial environments for E2E testing
[INSTALLED]: { [INSTALLED]: {
target: INSTALLED, target: INSTALLED,
@ -108,6 +111,9 @@ const applicationStateMachine = {
updateSuccessful: false, updateSuccessful: false,
}, },
}, },
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[UNINSTALL_EVENT]: { [UNINSTALL_EVENT]: {
target: UNINSTALLING, target: UNINSTALLING,
effects: { effects: {

View file

@ -4,3 +4,6 @@ import './jquery';
import './bootstrap'; import './bootstrap';
import './vue'; import './vue';
import '../lib/utils/axios_utils'; import '../lib/utils/axios_utils';
import { openUserCountsBroadcast } from './nav/user_merge_requests';
openUserCountsBroadcast();

View file

@ -0,0 +1,67 @@
import Api from '~/api';
let channel;
function broadcastCount(newCount) {
if (!channel) {
return;
}
channel.postMessage(newCount);
}
function updateUserMergeRequestCounts(newCount) {
const mergeRequestsCountEl = document.querySelector('.merge-requests-count');
mergeRequestsCountEl.textContent = newCount.toLocaleString();
mergeRequestsCountEl.classList.toggle('hidden', Number(newCount) === 0);
}
/**
* Refresh user counts (and broadcast if open)
*/
export function refreshUserMergeRequestCounts() {
return Api.userCounts()
.then(({ data }) => {
const count = data.merge_requests;
updateUserMergeRequestCounts(count);
broadcastCount(count);
})
.catch(ex => {
console.error(ex); // eslint-disable-line no-console
});
}
/**
* Close the broadcast channel for user counts
*/
export function closeUserCountsBroadcast() {
if (!channel) {
return;
}
channel.close();
channel = null;
}
/**
* Open the broadcast channel for user counts, adds user id so we only update
*
* **Please note:**
* Not supported in all browsers, but not polyfilling for now
* to keep bundle size small and
* no special functionality lost except cross tab notifications
*/
export function openUserCountsBroadcast() {
closeUserCountsBroadcast();
if (window.BroadcastChannel) {
const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id;
if (currentUserId) {
channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`);
channel.onmessage = ev => {
updateUserMergeRequestCounts(ev.data);
};
}
}
}

View file

@ -12,6 +12,7 @@ import 'core-js/es/promise/finally';
import 'core-js/es/string/code-point-at'; import 'core-js/es/string/code-point-at';
import 'core-js/es/string/from-code-point'; import 'core-js/es/string/from-code-point';
import 'core-js/es/string/includes'; import 'core-js/es/string/includes';
import 'core-js/es/string/starts-with';
import 'core-js/es/symbol'; import 'core-js/es/symbol';
import 'core-js/es/map'; import 'core-js/es/map';
import 'core-js/es/weak-map'; import 'core-js/es/weak-map';

View file

@ -0,0 +1,58 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
Icon,
},
props: {
projects: {
type: Array,
required: true,
},
selectedProject: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
dropdownText() {
if (Object.keys(this.selectedProject).length) {
return this.selectedProject.name;
}
return __('Select private project');
},
},
methods: {
selectProject(project) {
this.$emit('click', project);
},
},
};
</script>
<template>
<gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
<template slot="button-content">
<span class="str-truncated-100 mr-2">
<icon name="lock" />
{{ dropdownText }}
</span>
<icon name="chevron-down" class="ml-auto" />
</template>
<gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)">
<icon
name="mobile-issue-close"
:class="{ icon: project.id !== selectedProject.id }"
class="js-active-project-check"
/>
<span class="ml-1">{{ project.name }}</span>
</gl-dropdown-item>
</gl-dropdown>
</template>

View file

@ -0,0 +1,140 @@
<script>
import { GlLink } from '@gitlab/ui';
import { __, sprintf } from '../../locale';
import createFlash from '../../flash';
import Api from '../../api';
import state from '../state';
import Dropdown from './dropdown.vue';
export default {
components: {
GlLink,
Dropdown,
},
props: {
namespacePath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
newForkPath: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
data() {
return {
projects: [],
};
},
computed: {
selectedProject() {
return state.selectedProject;
},
noForkText() {
return sprintf(
__(
"To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.",
),
{ link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
false,
);
},
},
mounted() {
this.fetchProjects();
this.createBtn = document.querySelector('.js-create-target');
this.warningText = document.querySelector('.js-exposed-info-warning');
},
methods: {
selectProject(project) {
if (project) {
Object.assign(state, {
selectedProject: project,
});
if (project.namespaceFullPath !== this.namespacePath) {
this.showWarning();
}
} else if (this.createBtn) {
this.createBtn.setAttribute('disabled', 'disabled');
}
},
normalizeProjectData(data) {
return data.map(p => ({
id: p.id,
name: p.name_with_namespace,
pathWithNamespace: p.path_with_namespace,
namespaceFullpath: p.namespace.full_path,
}));
},
fetchProjects() {
Api.projectForks(this.projectPath, {
with_merge_requests_enabled: true,
min_access_level: 30,
visibility: 'private',
})
.then(({ data }) => {
this.projects = this.normalizeProjectData(data);
this.selectProject(this.projects[0]);
})
.catch(e => {
createFlash(__('Error fetching forked projects. Please try again.'));
throw e;
});
},
showWarning() {
if (this.warningText) {
this.warningText.classList.remove('hidden');
}
if (this.createBtn) {
this.createBtn.classList.add('btn-warning');
this.createBtn.classList.remove('btn-success');
}
},
},
};
</script>
<template>
<div class="confidential-merge-request-fork-group form-group">
<label>{{ __('Project') }}</label>
<div>
<dropdown
v-if="projects.length"
:projects="projects"
:selected-project="selectedProject"
@click="selectProject"
/>
<p class="text-muted mt-1 mb-0">
<template v-if="projects.length">
{{
__(
"To protect this issue's confidentiality, a private fork of this project was selected.",
)
}}
</template>
<template v-else>
{{ __('No forks available to you.') }}<br />
<span v-html="noForkText"></span>
</template>
<gl-link
:href="helpPagePath"
class="w-auto p-0 d-inline-block text-primary bg-transparent"
target="_blank"
>
<span class="sr-only">{{ __('Read more') }}</span>
<i class="fa fa-question-circle" aria-hidden="true"></i>
</gl-link>
</p>
</div>
</div>
</template>

View file

@ -0,0 +1,30 @@
import Vue from 'vue';
import { parseBoolean } from '../lib/utils/common_utils';
import ProjectFormGroup from './components/project_form_group.vue';
import state from './state';
export function isConfidentialIssue() {
return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential);
}
export function canCreateConfidentialMergeRequest() {
return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0;
}
export function init() {
const el = document.getElementById('js-forked-project');
return new Vue({
el,
render(h) {
return h(ProjectFormGroup, {
props: {
namespacePath: el.dataset.namespacePath,
projectPath: el.dataset.projectPath,
newForkPath: el.dataset.newForkPath,
helpPagePath: el.dataset.helpPagePath,
},
});
},
});
}

View file

@ -0,0 +1,5 @@
import Vue from 'vue';
export default Vue.observable({
selectedProject: {},
});

View file

@ -5,6 +5,12 @@ import Flash from './flash';
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
import { __, sprintf } from './locale'; import { __, sprintf } from './locale';
import {
init as initConfidentialMergeRequest,
isConfidentialIssue,
canCreateConfidentialMergeRequest,
} from './confidential_merge_request';
import confidentialMergeRequestState from './confidential_merge_request/state';
// Todo: Remove this when fixing issue in input_setter plugin // Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter); const InputSetter = Object.assign({}, ISetter);
@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter);
const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch'; const CREATE_BRANCH = 'create-branch';
function createEndpoint(projectPath, endpoint) {
if (canCreateConfidentialMergeRequest()) {
return endpoint.replace(
projectPath,
confidentialMergeRequestState.selectedProject.pathWithNamespace,
);
}
return endpoint;
}
export default class CreateMergeRequestDropdown { export default class CreateMergeRequestDropdown {
constructor(wrapperEl) { constructor(wrapperEl) {
this.wrapperEl = wrapperEl; this.wrapperEl = wrapperEl;
@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true; this.refIsValid = true;
this.refsPath = this.wrapperEl.dataset.refsPath; this.refsPath = this.wrapperEl.dataset.refsPath;
this.suggestedRef = this.refInput.value; this.suggestedRef = this.refInput.value;
this.projectPath = this.wrapperEl.dataset.projectPath;
this.projectId = this.wrapperEl.dataset.projectId;
// These regexps are used to replace // These regexps are used to replace
// a backend generated new branch name and its source (ref) // a backend generated new branch name and its source (ref)
@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown {
}; };
this.init(); this.init();
if (isConfidentialIssue()) {
this.createMergeRequestButton.setAttribute(
'data-dropdown-trigger',
'#create-merge-request-dropdown',
);
initConfidentialMergeRequest();
}
} }
available() { available() {
@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = true; this.isCreatingBranch = true;
return axios return axios
.post(this.createBranchPath) .post(createEndpoint(this.projectPath, this.createBranchPath), {
confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
})
.then(({ data }) => { .then(({ data }) => {
this.branchCreated = true; this.branchCreated = true;
window.location.href = data.url; window.location.href = data.url;
@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown {
this.isCreatingMergeRequest = true; this.isCreatingMergeRequest = true;
return axios return axios
.post(this.createMrPath) .post(this.createMrPath, {
target_project_id: canCreateConfidentialMergeRequest()
? confidentialMergeRequestState.selectedProject.id
: null,
})
.then(({ data }) => { .then(({ data }) => {
this.mergeRequestCreated = true; this.mergeRequestCreated = true;
window.location.href = data.url; window.location.href = data.url;
@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown {
} }
enable() { enable() {
if (isConfidentialIssue() && !canCreateConfidentialMergeRequest()) return;
this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled'); this.createMergeRequestButton.removeAttribute('disabled');
@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown {
if (!ref) return false; if (!ref) return false;
return axios return axios
.get(`${this.refsPath}${encodeURIComponent(ref)}`) .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
.then(({ data }) => { .then(({ data }) => {
const branches = data[Object.keys(data)[0]]; const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]]; const tags = data[Object.keys(data)[1]];
@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown {
let xhr = null; let xhr = null;
event.preventDefault(); event.preventDefault();
if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) {
this.droplab.hooks.forEach(hook => hook.list.toggle());
return;
}
if (this.isBusy()) { if (this.isBusy()) {
return; return;
} }

View file

@ -17,6 +17,7 @@ Vue.use(Translate);
export default () => { export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
@ -33,7 +34,6 @@ export default () => {
'stage-production-component': stageComponent, 'stage-production-component': stageComponent,
}, },
data() { data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({ const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath, requestPath: cycleAnalyticsEl.dataset.requestPath,
}); });
@ -56,7 +56,13 @@ export default () => {
}, },
}, },
created() { created() {
this.fetchCycleAnalyticsData(); // Conditional check placed here to prevent this method from being called on the
// new Cycle Analytics page (i.e. the new page will be initialized blank and only
// after a group is selected the cycle analyitcs data will be fetched). Once the
// old (current) page has been removed this entire created method as well as the
// variable itself can be completely removed.
// Follow up issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/64490
if (cycleAnalyticsEl.dataset.requestPath) this.fetchCycleAnalyticsData();
}, },
methods: { methods: {
handleError() { handleError() {

View file

@ -32,15 +32,15 @@ const CommentAndResolveBtn = Vue.extend({
buttonText: function() { buttonText: function() {
if (this.isDiscussionResolved) { if (this.isDiscussionResolved) {
if (this.textareaIsEmpty) { if (this.textareaIsEmpty) {
return __('Unresolve discussion'); return __('Unresolve thread');
} else { } else {
return __('Comment & unresolve discussion'); return __('Comment & unresolve thread');
} }
} else { } else {
if (this.textareaIsEmpty) { if (this.textareaIsEmpty) {
return __('Resolve discussion'); return __('Resolve thread');
} else { } else {
return __('Comment & resolve discussion'); return __('Comment & resolve thread');
} }
} }
}, },

View file

@ -49,6 +49,8 @@ export default {
return this.author.id ? this.author.id : ''; return this.author.id ? this.author.id : '';
}, },
authorUrl() { authorUrl() {
// TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings
// name: 'mailto:' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
return this.author.web_url || `mailto:${this.commit.author_email}`; return this.author.web_url || `mailto:${this.commit.author_email}`;
}, },
authorAvatar() { authorAvatar() {
@ -80,7 +82,7 @@ export default {
v-html="commit.title_html" v-html="commit.title_html"
></a> ></a>
<span class="commit-row-message d-block d-sm-none"> &middot; {{ commit.short_id }} </span> <span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<button <button
v-if="commit.description_html" v-if="commit.description_html"

View file

@ -1,6 +1,6 @@
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { n__, __ } from '~/locale'; import { n__, __, sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
@ -54,11 +54,7 @@ export default {
}, },
methods: { methods: {
commitsText(version) { commitsText(version) {
return n__( return n__(`%d commit,`, `%d commits,`, version.commits_count);
`${version.commits_count} commit,`,
`${version.commits_count} commits,`,
version.commits_count,
);
}, },
href(version) { href(version) {
if (this.isBase(version)) { if (this.isBase(version)) {
@ -76,7 +72,7 @@ export default {
if (this.targetBranch && (this.isBase(version) || !version)) { if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName; return this.targetBranch.branchName;
} }
return `version ${version.version_index}`; return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
}, },
isActive(version) { isActive(version) {
if (!version) { if (!version) {
@ -125,9 +121,9 @@ export default {
<div> <div>
<strong> <strong>
{{ versionName(version) }} {{ versionName(version) }}
<template v-if="isBase(version)"> <template v-if="isBase(version)">{{
(base) s__('DiffsCompareBaseBranch|(base)')
</template> }}</template>
</strong> </strong>
</div> </div>
<div> <div>

View file

@ -0,0 +1,55 @@
<script>
import { mapGetters } from 'vuex';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
UserAvatarLink,
},
props: {
hasForm: {
type: Boolean,
required: false,
default: false,
},
renderReplyPlaceholder: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters({
currentUser: 'getUserData',
userCanReply: 'userCanReply',
}),
},
};
</script>
<template>
<div class="discussion-reply-holder d-flex clearfix">
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
<user-avatar-link
:link-href="currentUser.path"
:img-src="currentUser.avatar_url"
:img-alt="currentUser.name"
:img-size="40"
class="d-none d-sm-block"
/>
<reply-placeholder
class="qa-discussion-reply"
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
/>
</template>
</template>
<note-signed-out-widget v-else />
</div>
</template>

View file

@ -80,7 +80,6 @@ export default {
v-show="isExpanded(discussion)" v-show="isExpanded(discussion)"
:discussion="discussion" :discussion="discussion"
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"

View file

@ -67,6 +67,18 @@ export default {
errorMessage() { errorMessage() {
return this.file.viewer.error_message; return this.file.viewer.error_message;
}, },
forkMessage() {
return sprintf(
__(
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
),
{
tag_start: '<span class="js-file-fork-suggestion-section-action">',
tag_end: '</span>',
},
false,
);
},
}, },
watch: { watch: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) { isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
@ -150,22 +162,18 @@ export default {
/> />
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note"> <span class="file-fork-suggestion-note" v-html="forkMessage"></span>
You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span> files
in this project directly. Please fork this project, make your changes there, and submit a
merge request.
</span>
<a <a
:href="file.fork_path" :href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
>Fork</a >{{ __('Fork') }}</a
> >
<button <button
class="js-cancel-fork-suggestion-button btn btn-grouped" class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button" type="button"
@click="hideForkMessage" @click="hideForkMessage"
> >
Cancel {{ __('Cancel') }}
</button> </button>
</div> </div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />

View file

@ -151,7 +151,11 @@ export default {
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false); stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
}, },
methods: { methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']), ...mapActions('diffs', [
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
handleToggleFile(e, checkTarget) { handleToggleFile(e, checkTarget) {
if ( if (
!checkTarget || !checkTarget ||
@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage'); this.$emit('showForkMessage');
}, },
handleToggleDiscussions() { handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile); this.toggleFileDiscussionWrappers(this.diffFile);
}, },
handleFileNameClick(e) { handleFileNameClick(e) {
const isLinkToOtherPage = const isLinkToOtherPage =

View file

@ -1,5 +1,4 @@
<script> <script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility'; import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@ -19,11 +18,13 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
discussionsExpanded: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
discussionsExpanded() {
return this.discussions.every(discussion => discussion.expanded);
},
allDiscussions() { allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []); return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
}, },
@ -45,26 +46,14 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleDiscussion']),
getTooltipText(noteData) { getTooltipText(noteData) {
let { note } = noteData; let { note } = noteData;
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) { if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP); note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
} }
return `${noteData.author.name}: ${note}`; return `${noteData.author.name}: ${note}`;
}, },
toggleDiscussions() {
const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
forceExpanded,
});
});
},
}, },
}; };
</script> </script>
@ -74,9 +63,9 @@ export default {
<button <button
v-if="discussionsExpanded" v-if="discussionsExpanded"
type="button" type="button"
aria-label="Show comments" :aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button" class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions" @click="$emit('toggleLineDiscussions')"
> >
<icon :size="12" name="collapse" /> <icon :size="12" name="collapse" />
</button> </button>
@ -87,7 +76,7 @@ export default {
:img-src="note.author.avatar_url" :img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)" :tooltip-text="getTooltipText(note)"
class="diff-comment-avatar js-diff-comment-avatar" class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions" @click.native="$emit('toggleLineDiscussions')"
/> />
<span <span
v-if="moreText" v-if="moreText"
@ -97,7 +86,7 @@ export default {
data-container="body" data-container="body"
data-placement="top" data-placement="top"
role="button" role="button"
@click="toggleDiscussions" @click="$emit('toggleLineDiscussions')"
>+{{ moreCount }}</span >+{{ moreCount }}</span
> >
</template> </template>

View file

@ -105,7 +105,13 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), ...mapActions('diffs', [
'loadMoreLines',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
'toggleLineDiscussionWrappers',
]),
handleCommentButton() { handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
}, },
@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(lineCode)"
> >
</a> </a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> <diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
"
/>
</template> </template>
</div> </div>
</template> </template>

View file

@ -1,11 +1,14 @@
<script> <script>
import diffDiscussions from './diff_discussions.vue'; import { mapActions } from 'vuex';
import diffLineNoteForm from './diff_line_note_form.vue'; import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default { export default {
components: { components: {
diffDiscussions, DiffDiscussions,
diffLineNoteForm, DiffLineNoteForm,
DiffDiscussionReply,
}, },
props: { props: {
line: { line: {
@ -21,6 +24,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
hasDraft: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
className() { className() {
@ -32,10 +40,12 @@ export default {
if (!this.line.discussions || !this.line.discussions.length) { if (!this.line.discussions || !this.line.discussions.length) {
return false; return false;
} }
return this.line.discussionsExpanded;
return this.line.discussions.every(discussion => discussion.expanded);
}, },
}, },
methods: {
...mapActions('diffs', ['showCommentForm']),
},
}; };
</script> </script>
@ -49,13 +59,23 @@ export default {
:discussions="line.discussions" :discussions="line.discussions"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<diff-line-note-form <diff-discussion-reply
v-if="line.hasForm" v-if="!hasDraft"
:diff-file-hash="diffFileHash" :has-form="line.hasForm"
:line="line" :render-reply-placeholder="Boolean(line.discussions.length)"
:note-target-line="line" @showNewDiscussionForm="
:help-page-path="helpPagePath" showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
/> "
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</template>
</diff-discussion-reply>
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -57,6 +57,7 @@ export default {
:diff-file-hash="diffFile.file_hash" :diff-file-hash="diffFile.file_hash"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
/> />
<inline-draft-comment-row <inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, line)" v-if="shouldRenderDraftRow(diffFile.file_hash, line)"

View file

@ -1,11 +1,14 @@
<script> <script>
import diffDiscussions from './diff_discussions.vue'; import { mapActions } from 'vuex';
import diffLineNoteForm from './diff_line_note_form.vue'; import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default { export default {
components: { components: {
diffDiscussions, DiffDiscussions,
diffLineNoteForm, DiffLineNoteForm,
DiffDiscussionReply,
}, },
props: { props: {
line: { line: {
@ -25,28 +28,44 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
hasDraftLeft: {
type: Boolean,
required: false,
default: false,
},
hasDraftRight: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded) ? this.line.left.discussionsExpanded
: false; : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded) ? this.line.right.discussionsExpanded
: false; : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft; return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return ( return (
this.line.right && this.line.right &&
this.line.right.discussions && this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight && this.hasExpandedDiscussionOnRight &&
this.line.right.type this.line.right.type
); );
@ -81,6 +100,22 @@ export default {
return hasCommentFormOnLeft || hasCommentFormOnRight; return hasCommentFormOnLeft || hasCommentFormOnRight;
}, },
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash });
},
}, },
}; };
</script> </script>
@ -90,37 +125,51 @@ export default {
<td class="notes-content parallel old" colspan="2"> <td class="notes-content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content"> <div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions <diff-discussions
v-if="line.left.discussions.length"
:discussions="line.left.discussions" :discussions="line.left.discussions"
:line="line.left" :line="line.left"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
</div> </div>
<diff-line-note-form <diff-discussion-reply
v-if="showLeftSideCommentForm" v-if="!hasDraftLeft"
:diff-file-hash="diffFileHash" :has-form="showLeftSideCommentForm"
:line="line.left" :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
:note-target-line="line.left" @showNewDiscussionForm="showNewDiscussionForm"
:help-page-path="helpPagePath" >
line-position="left" <template #form>
/> <diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td> </td>
<td class="notes-content parallel new" colspan="2"> <td class="notes-content parallel new" colspan="2">
<div v-if="shouldRenderDiscussionsOnRight" class="content"> <div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions <diff-discussions
v-if="line.right.discussions.length"
:discussions="line.right.discussions" :discussions="line.right.discussions"
:line="line.right" :line="line.right"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
</div> </div>
<diff-line-note-form <diff-discussion-reply
v-if="showRightSideCommentForm" v-if="!hasDraftRight"
:diff-file-hash="diffFileHash" :has-form="showRightSideCommentForm"
:line="line.right" :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
:note-target-line="line.right" @showNewDiscussionForm="showNewDiscussionForm"
line-position="right" >
/> <template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td> </td>
</tr> </tr>
</template> </template>

View file

@ -58,6 +58,8 @@ export default {
:diff-file-hash="diffFile.file_hash" :diff-file-hash="diffFile.file_hash"
:line-index="index" :line-index="index"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:has-draft-left="hasParallelDraftLeft(diffFile.file_hash, line) || false"
:has-draft-right="hasParallelDraftRight(diffFile.file_hash, line) || false"
/> />
<parallel-draft-comment-row <parallel-draft-comment-row
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"

View file

@ -6,5 +6,7 @@ export default {
imageDiscussions() { imageDiscussions() {
return this.diffFile.discussions; return this.diffFile.discussions;
}, },
hasParallelDraftLeft: () => () => false,
hasParallelDraftRight: () => () => false,
}, },
}; };

View file

@ -12,6 +12,7 @@ import {
getNoteFormData, getNoteFormData,
convertExpandLines, convertExpandLines,
idleCallback, idleCallback,
allDiscussionWrappersExpanded,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions, discussions = rootState.notes.discussions,
) => { ) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions discussions
.filter(discussion => discussion.diff_discussion) .filter(discussion => discussion.diff_discussion)
@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion, discussion,
diffPositionByLineCode, diffPositionByLineCode,
hash,
}); });
}); });
@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id }); commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
}; };
export const toggleLineDiscussions = ({ commit }, options) => {
commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId); const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
}); });
}; };
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions;
if (diff.highlighted_diff_lines) {
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
}
if (diff.parallel_diff_lines) {
linesWithDiscussions = diff.parallel_diff_lines.filter(
line =>
(line.left && line.left.discussions.length) ||
(line.right && line.right.discussions.length),
);
}
if (linesWithDiscussions.length) {
linesWithDiscussions.forEach(line => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
});
});
}
};
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({ const postData = getNoteFormData({
commit: state.commit, commit: state.commit,
@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true }) return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true })) .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true })) .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
}; };

View file

@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER'; export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';

View file

@ -6,6 +6,7 @@ import {
addContextLines, addContextLines,
prepareDiffData, prepareDiffData,
isDiscussionApplicableToLine, isDiscussionApplicableToLine,
updateLineInFile,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
@ -109,7 +110,7 @@ export default {
})); }));
}, },
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state; const { latestDiff } = state;
const discussionLineCode = discussion.line_code; const discussionLineCode = discussion.line_code;
@ -130,13 +131,27 @@ export default {
: [], : [],
}); });
const setDiscussionsExpanded = line => {
const isLineNoteTargeted = line.discussions.some(
disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
);
return {
...line,
discussionsExpanded:
line.discussions && line.discussions.length
? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted
: false,
};
};
state.diffFiles = state.diffFiles.map(diffFile => { state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) { if (diffFile.file_hash === fileHash) {
const file = { ...diffFile }; const file = { ...diffFile };
if (file.highlighted_diff_lines) { if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
lineCheck(line) ? mapDiscussions(line) : line, setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
); );
} }
@ -148,8 +163,10 @@ export default {
if (left || right) { if (left || right) {
return { return {
...line, ...line,
left: line.left ? mapDiscussions(line.left) : null, left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null, right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
}; };
} }
@ -173,32 +190,11 @@ export default {
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) { if (selectedFile) {
if (selectedFile.parallel_diff_lines) { updateLineInFile(selectedFile, lineCode, line =>
const targetLine = selectedFile.parallel_diff_lines.find( Object.assign(line, {
line => discussions: line.discussions.filter(discussion => discussion.notes.length),
(line.left && line.left.line_code === lineCode) || }),
(line.right && line.right.line_code === lineCode), );
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.discussions && selectedFile.discussions.length) { if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter( selectedFile.discussions = selectedFile.discussions.filter(
@ -207,6 +203,15 @@ export default {
} }
} }
}, },
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, { discussionsExpanded: expanded }),
);
},
[types.TOGGLE_FOLDER_OPEN](state, path) { [types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened; state.treeEntries[path].opened = !state.treeEntries[path].opened;
}, },

View file

@ -454,3 +454,48 @@ export const convertExpandLines = ({
}; };
export const idleCallback = cb => requestIdleCallback(cb); export const idleCallback = cb => requestIdleCallback(cb);
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
updateFn(targetLine[side]);
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
updateFn(targetInlineLine);
}
}
};
export const allDiscussionWrappersExpanded = diff => {
const discussionsExpandedArray = [];
if (diff.parallel_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.left && line.left.discussions.length) {
discussionsExpandedArray.push(line.left.discussionsExpanded);
}
if (line.right && line.right.discussions.length) {
discussionsExpandedArray.push(line.right.discussionsExpanded);
}
});
} else if (diff.highlighted_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.discussions.length) {
discussionsExpandedArray.push(line.discussionsExpanded);
}
});
}
return discussionsExpandedArray.every(el => el);
};

View file

@ -57,6 +57,7 @@ export default {
:user-callouts-path="userCalloutsPath" :user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath" :lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath" :help-canary-deployments-path="helpCanaryDeploymentsPath"
:deploy-boards-help-path="deployBoardsHelpPath"
/> />
<table-pagination <table-pagination

View file

@ -1,5 +1,5 @@
<script> <script>
import { s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility'; import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
@ -28,7 +28,7 @@ export default {
}, },
computed: { computed: {
title() { title() {
return 'Deploy to...'; return __('Deploy to...');
}, },
}, },
methods: { methods: {
@ -80,7 +80,8 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
> >
<span> <span>
<icon name="play" /> <icon name="chevron-down" /> <icon name="play" />
<icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" /> <gl-loading-icon v-if="isLoading" />
</span> </span>
</button> </button>
@ -94,9 +95,10 @@ export default {
class="js-manual-action-link no-btn btn d-flex align-items-center" class="js-manual-action-link no-btn btn d-flex align-items-center"
@click="onClickAction(action)" @click="onClickAction(action)"
> >
<span class="flex-fill"> {{ action.name }} </span> <span class="flex-fill">{{ action.name }}</span>
<span v-if="action.scheduledAt" class="text-secondary"> <span v-if="action.scheduledAt" class="text-secondary">
<icon name="clock" /> {{ remainingTime(action) }} <icon name="clock" />
{{ remainingTime(action) }}
</span> </span>
</button> </button>
</li> </li>

View file

@ -1,4 +1,5 @@
<script> <script>
import { __, sprintf } from '~/locale';
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
@ -14,7 +15,6 @@ import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue'; import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { CLUSTER_TYPE } from '~/clusters/constants';
/** /**
* Environment Item Component * Environment Item Component
@ -79,15 +79,6 @@ export default {
return this.model && this.model.is_protected; return this.model && this.model.is_protected;
}, },
/**
* Hide group cluster features which are not currently implemented.
*
* @returns {Boolean}
*/
disableGroupClusterFeatures() {
return this.model && this.model.cluster_type === CLUSTER_TYPE.GROUP;
},
/** /**
* Returns whether the environment can be stopped. * Returns whether the environment can be stopped.
* *
@ -172,7 +163,9 @@ export default {
this.model.last_deployment.user && this.model.last_deployment.user &&
this.model.last_deployment.user.username this.model.last_deployment.user.username
) { ) {
return `${this.model.last_deployment.user.username}'s avatar'`; return sprintf(__("%{username}'s avatar"), {
username: this.model.last_deployment.user.username,
});
} }
return ''; return '';
}, },
@ -293,6 +286,9 @@ export default {
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
isLastDeployment() { isLastDeployment() {
// TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings
// name: 'last?' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// Vue i18n ESLint rules issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/63560
return this.model && this.model.last_deployment && this.model.last_deployment['last?']; return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
}, },
@ -575,7 +571,6 @@ export default {
<terminal-button-component <terminal-button-component
v-if="model && model.terminal_path" v-if="model && model.terminal_path"
:terminal-path="model.terminal_path" :terminal-path="model.terminal_path"
:disabled="disableGroupClusterFeatures"
/> />
<rollback-component <rollback-component

View file

@ -1,4 +1,5 @@
<script> <script>
import { __ } from '~/locale';
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
@ -21,7 +22,7 @@ export default {
}, },
computed: { computed: {
title() { title() {
return 'Monitoring'; return __('Monitoring');
}, },
}, },
}; };

View file

@ -5,6 +5,7 @@
*/ */
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
export default { export default {
components: { components: {
@ -27,7 +28,7 @@ export default {
}, },
computed: { computed: {
title() { title() {
return 'Terminal'; return __('Terminal');
}, },
}, },
}; };

View file

@ -43,6 +43,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
deployBoardsHelpPath: {
type: String,
required: false,
default: '',
},
}, },
created() { created() {
@ -112,6 +117,7 @@ export default {
:user-callouts-path="userCalloutsPath" :user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath" :lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath" :help-canary-deployments-path="helpCanaryDeploymentsPath"
:deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage" @onChangePage="onChangePage"
> >
<empty-state <empty-state

View file

@ -22,6 +22,11 @@ export default {
required: true, required: true,
default: () => [], default: () => [],
}, },
deployBoardsHelpPath: {
type: String,
required: false,
default: '',
},
canReadEnvironment: { canReadEnvironment: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -106,8 +111,10 @@ export default {
<div class="deploy-board-container"> <div class="deploy-board-container">
<deploy-board <deploy-board
:deploy-board-data="model.deployBoardData" :deploy-board-data="model.deployBoardData"
:deploy-boards-help-path="deployBoardsHelpPath"
:is-loading="model.isLoadingDeployBoard" :is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard" :is-empty="model.isEmptyDeployBoard"
:has-legacy-app-label="model.hasLegacyAppLabel"
:logs-path="model.logs_path" :logs-path="model.logs_path"
/> />
</div> </div>

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