Merge tag 'debian/12.1.13-2' into buster-fasttrack

gitlab Debian release 12.1.13-2
This commit is contained in:
Pirate Praveen 2019-10-06 23:51:24 +05:30
commit 8563fafbe4
6809 changed files with 98427 additions and 198855 deletions

View file

@ -5,6 +5,7 @@ globals:
gl: false
gon: false
localStorage: false
IS_EE: false
plugins:
- import
- 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"
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
RAILS_ENV: "test"
NODE_ENV: "test"
SIMPLECOV: "true"
@ -37,6 +36,7 @@ include:
- local: .gitlab/ci/cng.gitlab-ci.yml
- local: .gitlab/ci/docs.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/qa.gitlab-ci.yml
- local: .gitlab/ci/reports.gitlab-ci.yml

View file

@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
*.scss @annabeldunstone @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 @pslaughter
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
@ -19,3 +19,5 @@ db/ @abrandl @NikolayS
/lib/gitlab/ci/templates/ @nolith @zj
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
/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.
# Useful to preview the docs changes live.
review-docs-deploy-manual:
<<: *review-docs
extends:
- .review-docs
- .no-docs-and-no-qa
stage: build
script:
- gem install gitlab --no-document
@ -21,9 +23,6 @@ review-docs-deploy-manual:
only:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
@ -66,6 +65,8 @@ docs lint:
- scripts/lint-changelog-yaml
- mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX
- cd /tmp/gitlab-docs
# Lint Markdown
- bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX -c $CI_PROJECT_DIR/.mdlrc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links

View file

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

View file

@ -28,23 +28,39 @@
policy: pull
stage: test
.dedicated-no-docs-pull-cache-job:
extends: .dedicated-pull-cache-job
.no-docs:
except:
refs:
- /(^docs[\/-].*|.*-docs$)/
.dedicated-no-docs-and-no-qa-pull-cache-job:
extends: .dedicated-pull-cache-job
.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:
extends:
- .dedicated-pull-cache-job
- .no-docs-and-no-qa
# Jobs that do not need a DB
.dedicated-no-docs-no-db-pull-cache-job:
extends: .dedicated-no-docs-pull-cache-job
variables:
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:
extends: .dedicated-runner
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
services:
- name: postgres:9.6
- name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
@ -10,11 +10,6 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
.use-mysql: &use-mysql
services:
- mysql:5.7
- redis:alpine
.only-schedules-master: &only-schedules-master
only:
- schedules@gitlab-org/gitlab-ce
@ -25,8 +20,9 @@
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
extends:
- .dedicated-no-docs-and-no-qa-pull-cache-job
- .use-pg
variables:
SETUP_DB: "false"
script:
@ -48,7 +44,9 @@
- bundle exec rake $CI_JOB_NAME
.rspec-metadata: &rspec-metadata
extends: .dedicated-pull-cache-job
extends:
- .dedicated-pull-cache-job
- .no-docs-and-no-qa
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@ -68,6 +66,8 @@
- scripts/gitaly-test-spawn
- date
- '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"
- date
artifacts:
@ -79,11 +79,9 @@
- rspec_flaky/
- rspec_profiling/
- tmp/capybara/
reports:
junit: junit_rspec.xml
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
- tmp/memory_test/
# reports:
# junit: junit_rspec.xml
.rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata
@ -94,10 +92,6 @@
<<: *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"
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
@ -131,8 +125,10 @@
- setup-test-env
setup-test-env:
extends: .dedicated-runner-default-cache
<<: *use-pg
extends:
- .dedicated-runner-default-cache
- .no-docs
- .use-pg
stage: prepare
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
@ -143,8 +139,6 @@ setup-test-env:
- tmp/tests
- config/secrets.yml
- vendor/gitaly-ruby
except:
- /(^docs[\/-].*|.*-docs$)/
rspec unit pg:
<<: *rspec-metadata-pg
@ -173,42 +167,6 @@ rspec system pg-10:
<<: *only-schedules-master
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-metadata-pg
script:
@ -226,16 +184,11 @@ rspec quarantine pg:
<<: *rspec-quarantine
allow_failure: true
rspec quarantine mysql:
<<: *rspec-metadata-mysql
<<: *rspec-quarantine
<<: *only-schedules-master
allow_failure: true
static-analysis:
extends: .dedicated-no-docs-no-db-pull-cache-job
dependencies:
- compile-assets
- compile-assets pull-cache
- setup-test-env
script:
- scripts/static-analysis
@ -250,6 +203,7 @@ static-analysis:
downtime_check:
<<: *rake-exec
except:
refs:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
@ -262,6 +216,7 @@ ee_compat_check:
<<: *rake-exec
dependencies: []
except:
refs:
- master
- tags
- /[\d-]+-stable(-ee)?/
@ -280,10 +235,6 @@ db:migrate:reset-pg:
<<: *db-migrate-reset
<<: *use-pg
db:migrate:reset-mysql:
<<: *db-migrate-reset
<<: *use-mysql
db:check-schema-pg:
<<: *db-migrate-reset
<<: *use-pg
@ -294,15 +245,11 @@ migration:path-pg:
<<: *migration-paths
<<: *use-pg
migration:path-mysql:
<<: *migration-paths
<<: *use-mysql
.db-rollback: &db-rollback
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
- bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
dependencies:
- setup-test-env
@ -310,26 +257,18 @@ db:rollback-pg:
<<: *db-rollback
<<: *use-pg
db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
gitlab:setup-pg:
<<: *gitlab-setup
<<: *use-pg
dependencies:
- setup-test-env
gitlab:setup-mysql:
<<: *gitlab-setup
<<: *use-mysql
dependencies:
- setup-test-env
coverage:
# 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
extends: .dedicated-runner-default-cache
extends:
- .dedicated-runner-default-cache
- .no-docs-and-no-qa
cache:
policy: pull
variables:
@ -337,6 +276,7 @@ coverage:
stage: post-test
script:
- bundle exec scripts/merge-simplecov
- bundle exec scripts/gather-test-memory-data
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts:
name: coverage
@ -344,6 +284,4 @@ coverage:
paths:
- coverage/index.html
- coverage/assets/
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
- tmp/memory_test/

View file

@ -1,98 +1,26 @@
include:
- template: Code-Quality.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
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
# since we're using Docker in Docker
tags: []
before_script: []
cache: {}
dependencies: []
variables:
SETUP_DB: "false"
sast:
extends: .dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
SAST_CONFIDENCE_LEVEL: 2
DOCKER_DRIVER: overlay2
allow_failure: true
extends: .dedicated-no-docs
tags: []
before_script: []
cache: {}
dependencies: []
services:
- 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
variables:
SAST_BRAKEMAN_LEVEL: 2
dependency_scanning:
extends: .dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
extends: .dedicated-no-docs
tags: []
before_script: []
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
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
services:
- docker:stable-dind
- docker:19.03.0-dind
tags:
- gitlab-org
- docker
@ -77,6 +77,7 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base
<<: *review-base
allow_failure: true
retry: 1
stage: review
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
@ -95,10 +96,16 @@ schedule:review-build-cng:
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
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:
paths:
- review_app_url.txt
paths: [review_app_url.txt]
expire_in: 2 days
when: always
@ -108,8 +115,6 @@ review-deploy:
schedule:review-deploy:
<<: *review-deploy-base
<<: *review-schedules-only
script:
- perform_review_app_deployment
review-stop:
<<: *review-base
@ -124,11 +129,11 @@ review-stop:
script:
- source scripts/review_apps/review-apps.sh
- delete
- cleanup
.review-qa-base: &review-qa-base
<<: *review-docker
allow_failure: true
retry: 2
stage: qa
variables:
<<: *review-docker-variables
@ -169,7 +174,38 @@ review-qa-all:
script:
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json
- 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-qa-base
@ -225,11 +261,12 @@ danger-review:
except:
refs:
- master
- /^[\d-]+-stable(-ee)?$/
variables:
- $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
- $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/
script:
- git 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

View file

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

View file

@ -12,7 +12,9 @@
- rspec_profiling/
retrieve-tests-metadata:
<<: *tests-metadata-state
extends:
- .tests-metadata-state
- .no-docs-and-no-qa
stage: prepare
cache:
key: tests_metadata
@ -25,9 +27,6 @@ retrieve-tests-metadata:
- 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
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
update-tests-metadata:
<<: *tests-metadata-state
@ -69,6 +68,7 @@ flaky-examples-check:
only:
- branches
except:
refs:
- master
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/

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.
* 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
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.
- [ ] 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.
- [ ] 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.
- [ ] Update the link in `features.yml` (if applicable)
- [ ] 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:
Exclude:
- 'app/models/ci/pipeline.rb'
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb'
- 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_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/wiki_page.rb'
- 'lib/gitlab/github_import/importer/releases_importer.rb'
@ -514,7 +512,6 @@ Security/YAMLLoad:
- 'spec/config/mail_room_spec.rb'
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- 'spec/models/project_services/kubernetes_service_spec.rb'
# Offense count: 34
# Configuration parameters: EnforcedStyle.

View file

@ -2,20 +2,51 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 12.0.9
## 12.1.13
### Security (1 change)
- Upgrade pages to 1.6.3.
- Fix private feature Elasticsearch leak.
## 12.0.8
## 12.1.12
### Security (22 changes)
### Security (11 changes)
- Add a policy check for system notes that may not be visible due to cross references to private items.
- Display only participants that user has permission to see on milestone page.
- Do not disclose project milestones on group milestones page when project milestones access is disabled in project settings.
- Fix new project path being disclosed through unsubscribe link of issue/merge requests.
- Prevent bypassing email verification using Salesforce.
- Do not show resource label events referencing not accessible labels.
- Cancel all running CI jobs triggered by the user who is just blocked.
- Fix Gitaly SearchBlobs flag RPC injection.
- Only render fixed number of mermaid blocks.
- Prevent GitLab accounts takeover if SAML is configured.
- Upgrade mermaid to prevent XSS.
## 12.1.11
- No changes.
## 12.1.10
- No changes.
## 12.1.9
### Security (1 change)
- Upgrade pages to 1.7.2.
## 12.1.8
### Security (21 changes)
- 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.
- Queries for Upload should be scoped by model.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly.
@ -37,36 +68,349 @@ entry.
- Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.0.7
## 12.1.7
- Unreleased due to QA failure.
## 12.0.6
## 12.1.6
### Security (2 changes)
- Upgrade Gitaly to 1.47.2 to prevent revision flag injection exploits.
- Upgrade pages to 1.6.2 to prevent gitlab api token recovery from cookie.
- Upgrade Gitaly to 1.53.2 to prevent revision flag injection exploits.
- Upgrade pages to 1.7.1 to prevent gitlab api token recovery from cookie.
## 12.0.5
## 12.1.5
- 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)
- Restrict slash commands to users who can log in.
- Patch XSS issue in wiki links.
- Queries for Upload should be scoped by model.
- Filter merge request params on the new merge request page.
- Fix Server Side Request Forgery mitigation bypass.
- Show badges if pipelines are public otherwise default to project permissions.
- Do not allow localhost url redirection in GitHub Integration.
- 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.
## 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)
- No changes.
@ -415,6 +759,15 @@ entry.
- 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)
### Fixed (5 changes)
@ -628,6 +981,27 @@ entry.
- 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)
### 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/gitlab_ui_wg')
danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
danger.import_dangerfile(path: 'danger/only_documentation')
end

View file

@ -1 +1 @@
1.47.3
1.53.4

View file

@ -1 +1 @@
1.6.3
1.7.2

28
Gemfile
View file

@ -1,6 +1,6 @@
source 'https://rubygems.org'
gem 'rails', '5.1.7'
gem 'rails', '5.2.3'
# Improves copy-on-write performance for MRI
gem 'nakayoshi_fork', '~> 0.0.4'
@ -11,7 +11,7 @@ gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
# 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
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 'graphiql-rails', '~> 1.4.10'
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?
gem 'hashie-forbidden_attributes'
@ -99,7 +100,7 @@ gem 'carrierwave', '~> 1.3'
gem 'mini_magick'
# 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.
# Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0'
@ -129,10 +130,10 @@ gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
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-plantuml', '0.0.8'
gem 'rouge', '~> 3.1'
gem 'asciidoctor-plantuml', '0.0.9'
gem 'rouge', '~> 3.5'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.3'
@ -211,7 +212,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
# HipChat integration
gem 'hipchat', '~> 1.5.0'
# JIRA integration
# Jira integration
gem 'jira-ruby', '~> 1.4'
# Flowdock integration
@ -300,13 +301,16 @@ gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
# Memory benchmarks
gem 'derailed_benchmarks', require: false
# Metrics
group :metrics do
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.9.4'
gem 'prometheus-client-mmap', '~> 0.9.8'
gem 'raindrops', '~> 0.18'
end
@ -336,7 +340,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0'
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_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
@ -365,6 +369,7 @@ group :development, :test do
gem 'haml_lint', '~> 0.31.0', require: false
gem 'simplecov', '~> 0.16.1', 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
@ -374,7 +379,6 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '1.0'
gem 'stackprof', '~> 0.2.10', require: false
gem 'derailed_benchmarks', 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'
# SSH host key support
gem 'net-ssh', '~> 5.0'
gem 'net-ssh', '~> 5.2'
gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support
@ -427,7 +431,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly'
gem 'gitaly-proto', '~> 1.37.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0'

View file

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

View file

@ -1 +1 @@
12.0.9
12.1.13

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',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
@ -23,6 +24,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
userCountsPath: '/api/:version/user_counts',
usersPath: '/api/:version/users.json',
userPath: '/api/:version/users/:id',
userStatusPath: '/api/:version/users/:id/status',
@ -113,6 +115,21 @@ const Api = {
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
* supplied parameters
@ -296,6 +313,11 @@ const Api = {
});
},
userCounts() {
const url = Api.buildUrl(this.userCountsPath);
return axios.get(url);
},
userStatus(id, options) {
const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {

View file

@ -102,7 +102,7 @@ class SafeMathRenderer {
maxSize: 20,
maxExpand: 20,
});
} catch {
} catch (e) {
// Don't show a flash for now because it would override an existing flash message
el.textContent = s__('math|There was an error rendering this math block');
// el.style.color = '#d00';

View file

@ -33,17 +33,21 @@ export default function renderMermaid($els) {
flowchart: {
htmlLabels: false,
},
securityLevel: 'strict',
});
let renderedChars = 0;
$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
* prevent mermaidjs from hanging up the entire thread and
* causing a DoS.
*/
if (source && source.length > MAX_CHAR_LIMIT) {
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
el.textContent = sprintf(
__(
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
@ -53,12 +57,21 @@ export default function renderMermaid($els) {
return;
}
renderedChars += source.length;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, 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');
// pre > code > svg

View file

@ -1,6 +1,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import { n__ } from '~/locale';
import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
@ -53,12 +54,19 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
caretTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() {
return (
this.list.type === 'backlog' ||
(!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: {
filter: {
@ -72,31 +80,34 @@ export default Vue.extend({
},
},
mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: e => {
onEnd(e) {
sortableEnd();
const sortable = this;
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));
this.$nextTick(() => {
instance.$nextTick(() => {
boardsStore.moveList(list, order);
});
}
},
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
Sortable.create(this.$el.parentNode, sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
@ -105,16 +116,17 @@ export default Vue.extend({
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
toggleExpanded() {
if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
localStorage.setItem(`${this.uniqueKey}.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>
import { __ } from '~/locale';
/* global ListLabel */
import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
@ -7,8 +8,8 @@ export default {
data() {
return {
predefinedLabels: [
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' }),
new ListLabel({ title: __('To Do'), color: '#F0AD4E' }),
new ListLabel({ title: __('Doing'), color: '#5CB85C' }),
],
};
},
@ -58,7 +59,11 @@ export default {
<template>
<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">
<li v-for="(label, index) in predefinedLabels" :key="index">
<span
@ -70,18 +75,21 @@ export default {
</li>
</ul>
<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>
<button
class="btn btn-success btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists"
>
Add default lists
{{ s__('BoardBlankState|Add default lists') }}
</button>
<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>
</div>
</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="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 />
</div>
<board-new-issue
@ -257,7 +257,7 @@ export default {
/>
<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" />
<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>
</li>
</ul>

View file

@ -102,9 +102,9 @@ export default {
<div class="board-card position-relative p-3 rounded">
<form @submit="submit($event)">
<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>
<label :for="list.id + '-title'" class="label-bold"> Title </label>
<label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label>
<input
:id="list.id + '-title'"
ref="input"
@ -122,12 +122,11 @@ export default {
class="float-left"
variant="success"
type="submit"
>{{ __('Submit issue') }}</gl-button
>
Submit issue
</gl-button>
<gl-button class="float-right" type="button" variant="default" @click="cancel">
Cancel
</gl-button>
<gl-button class="float-right" type="button" variant="default" @click="cancel">{{
__('Cancel')
}}</gl-button>
</div>
</form>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -123,7 +123,9 @@ export default {
class="empty-state add-issues-empty-state-filter text-center"
>
<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 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">

View file

@ -1,4 +1,5 @@
<script>
import { __ } from '~/locale';
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
@ -27,7 +28,7 @@ export default {
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
return this.selectedProject.name || __('Select a project');
},
},
mounted() {
@ -81,7 +82,7 @@ export default {
<template>
<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">
<button
class="dropdown-menu-toggle wide"
@ -92,9 +93,9 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<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">
<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" />
</div>
<div class="dropdown-content"></div>

View file

@ -76,7 +76,7 @@ export default Vue.extend({
<template>
<div class="block list">
<button class="btn btn-default btn-block" type="button" @click="removeIssue">
Remove from board
{{ __('Remove from board') }}
</button>
</div>
</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 FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
import { isEE } from '~/lib/utils/common_utils';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@ -10,7 +9,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
isGroup: isEE(),
isGroup: IS_EE,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});

View file

@ -6,28 +6,31 @@ import { __ } from '~/locale';
import './models/label';
import './models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import './models/issue';
import './models/list';
import './models/milestone';
import './models/project';
import boardsStore from './stores/boards_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
import modalMixin from './mixins/modal_mixins';
import './filters/due_date_filters';
import Board from './components/board';
import BoardSidebar from './components/board_sidebar';
import initNewListDropdown from './components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import '~/boards/models/milestone';
import '~/boards/models/project';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
import BoardService from 'ee_else_ce/boards/services/board_service';
import modalMixin from '~/boards/mixins/modal_mixins';
import '~/boards/filters/due_date_filters';
import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} 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;
@ -49,6 +52,7 @@ export default () => {
}
boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
issueBoardsApp = new Vue({
el: $boardApp,
@ -77,13 +81,14 @@ export default () => {
},
},
created() {
gl.boardService = new BoardService({
boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
gl.boardService = new BoardService();
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
@ -204,6 +209,8 @@ export default () => {
},
});
boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn');
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 */
import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config';
import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() {
$('.has-tooltip')
@ -20,7 +20,7 @@ export function getBoardSortableDefaultOptions(obj) {
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,

View file

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

View file

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

View file

@ -1,11 +1,9 @@
import { isEE } from '~/lib/utils/common_utils';
export default class ListMilestone {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
if (isEE) {
if (IS_EE) {
this.path = obj.path;
this.state = obj.state;
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';
import { mergeUrlParams } from '../../lib/utils/url_utility';
/* eslint-disable class-methods-use-this */
import boardsStore from '~/boards/stores/boards_store';
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) {
return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
return boardsStore.generateBoardsPath(id);
}
generateIssuesPath(id) {
return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
return boardsStore.generateIssuesPath(id);
}
static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
return boardsStore.generateIssuePath(boardId, id);
}
all() {
return axios.get(this.listsEndpoint);
return boardsStore.all();
}
generateDefaultLists() {
return axios.post(this.listsEndpointGenerate, {});
return boardsStore.generateDefaultLists();
}
createList(entityId, entityType) {
const list = {
[entityType]: entityId,
};
return axios.post(this.listsEndpoint, {
list,
});
return boardsStore.createList(entityId, entityType);
}
updateList(id, position) {
return axios.put(`${this.listsEndpoint}/${id}`, {
list: {
position,
},
});
return boardsStore.updateList(id, position);
}
destroyList(id) {
return axios.delete(`${this.listsEndpoint}/${id}`);
return boardsStore.destroyList(id);
}
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
return boardsStore.getIssuesForList(id, filter);
}
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(BoardService.generateIssuePath(this.boardId, id), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
}
newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), {
issue,
});
return boardsStore.newIssue(id, issue);
}
getBacklog(data) {
return axios.get(
mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
);
return boardsStore.getBacklog(data);
}
bulkUpdate(issueIds, extraData = {}) {
const data = {
update: Object.assign(extraData, {
issuable_ids: issueIds.join(','),
}),
};
return axios.post(this.bulkUpdatePath, data);
return boardsStore.bulkUpdate(issueIds, extraData);
}
static getIssueInfo(endpoint) {
return axios.get(endpoint);
return boardsStore.getIssueInfo(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 { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub';
const boardsStore = {
disabled: false,
timeTracking: {
limitToHours: false,
},
scopedLabels: {
helpLink: '',
enabled: false,
@ -25,6 +30,7 @@ const boardsStore = {
},
currentPage: '',
reload: false,
endpoints: {},
},
detail: {
issue: {},
@ -33,6 +39,19 @@ const boardsStore = {
issue: {},
list: {},
},
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = {
boardsEndpoint,
boardId,
listsEndpoint,
listsEndpointGenerate,
bulkUpdatePath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
@ -222,6 +241,143 @@ const boardsStore = {
setIssueDetail(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);

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 __('Updated');
return this.updateSuccessful ? __('Updated to') : __('Updated');
},
updateFailureDescription() {
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"
>
{{ versionLabel }}
<span v-if="updateSuccessful">to</span>
<gl-link
v-if="updateSuccessful"
:href="chartRepo"

View file

@ -2,7 +2,7 @@
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants';
@ -32,7 +32,7 @@ export default {
return [UPDATING].includes(this.knative.status);
},
saveButtonLabel() {
return this.saving ? this.__('Saving') : this.__('Save changes');
return this.saving ? __('Saving') : __('Save changes');
},
knativeInstalled() {
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.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<p

View file

@ -1,6 +1,7 @@
<script>
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
@ -22,7 +23,7 @@ export default {
return this.status === UNINSTALLING;
},
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.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[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 {

View file

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

View file

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

View file

@ -17,6 +17,7 @@ Vue.use(Translate);
export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
// eslint-disable-next-line no-new
new Vue({
@ -33,7 +34,6 @@ export default () => {
'stage-production-component': stageComponent,
},
data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
@ -56,7 +56,13 @@ export default () => {
},
},
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: {
handleError() {

View file

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

View file

@ -49,6 +49,8 @@ export default {
return this.author.id ? this.author.id : '';
},
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}`;
},
authorAvatar() {

View file

@ -1,6 +1,6 @@
<script>
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';
export default {
@ -54,11 +54,7 @@ export default {
},
methods: {
commitsText(version) {
return n__(
`${version.commits_count} commit,`,
`${version.commits_count} commits,`,
version.commits_count,
);
return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
@ -76,7 +72,7 @@ export default {
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
return `version ${version.version_index}`;
return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
@ -125,9 +121,9 @@ export default {
<div>
<strong>
{{ versionName(version) }}
<template v-if="isBase(version)">
(base)
</template>
<template v-if="isBase(version)">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
</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)"
:discussion="discussion"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
:line="line"
:help-page-path="helpPagePath"

View file

@ -67,6 +67,18 @@ export default {
errorMessage() {
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: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
@ -150,22 +162,18 @@ export default {
/>
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note">
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>
<span class="file-fork-suggestion-note" v-html="forkMessage"></span>
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
>Fork</a
>{{ __('Fork') }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
Cancel
{{ __('Cancel') }}
</button>
</div>
<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);
},
methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
...mapActions('diffs', [
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
handleToggleFile(e, checkTarget) {
if (
!checkTarget ||
@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage');
},
handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile);
this.toggleFileDiscussionWrappers(this.diffFile);
},
handleFileNameClick(e) {
const isLinkToOtherPage =

View file

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

View file

@ -105,7 +105,13 @@ export default {
},
},
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
...mapActions('diffs', [
'loadMoreLines',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
'toggleLineDiscussionWrappers',
]),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)"
>
</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>
</div>
</template>

View file

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

View file

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

View file

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

View file

@ -58,6 +58,8 @@ export default {
:diff-file-hash="diffFile.file_hash"
:line-index="index"
: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
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"

View file

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

View file

@ -12,6 +12,7 @@ import {
getNoteFormData,
convertExpandLines,
idleCallback,
allDiscussionWrappersExpanded,
} from './utils';
import * as types from './mutation_types';
import {
@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions
.filter(discussion => discussion.diff_discussion)
@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion,
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 });
};
export const toggleLineDiscussions = ({ commit }, options) => {
commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};
export const renderFileForDiscussionId = ({ commit, rootState, state }, 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 }) => {
const postData = getNoteFormData({
commit: state.commit,
@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.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 SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';

View file

@ -6,6 +6,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
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 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 => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
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) {
return {
...line,
left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null,
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.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 }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
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),
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, {
discussions: line.discussions.filter(discussion => discussion.notes.length),
}),
);
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) {
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) {
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 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"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
:deploy-boards-help-path="deployBoardsHelpPath"
/>
<table-pagination

View file

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

View file

@ -1,4 +1,5 @@
<script>
import { __, sprintf } from '~/locale';
import Timeago from 'timeago.js';
import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
@ -14,7 +15,6 @@ import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { CLUSTER_TYPE } from '~/clusters/constants';
/**
* Environment Item Component
@ -79,15 +79,6 @@ export default {
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.
*
@ -172,7 +163,9 @@ export default {
this.model.last_deployment.user &&
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 '';
},
@ -293,6 +286,9 @@ export default {
* @returns {Boolean|Undefined}
*/
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?'];
},
@ -575,7 +571,6 @@ export default {
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
:disabled="disableGroupClusterFeatures"
/>
<rollback-component

View file

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

View file

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

View file

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

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